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

Определение

Класс может расширить один другой класс через extends. Наследник получает доступное поведение базового типа и может добавлять или переопределять операции.

class Message {
    private final String body;
 
    Message(String body) {
        this.body = body;
    }
 
    String body() {
        return body;
    }
}
 
final class EmailMessage extends Message {
    private final String subject;
 
    EmailMessage(String subject, String body) {
        super(body);
        this.subject = subject;
    }
}

Java не поддерживает множественное наследование классов, но класс может реализовать несколько интерфейсов.

Что наследуется

Наследник получает члены базового класса с учётом доступности:

  • public и protected доступны по своим правилам;
  • package-private доступен только в том же пакете;
  • private-состояние остаётся частью объекта, но напрямую недоступно наследнику;
  • конструкторы не наследуются;
  • final-метод нельзя переопределить, final-класс нельзя расширить.

Каждый конструктор наследника начинает цепочку конструктора родителя через super(...). Объект наследника содержит состояние своей базовой части, а не отдельный «вложенный объект-родитель».

Подтип и контракт

Наследование полезно, когда объект наследника можно везде подставить вместо родителя, не меняя ожидаемого поведения:

void publish(Message message) {
    audit(message.body());
}
 
publish(new EmailMessage("Status", "Ready"));

Наследник не должен усиливать предусловия, ослаблять обещанные постусловия или нарушать инварианты родителя. Одного сходства полей недостаточно для отношения «является».

Динамический вызов

Переопределённый instance-метод выбирается по реальному классу объекта:

class Formatter {
    String format() {
        return "base";
    }
}
 
class JsonFormatter extends Formatter {
    @Override
    String format() {
        return "json";
    }
}
 
Formatter formatter = new JsonFormatter();
formatter.format(); // "json"

Поля и static-методы полиморфно не переопределяются: они разрешаются по статическому типу и могут лишь скрываться. Это одна из причин не строить API на скрытии.

protected

protected даёт доступ:

  • всем классам того же пакета;
  • наследникам в других пакетах, но с ограничениями на обращение через экземпляр.

Это шире, чем «только наследникам». Часто private плюс защищённая операция сохраняют инварианты лучше, чем открытие поля через protected.

Наследование или композиция

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

final class ReportService {
    private final Formatter formatter;
 
    ReportService(Formatter formatter) {
        this.formatter = formatter;
    }
}

Композицию проще заменять и тестировать. Наследование оправдано стабильным общим контрактом, а не только желанием избежать дублирования.

Ловушки

  • Говорить, что private-поля «не наследуются»: они присутствуют в объекте, но недоступны напрямую.
  • Ожидать наследование конструкторов.
  • Путать переопределение instance-метода со скрытием static-метода или поля.
  • Вызывать переопределяемый метод из конструктора.
  • Строить глубокую иерархию ради повторного использования.
  • Нарушать контракт базового типа в наследнике.

Вопросы на собеседовании

  1. Поддерживает ли Java множественное наследование?
  2. Что происходит с private-полями родителя?
  3. Наследуются ли конструкторы?
  4. По какому типу выбирается переопределённый instance-метод?
  5. Чем наследование отличается от композиции?
  6. Какие требования накладывает отношение подтипа на контракт?

Связанные заметки

Основы ООП · Абстрактные классы · Интерфейсы Java · Переопределение Java · Конструкторы Java · Полиморфизм · Модификаторы доступа