Как делается наследование классов

Пришло время поближе познакомиться со следующим китом ООП по имени наследование.

Что это такое? Давайте рассмотрим этот механизм на, уже ставшим хрестоматийным, примере описания графических примитивов: линии, треугольника, прямоугольника и эллипса. Разумеется, каждый примитив будет описываться своим классом:

  • 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;
}

И, потом, нам заказчик ставит задачу: у каждого примитива должна быть своя толщина линии и цвет. Что мы можем сделать, имея текущие знания по ООП? Да, добавить в каждый класс соответствующие поля:

И получаем дублирование кода. Чтобы выйти из этой ситуации, как раз и нужен наш синий кит по имени наследование. Мы можем сделать так. Создать отдельный класс, в котором определим эти два общих свойства:

class Properties {
    int width, color;
}

А, затем, унаследуем от него классы наших графических примитивов. Наследование классов в Java реализуется с помощью синтаксиса:

class A extends B {
   // тело класса
}

Здесь класс A называется дочерним или производным классом, а класс B – базовым или родительским, или суперклассом. И в нашем случае наследование следует записать так:

class Line extends Properties { ... }
class Triangle extends Properties { ... }
class Rectangle extends Properties { ... }
class Ellipse extends Properties { ... }

В результате, получим следующую иерархию классов:

Благодаря тому, что у всех четырех классов родительский класс Properties содержит общедоступные поля width, color, дочерние классы графических примитивов наследуют эту информацию и включают ее в свой состав. То есть, создавая объект, например, класса Line:

Line l1 = new Line();

Мы видим, что он содержит также и поля width и color:

l1.color = 1;
l1.width = 5;

помимо координат, которые определены непосредственно в классе Line:

l1.x1 = l1.x2 = 0;
l1.y1 = l1.y2 = 10;

Вот так механизм наследования позволяет брать (наследовать) поля, а также методы базового класса и образовывать более сложный объект. Что же происходит, когда мы создаем объекты дочерних классов? Например, в случае с классом Line, фактически, создается экземпляр класса Line, включающий в себя еще и базовый класс Properties:

И, действительно, если в обоих классах прописать свои конструкторы:

class Properties {
    int width, color;
 
    Properties() {
        System.out.println("Конструктор Properties");
    }
}
 
class Line extends Properties {
    double x1, y1;
    double x2, y2;
 
    Line() {
        System.out.println("Конструктор Line");
    }
}

То в консоли увидим:

Конструктор Properties
Конструктор Line

Это как раз и говорит о том, что сначала создается объект класса Properties, а уже потом – объект класса Line и получается такой составной объект. Отсюда мы получаем одно важное следствие при работе с такими экземплярами: можно обратиться к Properties, который содержится в Line, используя операцию приведения типов:

или, так:

Properties p = l1;

(приведение типов сработает автоматически). Здесь p все еще ссылка на объект Line, но через нее мы можем работать только с элементами класса Properties:

p.color = 1;
p.width = 5;

Обратиться к координатам уже не получится:

p.x1 = -1;   // ошибка, такого поля в Properties нет

Конечно, сейчас вы можете подумать: все это хорошо, но зачем это надо, когда через ссылку l1 можно оперировать всеми данными без исключения? Да, в этом простом примере именно так и есть, операция избыточна. Но у нас же могут быть и другие графические примитивы. И, смотрите, для единообразного хранения ссылок на них, можно создать вот такой динамический массив:

Properties geom[] = new Properties[4];

а, затем, создать четыре разных объекта:

geom[0] = new Line();
geom[1] = new Triangle();
geom[2] = new Rectangle();
geom[3] = new Ellipse();

Видите, как это удобно использовать единый интерфейс – класс Properties для хранения нарисованных объектов. Более подробно об управлении дочерними объектами через базовый класс Properties мы поговорим в теме полиморфизм.

Итак, мы увидели, как представляется дочерний объект в памяти устройства и в каком порядке вызываются конструкторы базовых и дочерних классов: сверху-вниз – от самого первого родительского вниз до последнего дочернего:

Но, глядя на этот рисунок, у вас здесь может возникнуть еще один каверзный вопрос: а что если дочерний класс будет наследоваться сразу от двух родительских? К счастью в Java такой проблемы нет, так как наследование можно выполнять одновременно только от одного класса, то есть, множественное наследование запрещено и максимум, что можно сделать – это реализовать цепочку из классов, как показано на рисунке. Забегая вперед, скажу, что отсутствие механизма множественного наследования компенсируется специальными конструкциями под названием интерфейсы. О них мы тоже будем говорить, но позже. Ну а реализовать цепочку наследования из классов можно, например, так:

class Geom {
    int id;
}
 
class Properties extends Geom { ... }
class Line extends Properties { ... }

Здесь на вершине иерархии стоит класс Geom, далее класс Properties и в конце – класс Line.

Путь кодера

Подвиг 1. Объявите классы для описания мебели: стулья, шкафы, полки, столы. У этих классов имеются общие поля: название, габариты, цена. И уникальные для каждого объекта:

  • для стула: число ножек, высота ножек, наличие спинки;
  • для шкафов: материал ручек, число створок и шкафчиков;
  • для полок: число сегментов и размер каждого сегмента;
  • для столов: число ножек и площадь столешницы.

Подумайте, как описать эти объекты. Создайте их и выведите значение полей в консоль.

Подвиг 2. Требуется создать описание окна приложения для разных устройств: смартфонов, планшетов и настольных компьютеров (десктопов). Окно имеет общие параметры: заголовок, шрифт, наличие/отсутствие рамки. И уникальные для каждого устройства:

  • для смартфонов: ничего (все берется из базового класса);
  • для планшетов: положение и размер окна;
  • для десктопов: положение и размер окна, возможность менять размеры, полноэкранный режим.

Подумайте, как описать эти классы. Создайте экземпляры классов для каждого устройства и выведите значение полей в консоль.

Видео по теме