Наследование в 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 CatAnimal — это суперкласс (или родитель), 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), но делать это следует редко.
Рабочий пример
Что дальше
super упоминался здесь несколько раз — в цепочке конструкторов и как способ обратиться к переопределённому родительскому методу. Следующая глава о ключевом слове super разбирает каждую ситуацию, в которой оно встречается. Когда родитель должен определять что делают его потомки, но не как, обращаются к абстрактным классам, которые непосредственно строятся на правилах наследования, рассмотренных здесь.