Пришло время
поближе познакомиться со следующим китом ООП по имени наследование.
Что это такое?
Давайте рассмотрим этот механизм на, уже ставшим хрестоматийным, примере
описания графических примитивов: линии, треугольника, прямоугольника и эллипса.
Разумеется, каждый примитив будет описываться своим классом:
-
class
Line {} – для
линий;
-
class
Triangle {} – для
треугольников;
-
class
Rectangle {} – для
прямоугольников;
-
class
Ellipse {} – для
эллипсов.
Также очевидно,
что каждый из этих классов должен хранить координаты вершин для представления
примитива:
class Line {
double x1, y1;
double x2, y2;
}
class Triangle {
double x1, y1;
double x2, y2;
double x3, y3;
}
class Rectangle {
double x1, y1;
double x2, y2;
}
class Ellipse {
double x1, y1;
double x2, y2;
}
И, потом, нам
заказчик ставит задачу: у каждого примитива должна быть своя толщина линии и
цвет. Что мы можем сделать, имея текущие знания по ООП? Да, добавить в каждый
класс соответствующие поля:
И получаем
дублирование кода. Чтобы выйти из этой ситуации, как раз и нужен наш синий кит
по имени наследование. Мы можем сделать так. Создать отдельный класс, в котором
определим эти два общих свойства:
А, затем, унаследуем
от него классы наших графических примитивов. Наследование классов в Java реализуется с
помощью синтаксиса:
class A extends B {
// тело класса
}
Здесь класс A называется
дочерним или производным классом, а класс B – базовым или
родительским, или суперклассом. И в нашем случае наследование следует записать так:
В результате,
получим следующую иерархию классов:
Благодаря тому,
что у всех четырех классов родительский класс Properties содержит
общедоступные поля width, color, дочерние
классы графических примитивов наследуют эту информацию и включают ее в свой
состав. То есть, создавая объект, например, класса Line:
Мы видим, что он
содержит также и поля width и color:
l1.color = 1;
l1.width = 5;
помимо
координат, которые определены непосредственно в классе Line:
l1.x1 = l1.x2 = 0;
l1.y1 = l1.y2 = 10;
Вот так механизм
наследования позволяет брать (наследовать) поля, а также методы базового класса
и образовывать более сложный объект. Что же происходит, когда мы создаем
объекты дочерних классов? Например, в случае с классом Line, фактически,
создается экземпляр класса Line, включающий в себя еще и базовый класс Properties:
И,
действительно, если в обоих классах прописать свои конструкторы:
То в консоли
увидим:
Конструктор Properties
Конструктор Line
Это как раз и
говорит о том, что сначала создается объект класса Properties, а уже потом –
объект класса Line и получается такой составной объект. Отсюда мы получаем одно
важное следствие при работе с такими экземплярами: можно обратиться к Properties,
который содержится в Line, используя операцию приведения типов:
или, так:
(приведение
типов сработает автоматически). Здесь p все еще ссылка
на объект Line, но через нее
мы можем работать только с элементами класса Properties:
p.color = 1;
p.width = 5;
Обратиться к
координатам уже не получится:
p.x1 = -1; // ошибка, такого поля в Properties нет
Конечно, сейчас
вы можете подумать: все это хорошо, но зачем это надо, когда через ссылку l1 можно
оперировать всеми данными без исключения? Да, в этом простом примере именно так
и есть, операция избыточна. Но у нас же могут быть и другие графические
примитивы. И, смотрите, для единообразного хранения ссылок на них, можно
создать вот такой динамический массив:
а, затем,
создать четыре разных объекта:
geom[0] = new Line();
geom[1] = new Triangle();
geom[2] = new Rectangle();
geom[3] = new Ellipse();
Видите, как это
удобно использовать единый интерфейс – класс Properties для хранения
нарисованных объектов. Более подробно об управлении дочерними объектами через
базовый класс Properties мы поговорим в
теме полиморфизм.
Итак, мы увидели,
как представляется дочерний объект в памяти устройства и в каком порядке
вызываются конструкторы базовых и дочерних классов: сверху-вниз – от самого
первого родительского вниз до последнего дочернего:
Но, глядя на
этот рисунок, у вас здесь может возникнуть еще один каверзный вопрос: а что
если дочерний класс будет наследоваться сразу от двух родительских? К счастью в
Java такой проблемы
нет, так как наследование можно выполнять одновременно только от одного класса,
то есть, множественное наследование запрещено и максимум, что можно сделать –
это реализовать цепочку из классов, как показано на рисунке. Забегая вперед,
скажу, что отсутствие механизма множественного наследования компенсируется
специальными конструкциями под названием интерфейсы. О них мы тоже будем
говорить, но позже. Ну а реализовать цепочку наследования из классов можно,
например, так:
Здесь на вершине
иерархии стоит класс Geom, далее класс Properties и в конце –
класс Line.
Путь кодера
Подвиг 1. Объявите классы
для описания мебели: стулья, шкафы, полки, столы. У этих классов имеются общие
поля: название, габариты, цена. И уникальные для каждого объекта:
-
для
стула: число ножек, высота ножек, наличие спинки;
-
для
шкафов: материал ручек, число створок и шкафчиков;
-
для
полок: число сегментов и размер каждого сегмента;
-
для
столов: число ножек и площадь столешницы.
Подумайте, как
описать эти объекты. Создайте их и выведите значение полей в консоль.
Подвиг 2. Требуется
создать описание окна приложения для разных устройств: смартфонов, планшетов и
настольных компьютеров (десктопов). Окно имеет общие параметры: заголовок,
шрифт, наличие/отсутствие рамки. И уникальные для каждого устройства:
-
для
смартфонов: ничего (все берется из базового класса);
-
для
планшетов: положение и размер окна;
-
для
десктопов: положение и размер окна, возможность менять размеры, полноэкранный
режим.
Подумайте, как
описать эти классы. Создайте экземпляры классов для каждого устройства и
выведите значение полей в консоль.