Ключевое слово super, оператор instanceof

На предыдущем занятии мы с вами рассмотрели основу механизма наследования. Здесь затронем некоторые важные нюансы этого процесса и начнем с рассмотрения ключевого слова super.

Ключевое слово super

Часто в базовых классах имеются несколько конструкторов. Как в этом случае узнать, какой из конструкторов будет вызван при создании объекта дочернего класса? Например, объявим два класса:

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

Здесь в базовом классе Properties два конструктора. Если теперь выполнить создание объекта дочернего класса Line:

Line line = new Line();

то в консоли увидим строчки:

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

Это говорит о том, что при создании объекта базового класса, вызывается или конструктор без аргументов, или конструктор по умолчанию. Причем, конструктор по умолчанию будет существовать, только если в классе не объявлены никакие другие конструкторы. То есть, если конструктор Properties() закомментировать и оставить только второй, то объект дочернего класса Line не будет создан, возникнет ошибка, т.к. отсутствует конструктор без аргументов. А вот если в комментарии поставить и второй конструктор, то все заработает благодаря появлению конструктора по умолчанию.

Но, что если мы хотим воспользоваться вторым конструктором базового класса с двумя аргументами? В этом случае, при вызове конструктора дочернего класса нужно явно указать какой конструктор базового класса следует использовать. Это делается с помощью ключевого слова super, следующим образом:

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

Здесь в конструкторе Line первой строчкой идет вызов конструктора базового класса с двумя аргументами. Обратите внимание, вызывать конструктор базового класса следует сразу же в первой строчке, т.к. сначала должен быть создан объект базового класса, а затем уже, объект дочернего класса.

При запуске программы увидим две строчки:

Конструктор Properties(width, color)
Конструктор Line()

Причем, если у нас цепочка из нескольких классов, унаследованных друг от друга, то super будет ссылаться на ближайший базовый класс:

Ключевое слово super в роли ссылки

После создания объекта базового класса, ключевое слово super можно использовать как ссылку на этот объект. Например, определим в базовом классе целочисленное поле id со значением 1:

int id = 1;

и точно такое же поле в дочернем классе, но со значением 2:

int id = 2;

Как теперь из дочернего класса обратиться к id базового класса? Да, это можно сделать через ссылку super:

class Line extends Properties {
    int id = 2;
...
 
    void showId() {
        System.out.println("id = "+ id + ", super.id = " + super.id);
    }
}

При вызове этого метода, в консоли увидим:

id = 2, super.id = 1

Также, если определить метод showId() в базовом классе:

class Properties {
    int id = 1;
...
    void showId() {
        System.out.println("id = "+ id);
    }
}

то мы можем к нему обратиться из дочернего через ссылку super:

class Line extends Properties {
    int id = 2;
...
    void showId() {
        super.showId();
        System.out.println("id = "+ id + ", super.id = " + super.id);
    }
}

Вот так можно вызывать переопределенные поля и методы из объекта базового класса, используя ссылку super.

Оператор instanceof

В действительности в Java любой класс автоматически наследуется от предопределенного класса Object. То есть, иерархию наследования в нашем примере следовало бы изобразить вот так:

И объекты дочерних классов можно было бы создавать, используя ссылку типа Object:

Object g1 = new Line();
Object g2 = new Triangle();
Object g3 = new Properties();

При этом тип данных для ссылок g1, g2, g3 будет автоматически приведен к типу Object, т.к. это базовый суперкласс для всех этих объектов. Это, так называемое, восходящее преобразование (по-английски upcasting). Но мы можем выполнять и нисходящие преобразования: от ссылок на базовые классы к ссылкам на дочерние классы. Такое обратное преобразование типов автоматически уже не происходит и мы должны явно указать, какой тип дочернего класса нам нужен. Например, так:

Line l1 = (Line)g1;
Line l2 = (Line)g2;     // ошибка - это класс Triangle
Properties p1 = (Properties)g3;

И смотрите, что может произойти. Когда мы пытаемся ссылку g2 привести к типу Line, то в процессе выполнения программы возникнет исключение (ошибка), что класс Triangle не может быть приведен к классу Line. И это логично, т.к. оба класса – самостоятельные дочерние, унаследованные от Properties. Нельзя один подменить другим. Но, хорошо, мы в этой простой программе знаем, что g2 – это ссылка на Triangle. А как быть в сложных проектах, когда имеется обобщенная ссылка и нам важно знать, какие дочерние классы включены в объект, на который ссылается g2? Для таких случаев существует оператор

<ссылка> instanceof <имя класса>

который возвращает true, если имя дочернего класса содержится в объекте, по указанной ссылке. Например, прежде чем выполнять нисходящее преобразование типов, было бы правильно проверить, а содержится ли объект дочернего класса в объекте, к которому мы хотим привести тип данных:

        Line l1 = null, l2 = null;
        if(g1 instanceof Line)
            l1 = (Line)g1;
 
        if(g2 instanceof Line)
            l2 = (Line)g2;     // это класс Triangle
 
        Properties p1 = null;
        if(g3 instanceof Properties)
            p1 = (Properties)g3;

Теперь, при запуске программы у нас никаких ошибок не будет, т.к. проверка g2 instanceof Line будет ложной и преобразование не выполняется.

Путь кодера

Подвиг 1. Объявите базовый класс Stationery для описания канцтоваров с полями: цена, наличие на складе, идентификатор. В этом классе реализовать два конструктора: без аргументов и с тремя аргументами для инициализации его полей. Затем, объявить производные от него классы для описания: ручек, карандашей, тетрадей. В дочерних классах реализовать несколько конструкторов с вызовом конструктора базового класса без аргументов и с аргументами. Также в производных классах придумайте разные поля (разные у разных классов) и пропишите методы в соответствующих классах:

  • displayPen() – для отображения данных по ручкам;
  • displayPencil() – для отображения данных по карандашам;
  • displayNotebook() – для отображения данных по тетрадям.

Создайте объекты дочерних классов и выведите информацию по ним в консоль.

Подвиг 2. Используя классы из первого подвига, создайте динамический массив со ссылками Object на экземпляры дочерних классов. Через эти ссылки вызовите методы displayPen(),displayPencil() и displayNotebook() дочерних классов (используя нисходящее приведение типов).

Видео по теме