Перейти к содержимому
Теория · Наследование и полиморфизм · урок 1 из 10

Наследование

Чему вы научитесь

Чему вы научитесь:

  • объявлять класс-наследник через extends и вызывать конструктор родителя через super(…);
  • предсказывать, чей метод выполнит вызов a.sound(), и отличать переопределение от перегрузки;
  • объяснять, зачем классу и методу слова abstract и final;
  • приводить ссылку к типу наследника и проверять тип объекта через instanceof.

Представьте телевизор и пульт к нему. На пульте две кнопки: «Вкл» и «Громкость». Через год фирма выпускает новую модель. Она умеет всё, что умела прежняя, и вдобавок записывает передачи. Новую модель не проектируют с нуля: берут прежнюю и добавляют одну функцию.

Поэтому старый пульт подходит и к новому телевизору. Каждую его кнопку новая модель понимает: всё прежнее в ней осталось.

В Java так устроено наследование: новый класс получает поля и методы готового класса и добавляет к ним свои.

Зачем это нужно? Допустим, в программе есть собаки и кошки, и те и другие едят. Без наследования метод eat пришлось бы написать в обоих классах, а найденную в нём ошибку — исправлять дважды. С наследованием общее пишут один раз, в классе Animal:

JAVA
public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.eat();  // eats
        d.bark(); // Woof
    }
}

class Animal {
    void eat() { System.out.println("eats"); }
}

class Dog extends Animal {
    void bark() { System.out.println("Woof"); }
}

Давайте разберём программу по шагам:

  1. Класс Animal описывает то, что умеет любое животное: метод eat. Тело метода здесь записано в одну строку, чтобы пример был короче.
  2. Запись class Dog extends Animal объявляет класс Dog наследником Animal. Слово extends значит «расширяет».
  3. В самом Dog написан только метод bark.
  4. new Dog() строит собаку, и d.eat() печатает eats: этот метод собака получила от Animal.
  5. d.bark() печатает Woof — это собственный метод Dog.

Родитель и наследник

Наследование — способ объявить новый класс на основе существующего: новый класс получает поля и методы старого. Класс, от которого наследуют, называют суперклассом или родителем, а новый класс — подклассом или наследником. Здесь Animal — суперкласс, а Dog — подкласс.

Наследование описывает отношение «является»: собака является животным. Поэтому всё, что умеет Animal, умеет и Dog. Обратное неверно: у Animal нет метода bark.

Что наследник получает, а что нет:

  • поля и методы родителя он получает: у объекта Dog к ним обращаются, как к собственным;
  • private-поля и методы родителя видны только внутри родителя: если обратиться из Dog к private-полю name класса Animal, компилятор ответит name has private access in Animal;
  • конструкторы не наследуются: у каждого класса свои.

Конструктор родителя: super(…)

Собака — это животное плюс своё. Поэтому, когда строится объект Dog, сначала должна отработать часть, которую описывает Animal. Если у Animal есть конструктор с параметром, наследник вызывает его словом super:

JAVA
public class Main {
    public static void main(String[] args) {
        Dog d = new Dog("Rex");
        System.out.println(d.name); // Rex
    }
}

class Animal {
    String name;
    Animal(String name) { this.name = name; }
}

class Dog extends Animal {
    Dog(String name) { super(name); }
}

Разберём по шагам:

  1. new Dog("Rex") вызывает конструктор Dog с аргументом "Rex".
  2. Первая инструкция этого конструктора, super(name), вызывает конструктор Animal и передаёт ему "Rex".
  3. Конструктор Animal кладёт имя в поле name, и управление возвращается в конструктор Dog.
  4. d.name печатает Rex: поле объявлено в Animal, а заполнено через super.

Правила у super(…) такие:

  • его пишут первой инструкцией конструктора, как и this(…); Java 25 разрешает перед ним некоторые инструкции, но код курса компилируется и на Java 21;
  • если super(…) не написать, компилятор сам вставит в начало конструктора super() — вызов конструктора родителя без аргументов;
  • поэтому конструктор родителя всегда заканчивает работу раньше, чем выполняется код наследника после super(…).

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

Один родитель и класс Object

У класса Java ровно один родитель: запись class Dog extends Animal, Pet не скомпилируется. Зато цепочка может быть длинной: класс Puppy extends Dog получит всё и от Dog, и от Animal.

А если extends не написать, родителем станет класс Object. Он стоит в корне всех классов: любой класс — его наследник, напрямую или через цепочку. Поэтому у любого объекта есть методы Object, например toString.

Здесь картинка с телевизорами неточна. Новую модель инженеры могут собрать из узлов двух прежних, а у класса Java родитель всегда один.

Частая ошибка

Родитель требует аргумент, а наследник не вызвал super(…):

JAVA
class Animal {
    String name;
    Animal(String name) { this.name = name; }
}

class Dog extends Animal {
    Dog(String name) { } // ✗
}
TEXT
error: constructor Animal in class Animal cannot be applied to given types;

Программа не компилируется. Раз super(…) не написан, компилятор вставил super() без аргументов, а у Animal такого конструктора нет — есть только конструктор с String. Сообщение то же, что и при вызове new без нужных аргументов. Вызовите конструктор родителя сами: Dog(String name) { super(name); }.

Главное

  • class Dog extends Animal объявляет наследника: Dog получает поля и методы Animal и добавляет свои.
  • private-поля и методы родителя наследнику не видны, а конструкторы не наследуются.
  • super(…) вызывает конструктор родителя; без него компилятор вставляет super(), и конструктор родителя отрабатывает раньше кода наследника.
  • У класса ровно один родитель, а в корне всех классов стоит Object.