W3docs

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

Повторное использование и расширение поведения классов в Java с помощью ключевого слова extends и правил одиночного наследования.

Наследование позволяет классу строиться на основе другого, а не создаваться с нуля. Новый класс — подкласс — получает все поля и методы родителя, добавляя (или заменяя) только то, что отличается. Именно так Java выражает отношения «является»: Cat является Animal, AdminUser является User.

Ключевое слово — extends. Механика проста. Сложная часть — и именно о ней эта глава — распознать, когда наследование является правильным инструментом, а когда нет.

Первый пример

public class Animal {
  String name;
  void breathe() { System.out.println(name + " breathes"); }
}

public class Cat extends Animal {
  void purr() { System.out.println(name + " purrs"); }
}

Cat объявляет только purr(). При этом у неё всё равно есть name и breathe(), поскольку они унаследованы:

Cat c = new Cat();
c.name = "Mittens";
c.breathe();     // Mittens breathes — inherited from Animal
c.purr();        // Mittens purrs   — declared on Cat

Animal — это суперкласс (или родитель), Catподкласс (или потомок). В других языках их называют базовым/производным классом.

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

Подкласс наследует:

  • Все public и protected поля и методы от каждого предка.
  • Поля и методы с доступом уровня пакета, если подкласс находится в том же пакете.
  • Все унаследованные члены сохраняют свои исходные модификаторы.

Подкласс не наследует:

  • Конструкторы. Они не являются членами класса в том же смысле — подкласс должен иметь собственные.
  • Поля и методы с модификатором private. Они существуют в родителе (размещение Cat в памяти всё равно включает приватные поля Animal), но подкласс не может обращаться к ним по имени. Единственный способ их прочитать — через унаследованные методы доступа (public/protected).

Одиночное наследование — только один родитель

Каждый класс расширяет ровно один другой класс. В Java нет множественного наследования для классов:

public class Hybrid extends Animal, Vehicle { }    // ERROR — no multiple inheritance

Вы можете реализовывать множество интерфейсов (рассматривается в главе об интерфейсах) — это даёт гибкость множественных источников, которую большинство языков обеспечивают через множественное наследование, без головной боли с неоднозначностью.

Если вы вообще не пишете extends, класс неявно расширяет Object:

public class Foo { }
// is equivalent to
public class Foo extends Object { }

Вот почему у каждого объекта Java есть методы toString(), equals(), hashCode() и getClass() — все они находятся в Object. Глава о классе Object разбирает их подробно.

Классы, которые нельзя расширять

Класс, помеченный final, не может быть родителем — попытка расширить его приведёт к ошибке компиляции:

public final class Money { }

public class Coupon extends Money { }   // ERROR — cannot inherit from final Money

Это намеренно, а не упущение: многие базовые типы являются final именно для того, чтобы ни один подкласс не мог изменить их поведение. String, Integer и другие классы-обёртки — все final, что частично делает их безопасными для совместного использования и кэширования. Когда вам нужен тип, который нельзя наследовать — как правило, для гарантий безопасности или неизменяемости — пометьте его final.

Конструкторы и super

Каждый конструктор подкласса в качестве первого действия должен вызвать конструктор родителя. Если вы не пишете этот вызов, Java вставляет super() за вас:

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

public class Cat extends Animal {
  public Cat(String name) {
    super(name);             // call Animal(String)
  }
}

Если бы у Animal не было конструктора без аргументов, а Cat не написал бы super(...), компилятор выдал бы ошибку — нет Animal() для неявной вставки. Глава о ключевом слове super полностью охватывает цепочку конструкторов через super(...) и вызовы super.method().

Переопределение

Подкласс может заменить унаследованный метод, объявив метод с той же сигнатурой:

public class Animal {
  String speak() { return "(some noise)"; }
}

public class Cat extends Animal {
  @Override
  String speak() { return "meow"; }
}

Cat c = new Cat();
System.out.println(c.speak());   // meow

@Override — это аннотация, которая говорит компилятору: «Я намереваюсь переопределить этот метод — пожалуйста, выдай ошибку, если это не так». Всегда используйте её. Она обнаруживает опечатки и несоответствия сигнатур, которые иначе молча создали бы новый метод вместо переопределения старого. Глава о переопределении методов охватывает все правила.

Восходящее приведение и полиморфизм

Экземпляр подкласса можно присвоить переменной родительского типа:

Animal a = new Cat();    // upcast — implicit
a.speak();               // calls Cat's speak() — picked at runtime

Это основа полиморфизма, следующей главы. Переменная a имеет тип Animal, но фактический объект — Cat, поэтому выполняется версия speak из Cat.

Когда наследование — неподходящий инструмент

Наследование — наиболее часто применяемый механизм в ООП. Некоторые признаки того, что вместо него следует использовать композицию (поле другого типа):

  • Подкласс по сути не проходит проверку «является ли он». Stack на самом деле не является Vector — однако java.util.Stack расширяет Vector и широко считается ошибкой проектирования.
  • Изменяемые внутренности родителя просачиваются в подкласс. Изменения в реализации родителя ломают подкласс.
  • Вы наследуете, чтобы повторно использовать несколько методов, а не потому что типы действительно взаимозаменяемы.

Правило большого пальца из Effective Java Джошуа Блоха: отдавайте предпочтение композиции над наследованием. Если B нужно поведение A, но B не является настоящим A, дайте B приватное поле типа A и перенаправляйте то, что нужно.

// Inheritance — fragile
public class MyList extends ArrayList<String> { ... }

// Composition — robust
public class MyList {
  private final List<String> inner = new ArrayList<>();
  public void add(String s) { inner.add(s); }
}

Версия с композицией защищена от сюрпризов, когда ArrayList добавляет новые методы или меняет работу своих приватных полей.

Наследование и доступ

Унаследованные члены сохраняют тот модификатор, который был у них в родителе. Подкласс не может сужать видимость переопределённого метода — сделать public метод private в подклассе нарушило бы принцип подстановки Лисков, и компилятор откажет:

public class A {
  public void hello() { }
}
public class B extends A {
  private void hello() { }    // ERROR — cannot reduce visibility
}

Вы можете расширять видимость (переопределить protected метод как public), но делать это следует редко.

Рабочий пример

java— editable, runs on the server

Что дальше

super упоминался здесь несколько раз — в цепочке конструкторов и как способ обратиться к переопределённому родительскому методу. Следующая глава о ключевом слове super разбирает каждую ситуацию, в которой оно встречается. Когда родитель должен определять что делают его потомки, но не как, обращаются к абстрактным классам, которые непосредственно строятся на правилах наследования, рассмотренных здесь.

Практика

Практика
Какое утверждение о наследовании в Java верно?
Какое утверждение о наследовании в Java верно?
Was this page helpful?