ООП ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-08-01

Наследование в 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;
    }
}

Получилась цепочка Box6HeavyBoxShipment. Объект 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

Please log in or register to have a possibility to add comment.