Наследование в Java и ключевое слово extends
Удалите из класса Box6 ниже одну-единственную строку — пустой конструктор public Box6() {} — и перестанут компилироваться сразу все его наследники, хотя ни один из них этот конструктор явно не вызывает. Компилятор скажет: constructor Box6 in class Box6 cannot be applied to given types. Причина в том, что первой строкой любого конструктора подкласса Java неявно подставляет вызов конструктора суперкласса без аргументов.
Наследование в Java — это механизм ООП, при котором один класс (подкласс) получает поля и методы другого класса (суперкласса) и может добавлять к ним собственные. Наследование объявляется ключевым словом extends: class Подкласс extends Суперкласс. Оно позволяет переиспользовать код и строить иерархии типов, на которых работает полиморфизм.
Ниже разберём синтаксис extends, что именно наследуется, а что нет, как работает upcasting, в каком порядке вызываются конструкторы и как запретить наследование.
Что такое наследование и ключевое слово extends
Рассмотрим базовый класс, описывающий коробку:
public class Box6 {
double width;
double height;
double depth;
Box6(double w, double h, double d) {
width = w;
height = h;
depth = d;
}
public Box6() {}
double getVolume() {
return width * height * depth;
}
} Представим, что нужны новые типы коробок: ColorBox (с цветом) и HeavyBox (с весом). Без наследования пришлось бы копировать в каждый класс одни и те же три поля и метод getVolume(). Наследование избавляет от дублирования: общие характеристики остаются в базовом классе, а специализированные классы только расширяют его.
Терминология в Java:
- суперкласс (superclass) — базовый, родительский класс;
- подкласс (subclass) — наследник, расширяющий суперкласс.
Отношение между ними называют IS-A («является»): ColorBox является Box6. Если фраза «X является Y» звучит неестественно, наследование, скорее всего, выбрано неверно.
Синтаксис:
class Подкласс extends Суперкласс {
// тело класса
} Пример двух наследников:
public class ColorBox extends Box6 {
String color;
public ColorBox(int width, int height, int depth, String color) {
this.width = width;
this.height = height;
this.depth = depth;
this.color = color;
}
} public class HeavyBox extends Box6 {
int weight;
public HeavyBox(int width, int height, int depth, int weight) {
this.width = width;
this.height = height;
this.depth = depth;
this.weight = weight;
}
} Оба класса получают поля width, height, depth и метод getVolume() из Box6, а объявляют только то, чем отличаются от него. Класс может расширять ровно один суперкласс, но у одного суперкласса может быть сколько угодно наследников.
Важно
Конструкторы ColorBox и HeavyBox присваивают унаследованные поля напрямую, поэтому Java неявно вставляет в них вызов super() — конструктора Box6 без аргументов. Именно ради него в Box6 объявлен пустой public Box6() {}. Идиоматичнее передать размеры родителю явно — super(width, height, depth); ключевое слово super подробно разбирается в следующем уроке.
Доступ к членам класса при наследовании
Наследуются не все члены суперкласса: видимость решают модификаторы доступа. Ключевой для наследования модификатор — protected: он открывает член наследникам даже в другом пакете, оставляя его закрытым для остального кода.
| Модификатор | Тот же класс | Тот же пакет | Подкласс в другом пакете | Любой класс |
|---|---|---|---|---|
public | да | да | да | да |
protected | да | да | да | нет |
| без модификатора (default, package-private) | да | да | нет | нет |
private | да | нет | нет | нет |
private-члены не наследуются вообще. Поле остаётся в памяти объекта подкласса, но обратиться к нему по имени нельзя — только через public- или protected-метод суперкласса:
public class A {
public int value1;
private int value2;
public int getValue2() {
return value2;
}
} public class B extends A {
public int sum() {
// return value1 + value2; // ошибка компиляции: value2 has private access in A
return value1 + getValue2();
}
} Неочевидный момент
Поля width, height, depth в Box6 объявлены без модификатора, поэтому ColorBox и HeavyBox видят их только потому, что лежат в том же пакете. Перенесите наследника в другой пакет — и код перестанет компилироваться. Для полей, к которым должны обращаться наследники, используйте protected, а лучше private плюс методы доступа.
Что наследуется, а что нет
Короткая сводка по членам класса:
| Член суперкласса | Наследуется | Комментарий |
|---|---|---|
| public- и protected-поля и методы | да | Доступны в подклассе по имени, как собственные |
| Члены без модификатора (default) | только в том же пакете | Для наследника из другого пакета их как будто нет |
| private-поля и методы | нет | Существуют в объекте, но доступны лишь через методы суперкласса |
| Конструкторы | нет | Вызываются из подкласса через super(...), явно или неявно |
| static-поля и методы | да | Переопределить нельзя: одноимённый static-метод скрывает родительский |
Отдельно стоит запомнить разницу двух механизмов: обычные методы переопределяются (overriding, аннотация @Override) и вызываются по фактическому типу объекта, а поля и static-методы скрываются (hiding) и разрешаются по типу ссылки на этапе компиляции.
Ссылка суперкласса на объект подкласса: upcasting и downcasting
Переменная типа суперкласса может ссылаться на объект любого подкласса — это upcasting, восходящее приведение типов, на котором держится полиморфизм:
Box6 box = new HeavyBox(15, 10, 20, 5);
System.out.println(box.getVolume()); // 3000.0
// System.out.println(box.weight); // ошибка компиляции: cannot find symbol Приведение вверх выполняется автоматически и всегда безопасно: каждый HeavyBox является Box6. Обратное неверно — не всякая коробка тяжёлая, поэтому такое присваивание не компилируется:
// HeavyBox box = new Box6(); // ошибка компиляции: incompatible types Какие члены доступны, определяет тип переменной, а не объект. Через ссылку Box6 не видно поля weight, даже если в куче лежит HeavyBox. Чтобы добраться до членов подкласса, нужно явное downcasting — нисходящее приведение типов:
Box6 box = new HeavyBox(15, 10, 20, 5);
HeavyBox heavy = (HeavyBox) box;
System.out.println(heavy.weight); // 5
Box6 plain = new Box6();
HeavyBox wrong = (HeavyBox) plain; // компилируется, но в рантайме ClassCastException Чтобы не поймать ClassCastException, проверяйте тип оператором instanceof. Начиная с Java 16 доступен pattern matching — проверка и приведение в одном выражении:
if (box instanceof HeavyBox heavy) {
System.out.println(heavy.weight);
} Важное исключение из правила про тип ссылки: если подкласс переопределил метод суперкласса, вызовется версия из подкласса — JVM выбирает метод по фактическому типу объекта. Именно это и называют динамической диспетчеризацией.
Многоуровневое наследование
Иерархия не ограничена двумя уровнями: наследник сам может быть суперклассом для следующего класса.
public class Shipment extends HeavyBox {
double cost;
public Shipment(int w, int h, int d, int weight, double cost) {
super(w, h, d, weight);
this.cost = cost;
}
} Получилась цепочка Box6 → HeavyBox → Shipment. Объект Shipment содержит поля всех трёх уровней: width, height, depth (от Box6), weight (от HeavyBox) и собственное cost. Здесь конструктор Shipment уже явно передаёт данные родителю через super(w, h, d, weight).
На вершине любой иерархии стоит java.lang.Object: даже если класс не объявляет extends, он неявно наследует Object и вместе с ним методы toString(), equals(), hashCode() и другие.
Порядок вызова конструкторов
Конструкторы выполняются сверху вниз — от корня иерархии к самому дальнему потомку. Причина уже знакома: первой инструкцией конструктора подкласса стоит вызов конструктора суперкласса.
class E {
E() { System.out.println("E"); }
}
class F extends E {
F() { System.out.println("F"); }
}
class G extends F {
G() { System.out.println("G"); }
}
// new G(); Результат:
E
F
G Это логично: подкласс достраивает объект поверх состояния родителя, поэтому родительская часть должна быть проинициализирована первой. Если в суперклассе нет конструктора без параметров, неявный super() подставить не из чего — тогда подкласс обязан вызвать super(...) с аргументами явно, иначе будет ошибка компиляции.
Множественное наследование: почему его нет в Java
Класс в Java может расширять только один суперкласс. Такая запись не скомпилируется:
// class Shipment extends HeavyBox, ColorBox {} // ошибка компиляции Причина — проблема ромба (diamond problem): если два родителя содержат одноимённое поле или метод, компилятор не может решить, какую версию унаследовать. Java упростила модель и запретила множественное наследование классов, оставив множественное наследование типов через интерфейсы:
public class Shipment extends HeavyBox implements Comparable<Shipment>, Serializable {
// extends - один класс, implements - сколько угодно интерфейсов
} С Java 8 интерфейсы умеют содержать default-методы, то есть реализацию. Конфликт всё же возможен, но компилятор его не терпит: если два интерфейса дают одинаковый default-метод, класс обязан переопределить его сам и при необходимости выбрать нужную версию через Имя.super.method().
Как запретить наследование: final и sealed
Иногда расширять класс нельзя по замыслу — например, чтобы никто не сломал инварианты неизменяемого объекта. Есть три уровня контроля:
final class— класс нельзя наследовать вообще. Так объявленыString,Integer,LocalDate.final-метод — класс наследовать можно, но конкретный метод не переопределяется.sealed class(Java 17) — список наследников фиксирован автором класса.
public sealed class Box permits ColorBox, HeavyBox {
}
public final class ColorBox extends Box { }
public non-sealed class HeavyBox extends Box { } Каждый наследник sealed-класса обязан быть объявлен как final, sealed или non-sealed. Такая иерархия закрыта и полностью известна компилятору — это делает исчерпывающими switch по типу и упрощает поддержку.
Наследование или композиция
Наследование — отношение IS-A, композиция — отношение HAS-A: объект не расширяет другой класс, а хранит его как поле и делегирует ему работу. Классический промах в самом JDK — java.util.Stack extends Vector: вместе с реализацией стек унаследовал методы вроде add(int index, E element), которые позволяют вставить элемент в середину и полностью нарушают дисциплину LIFO.
Практическое правило: наследуйте, когда подкласс действительно является частным случаем суперкласса и вы контролируете оба класса; в остальных случаях выбирайте композицию — она не привязывает вас к внутренней реализации родителя. Подробнее об отношениях классов — в уроке Связи между классами: композиция и агрегация.
На чём чаще всего ошибаются
- Ждут доступа к private-полям родителя. Они не наследуются: нужен public- или protected-метод доступа.
- Путают тип ссылки и тип объекта. Поля и перегрузка выбираются по типу переменной на этапе компиляции, переопределённые методы — по фактическому типу объекта в рантайме.
- Забывают про конструктор без параметров в суперклассе. Если его нет, каждый подкласс обязан вызвать
super(...)с аргументами явно. - Приводят тип вниз без проверки. Downcasting компилируется всегда, а падает уже в рантайме с
ClassCastException; спасаетinstanceof. - Наследуются ради переиспользования кода. Если «X является Y» звучит неправдой, нужна композиция, а не
extends.
Подробнее о самом понятии можно почитать в статье «Наследование (программирование)» и в официальном руководстве Oracle.
Часто задаваемые вопросы
Наследуются ли private-поля и конструкторы в Java?
Нет. private-члены и конструкторы не наследуются. private-поле физически присутствует в объекте подкласса, но обратиться к нему по имени нельзя — только через public- или protected-метод суперкласса. Конструктор родителя не становится конструктором наследника, он лишь вызывается из него через super(...).
Почему в Java нельзя наследовать несколько классов сразу?
Из-за проблемы ромба: при двух родителях с одноимённым методом или полем компилятор не смог бы выбрать нужную версию. Поэтому extends принимает только один класс, а множественное наследование типов реализуется через интерфейсы — их можно перечислить в implements сколько угодно.
Чем extends отличается от implements?
extends наследует состояние и реализацию от одного класса, implements обязывает класс выполнить контракт интерфейса. Интерфейсы можно реализовывать во множественном числе, а с Java 8 они могут содержать default-методы с реализацией, но не хранят состояние объекта. Сам интерфейс, кстати, тоже может расширять другие интерфейсы через extends.
Как запретить наследование своего класса?
Пометьте класс модификатором final — так сделано у String и классов-обёрток. Если нужно разрешить наследование строго определённым классам, используйте sealed-классы из Java 17: sealed class Box permits ColorBox, HeavyBox. Запретить переопределение отдельного метода можно модификатором final на самом методе.
Итоги
- Наследование объявляется через
extendsи задаёт отношение IS-A между подклассом и суперклассом. - public- и protected-члены наследуются, private — нет; конструкторы не наследуются никогда.
- Переменная типа суперкласса может ссылаться на объект подкласса (upcasting); набор доступных членов определяет тип ссылки, а версию переопределённого метода — тип объекта.
- Конструкторы выполняются от корня иерархии вниз, потому что подкласс неявно вызывает
super(). - Множественного наследования классов в Java нет — его роль выполняют интерфейсы.
- Когда «X является Y» звучит натянуто, используйте композицию вместо
extends.
Video Explanation
Prefer video format? Watch this lesson with examples and explanations.
Comments