Статьи

Полный курс по языку Java — от установки JDK до многопоточности и HTTP-запросов

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

Страница 16 из 30 · 2026-10-25 · 4 мин чтения

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

Класс-наследник получает поля и методы родителя и может добавлять свои.

public class Animal {    protected String name;    public Animal(String name) {        this.name = name;    }    public void eat() {        System.out.println(name + " ест");    }    public void sound() {        System.out.println(name + " издает звук");    }}
public class Dog extends Animal {    public Dog(String name) {        super(name);    }    @Override    public void sound() {        System.out.println(name + " лает: Гав!");    }    public void fetch() {        System.out.println(name + " приносит палку");    }}
public class Main {    public static void main(String[] args) {        Dog dog = new Dog("Рекс");        dog.eat();        dog.sound();        dog.fetch();    }}

В Java допускается только одиночное наследование классов. Все классы неявно наследуют Object.

super

  • super(...) вызывает конструктор родителя. Должен быть первой строкой конструктора. Если не указан, вызывается конструктор родителя без параметров.
  • super.method() вызывает версию метода родителя.
public class Cat extends Animal {    public Cat(String name) {        super(name);    }    @Override    public void sound() {        super.sound();        System.out.println(name + " мяукает: Мяу!");    }}

Порядок создания объекта

Сначала выполняется конструктор родителя, затем потомка:

public class Parent {    public Parent() {        System.out.println("Parent");    }}
public class Child extends Parent {    public Child() {        System.out.println("Child");    }    public static void main(String[] args) {        new Child();    }}

Вывод: Parent, затем Child.

Переопределение и аннотация @Override

Переопределенный метод должен иметь ту же сигнатуру. Аннотация @Override заставляет компилятор проверить это:

public class Shape {    public double area() {        return 0;    }}
public class Circle extends Shape {    private final double radius;    public Circle(double radius) {        this.radius = radius;    }    @Override    public double area() {        return Math.PI * radius * radius;    }}

Правила переопределения:

  • нельзя сужать доступ (public метод нельзя сделать protected);
  • возвращаемый тип совпадает или является его подтипом;
  • static, private и final методы не переопределяются.

final

  • final class — от класса нельзя наследоваться;
  • final метод — нельзя переопределить;
  • final поле или переменная — нельзя переприсвоить.
public final class Utils {    private Utils() {    }}

Приведение типов

public class Casting {    public static void main(String[] args) {        Animal animal = new Dog("Бобик");        animal.sound();        if (animal instanceof Dog dog) {            dog.fetch();        }        Animal other = new Cat("Мурка");        Dog wrong = (Dog) other;    }}

Последняя строка бросит ClassCastException. Перед явным приведением вниз проверяйте instanceof.

Класс Object

Базовые методы: toString(), equals(), hashCode(), getClass(). Их часто переопределяют.

public class ObjectDemo {    public static void main(String[] args) {        Object obj = "строка";        System.out.println(obj.getClass().getName());        System.out.println(obj.getClass().getSimpleName());    }}

Композиция

Композиция — класс содержит другие объекты и делегирует им работу. Отношение «имеет», в отличие от «является» при наследовании.

public class Engine {    private boolean running;    public void start() {        running = true;        System.out.println("Двигатель запущен");    }    public void stop() {        running = false;        System.out.println("Двигатель остановлен");    }    public boolean isRunning() {        return running;    }}
public class Car {    private final Engine engine = new Engine();    private final String model;    public Car(String model) {        this.model = model;    }    public void drive() {        engine.start();        System.out.println(model + " едет");    }    public void park() {        engine.stop();    }}

Что выбрать

Наследование подходит, если выполняются оба условия:

  • потомок действительно «является» родителем во всех сценариях использования;
  • потомок не нарушает поведение, ожидаемое от родителя.

Классический контрпример: Square extends Rectangle. Если Rectangle позволяет менять ширину и высоту независимо, квадрат нарушит это ожидание.

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

Пример: делегирование

import java.util.ArrayList;import java.util.List;public class CountingList {    private final List<String> inner = new ArrayList<>();    private int additions;    public void add(String item) {        inner.add(item);        additions++;    }    public String get(int index) {        return inner.get(index);    }    public int getAdditions() {        return additions;    }    public static void main(String[] args) {        CountingList list = new CountingList();        list.add("a");        list.add("b");        System.out.println(list.getAdditions());    }}

Если бы CountingList наследовал ArrayList, метод addAll внутри мог вызывать add и счетчик получился бы неверным. Композиция избегает таких ловушек.