Наследование - это повторное использование по принципу "является"
Наследование позволяет новому классу опираться на уже существующий. Новый класс -подкласс- автоматически получает поля и методы суперкласса, а затем добавляет или изменяет то, что ему нужно. К нему прибегают, когда один тип является более конкретной разновидностью другого: Dog является Animal, SavingsAccount является BankAccount.
Записывается это с помощью ключевого слова extends. Всё, что в родителе помечено как public или protected, доступно в потомке без переписывания.
Dog нигде не объявляет ни name, ни eat(), но обладает и тем, и другим. В этом весь смысл: общее поведение живёт в одном месте.
super: доступ к родителю
Конструктор подкласса должен сначала убедиться, что родитель проинициализирован. Это делается с помощью super(...), которое вызывает конструктор родителя и должно быть первой инструкцией в конструкторе подкласса. Если вы его опустите, Java неявно вставит вызов конструктора родителя без аргументов - а если у родителя такого конструктора нет, код не скомпилируется.
Вывод показывает, что конструктор родителя выполняется раньше тела потомка - построение идёт сверху вниз по иерархии.
Переопределение методов
Подкласс может заменить унаследованный метод, переопределив его с той же сигнатурой. Это называется переопределением, и его всегда стоит помечать аннотацией @Override. Аннотация не обязательна, но она заставляет компилятор проверить, что вы действительно совпали с методом родителя, - она ловит опечатки вроде tostring() вместо toString(), которые иначе молча создали бы совершенно новый метод.
Хотя массив имеет тип Animal, каждый элемент выполняет свой собственный speak(). Java выбирает метод по реальному объекту во время выполнения, а не по объявленному типу переменной - в этом и состоит основа полиморфизма.
Вызов родительской версии через super.method()
Переопределение не обязано отбрасывать работу родителя. Используйте super.method(), чтобы выполнить унаследованную версию, а затем дополнить её:
Без super. вызов log внутри TimestampLogger.log вызвал бы сам себя и ушёл в бесконечную рекурсию. super. явно означает "версия родителя".
Унаследованные поля и доступ
Подкласс видит public- и protected-члены родителя, но не видит private. private-поля по-прежнему существуют в объекте - собственные методы родителя могут с ними работать, - но подкласс не может обращаться к ним напрямую. Используйте protected, когда хотите дать доступ подклассам, оставив член скрытым от несвязанного кода.
class Base {
private int secret; // невидимо для подклассов
protected int shared; // видимо для подклассов
}
class Derived extends Base {
void demo() {
shared = 5; // OK
// secret = 5; // ошибка компиляции - private в Base
}
}
Именно поэтому конструктор подкласса часто обязан вызывать super(...): это единственный способ инициализировать приватное состояние родителя.
Остановка наследования с помощью final
Иногда класс вообще не должен наследоваться - String объявлен final именно по этой причине. Пометка класса как final запрещает создавать подклассы; пометка метода как final запрещает его переопределять, но при этом по-прежнему позволяет расширять класс.
final class Constants { } // нельзя унаследовать
class Config {
final void load() { } // подклассы могут расширять Config,
// но не могут переопределить load()
}
Прибегайте к final, когда поведение класса должно быть гарантированным и неизменным во всей программе - это намеренный сигнал "не наследовать", а не вариант по умолчанию.
Распространённая ошибка: предпочитайте композицию, когда это не "является"
Наследование соблазнительно, потому что повторно использует код, но оно жёстко связывает потомка с родителем. Если отношение не является настоящим "является" - скажем, Car, которому просто нужен Engine, - не пишите Car extends Engine. Автомобиль имеет двигатель, но не является двигателем. Смоделируйте это через поле (композицию):
class Car {
private Engine engine = new Engine(); // Car ИМЕЕТ Engine
void start() { engine.ignite(); }
}
Используйте наследование только тогда, когда подкласс действительно является специализированной формой суперкласса и вы хотите унаследовать и подменить его поведение.
Далее: интерфейсы
Наследование через extends даёт вам единственного родителя и общую реализацию. Но класс может расширять только один класс - так как же дать несвязанным классам общую возможность? Для этого и существуют интерфейсы: контракт, который могут реализовать многие классы, и тема следующей страницы.
Часто задаваемые вопросы
Что такое наследование в Java?
Наследование позволяет одному классу (подклассу) повторно использовать поля и методы другого класса (суперкласса) с помощью ключевого слова extends. Подкласс автоматически получает public- и protected-члены родителя и может добавлять новые или заменять унаследованное поведение через переопределение. Оно моделирует отношение "является": Dog является Animal.
Что делает ключевое слово super в Java?
super ссылается на родительский класс. super(...) в конструкторе вызывает конструктор родителя (и должно быть первой инструкцией), а super.method() вызывает родительскую версию метода, который вы переопределили. Это позволяет подклассу опираться на логику родителя, а не заменять её полностью.
В чём разница между переопределением и перегрузкой в Java?
Переопределение (overriding) заново определяет унаследованный метод в подклассе с той же сигнатурой, изменяя поведение, - помечайте его аннотацией @Override. Перегрузка (overloading) определяет несколько методов с одинаковым именем, но разными списками параметров в одном классе. Переопределение связано с наследованием и диспетчеризацией во время выполнения; перегрузка - это просто два метода, которые случайно имеют одно и то же имя.