Привет! Сегодня мы поговорим об одном из самых важных принципов в Java — наследовании. Это такая штука, которая сначала кажется простой, а потом оказывается, что в ней куча нюансов. Но не переживай, разберёмся вместе! Помнишь, когда ты только начинал программировать, ты просто писал код? Создавал классы, методы, переменные... А потом вдруг понял, что копируешь один и тот же код в разные места? Вот тут-то и приходит на помощь наследование.instanceof и основы наследования - 1

Что такое наследование и зачем оно вообще нужно?

Наследование — это механизм в Java, который позволяет создавать новый класс на основе уже существующего. Класс-наследник получает доступ к полям и методам родительского класса. Звучит абстрактно? Давай на примере. Представь, что тебе нужно создать в программе несколько классов машин: Грузовик, Гоночная машина, Седан, Пикап и так далее. Ты садишься писать код и понимаешь: блин, у всех же машин есть куча общего! У всех есть:
  • Название модели
  • Год выпуска
  • Объём двигателя
  • Максимальная скорость
  • Четыре колеса (ну или больше, но суть ты понял)
  • Возможность ехать вперёд и тормозить
Что ты можешь сделать? Вариант 1: Создавать эти поля в каждом классе заново. Грузовик — пишешь поля. Седан — пишешь те же поля. Пикап — опять те же поля. Скучно, долго, и главное — если потом захочешь что-то изменить, придётся лезть в каждый класс. Вариант 2: Вынести общие поля и методы в родительский класс Car, а все остальные классы унаследовать от него с помощью ключевого слова extends. Второй вариант, конечно, гораздо лучше. Давай посмотрим, как это работает:

public class Car {
    private String model;
    private int maxSpeed;
    private int yearOfManufacture;

    public Car(String model, int maxSpeed, int yearOfManufacture) {
        this.model = model;
        this.maxSpeed = maxSpeed;
        this.yearOfManufacture = yearOfManufacture;
    }

    public void gas() {
        System.out.println("Машина ускоряется!");
    }

    public void brake() {
        System.out.println("Машина тормозит!");
    }
}
Вот наш базовый класс Car. А теперь создадим класс Truck (грузовик), который наследуется от Car:

public class Truck extends Car {
    public Truck(String model, int maxSpeed, int yearOfManufacture) {
        super(model, maxSpeed, yearOfManufacture);
    }
}
Видишь ключевое слово extends? Оно говорит: "Эй, Java, класс Truck — это расширенная версия класса Car". И теперь у грузовика есть всё, что есть у машины! А вот и седан:

public class Sedan extends Car {
    public Sedan(String model, int maxSpeed, int yearOfManufacture) {
        super(model, maxSpeed, yearOfManufacture);
    }
}

Стоп, а что за super?

Отличный вопрос! Слово super — это способ обратиться к родительскому классу. Когда ты пишешь super(model, maxSpeed, yearOfManufacture), ты вызываешь конструктор родительского класса Car. Это очень важно понимать: когда ты создаёшь объект класса-наследника, сначала Java создаёт "родительскую часть" объекта, а потом добавляет к ней то, что специфично для наследника. Давай посмотрим на более детальный пример:

public class Animal {
    String brain = "Мозг животного";
    String heart = "Сердце животного";

    public Animal(String brain, String heart) {
        System.out.println("Создаётся животное!");
        System.out.println("Мозг: " + brain);
        System.out.println("Сердце: " + heart);
        this.brain = brain;
        this.heart = heart;
    }
}

public class Cat extends Animal {
    String tail = "Хвост кота";

    public Cat(String brain, String heart, String tail) {
        super(brain, heart);  // Сначала вызываем родительский конструктор
        System.out.println("Теперь добавляем кошачьи части!");
        this.tail = tail;
    }

    public static void main(String[] args) {
        Cat cat = new Cat("Кошачий мозг", "Кошачье сердце", "Пушистый хвост");
    }
}
Запусти этот код в своей IDE, и увидишь:

Создаётся животное!
Мозг: Кошачий мозг
Сердце: Кошачье сердце
Теперь добавляем кошачьи части!
Видишь? Сначала отработал конструктор Animal, и только потом конструктор Cat. Это железное правило Java: конструктор родительского класса всегда вызывается первым.

Добавляем специфичное поведение

Окей, мы разобрались с общими полями. Но ведь у разных машин есть и уникальное поведение, верно? Гоночные машины могут делать пит-стопы, грузовики — перевозить тяжёлый груз, а седаны — комфортно везти пассажиров. Давай добавим специфичные методы:

public class Car {
    private String model;
    private int maxSpeed;

    public Car(String model, int maxSpeed) {
        this.model = model;
        this.maxSpeed = maxSpeed;
    }

    public void gas() {
        System.out.println(model + " ускоряется!");
    }

    public void brake() {
        System.out.println(model + " тормозит!");
    }
}

public class F1Car extends Car {
    public F1Car(String model, int maxSpeed) {
        super(model, maxSpeed);
    }

    // Специфичный метод только для гоночных машин
    public void pitStop() {
        System.out.println("Пит-стоп! Меняем шины за 2 секунды!");
    }

    public static void main(String[] args) {
        F1Car formula = new F1Car("Ferrari SF-23", 350);
        formula.gas();       // Метод из родительского класса
        formula.pitStop();   // Свой метод
        formula.brake();     // Снова метод из родительского класса
    }
}
Вывод:

Ferrari SF-23 ускоряется!
Пит-стоп! Меняем шины за 2 секунды!
Ferrari SF-23 тормозит!
Круто, правда? У нас есть и общие методы (gas() и brake()), и специфичные для гоночных машин (pitStop()).

Переопределение методов: когда наследник делает по-своему

А что если нам не нравится, как работает родительский метод? Например, гоночная машина ускоряется не так, как обычная машина — она рычит мотором на всю катушку! Для этого существует переопределение методов.

public class Car {
    protected String model;

    public Car(String model) {
        this.model = model;
    }

    public void gas() {
        System.out.println(model + " спокойно ускоряется");
    }
}

public class F1Car extends Car {
    public F1Car(String model) {
        super(model);
    }

    @Override
    public void gas() {
        System.out.println(model + " ВЗРЫВАЕТСЯ вперёд с диким рёвом мотора!");
    }

    public static void main(String[] args) {
        Car regularCar = new Car("Toyota Camry");
        F1Car raceCar = new F1Car("Ferrari SF-23");

        regularCar.gas();
        raceCar.gas();
    }
}
Вывод:

Toyota Camry спокойно ускоряется
Ferrari SF-23 ВЗРЫВАЕТСЯ вперёд с диким рёвом мотора!
Аннотация @Override говорит компилятору: "Я переопределяю метод родителя". Это не обязательно, но очень полезно — если ты случайно ошибёшься в названии метода, компилятор тебя поправит. Кстати, обрати внимание: я изменил модификатор доступа поля model с private на protected. Это важный момент!

Модификаторы доступа при наследовании

Когда речь идёт о наследовании, модификаторы доступа работают так:
  • private — поле или метод доступен только внутри класса. Наследники его не видят.
  • protected — доступен в классе и всех его наследниках.
  • public — доступен везде.
  • package-private (без модификатора) — доступен в пределах пакета.
Если ты хочешь, чтобы наследники могли работать с полем, используй protected. Если нет — private.

Практический пример: система сотрудников

Давай посмотрим на более жизненный пример. Представь, что ты пишешь систему для компании, и тебе нужно работать с сотрудниками.

public class Employee {
    protected String name;
    protected int id;
    protected double baseSalary;

    public Employee(String name, int id, double baseSalary) {
        this.name = name;
        this.id = id;
        this.baseSalary = baseSalary;
    }

    public double calculateSalary() {
        return baseSalary;
    }

    public void displayInfo() {
        System.out.println("Сотрудник: " + name);
        System.out.println("ID: " + id);
        System.out.println("Зарплата: $" + calculateSalary());
    }
}

public class Manager extends Employee {
    private double bonus;

    public Manager(String name, int id, double baseSalary, double bonus) {
        super(name, id, baseSalary);
        this.bonus = bonus;
    }

    @Override
    public double calculateSalary() {
        return baseSalary + bonus;
    }
}

public class Developer extends Employee {
    private int completedProjects;
    private double projectBonus;

    public Developer(String name, int id, double baseSalary, int completedProjects) {
        super(name, id, baseSalary);
        this.completedProjects = completedProjects;
        this.projectBonus = 500.0;
    }

    @Override
    public double calculateSalary() {
        return baseSalary + (completedProjects * projectBonus);
    }

    @Override
    public void displayInfo() {
        super.displayInfo();  // Вызываем родительский метод
        System.out.println("Завершённых проектов: " + completedProjects);
    }
}
Теперь проверим, как это работает:

public class Company {
    public static void main(String[] args) {
        Employee regularEmployee = new Employee("Иван Петров", 101, 3000);
        Manager manager = new Manager("Мария Сидорова", 201, 5000, 2000);
        Developer developer = new Developer("Алексей Кодов", 301, 4000, 3);

        System.out.println("=== Обычный сотрудник ===");
        regularEmployee.displayInfo();

        System.out.println("\n=== Менеджер ===");
        manager.displayInfo();

        System.out.println("\n=== Разработчик ===");
        developer.displayInfo();
    }
}
Вывод:

=== Обычный сотрудник ===
Сотрудник: Иван Петров
ID: 101
Зарплата: $3000.0

=== Менеджер ===
Сотрудник: Мария Сидорова
ID: 201
Зарплата: $7000.0

=== Разработчик ===
Сотрудник: Алексей Кодов
ID: 301
Зарплата: $5500.0
Завершённых проектов: 3
Видишь, как круто? У каждого типа сотрудника своя логика расчёта зарплаты, но базовая структура одинаковая.

Цепочки наследования: наследник наследника

В Java можно создавать целые цепочки наследования. Например:

public class Vehicle {
    protected String name;

    public Vehicle(String name) {
        this.name = name;
        System.out.println("Создано транспортное средство: " + name);
    }
}

public class Car extends Vehicle {
    public Car(String name) {
        super(name);
        System.out.println("Это автомобиль!");
    }
}

public class ElectricCar extends Car {
    private int batteryCapacity;

    public ElectricCar(String name, int batteryCapacity) {
        super(name);
        this.batteryCapacity = batteryCapacity;
        System.out.println("Это электромобиль с батареей " + batteryCapacity + " кВт⋅ч");
    }

    public static void main(String[] args) {
        ElectricCar tesla = new ElectricCar("Tesla Model 3", 75);
    }
}
Вывод:

Создано транспортное средство: Tesla Model 3
Это автомобиль!
Это электромобиль с батареей 75 кВт⋅ч
Конструкторы вызываются по цепочке: сначала Vehicle, потом Car, потом ElectricCar. Это всегда работает так — от самого верхнего предка к самому нижнему потомку.

Object — прародитель всех классов

Кстати, есть один интересный факт: в Java вообще все классы наследуются от класса Object. Даже если ты не пишешь extends Object, Java делает это автоматически.

public class MyClass {
    // Неявно здесь: extends Object
}
Это значит, что у любого твоего класса есть методы из Object: toString(), equals(), hashCode() и другие. Именно поэтому ты можешь вызвать toString() у любого объекта — этот метод унаследован от Object!

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

Окей, наследование — это круто. Но есть важный момент, о котором нужно знать: не всегда наследование — правильный выбор. Иногда лучше использовать композицию. Наследование работает по принципу "является" (IS-A):
  • Грузовик ЯВЛЯЕТСЯ машиной
  • Кот ЯВЛЯЕТСЯ животным
  • Менеджер ЯВЛЯЕТСЯ сотрудником
Композиция работает по принципу "имеет" (HAS-A):
  • Машина ИМЕЕТ двигатель
  • Компьютер ИМЕЕТ процессор
  • Студент ИМЕЕТ адрес
Вот пример композиции:

public class Engine {
    private int horsepower;

    public Engine(int horsepower) {
        this.horsepower = horsepower;
    }

    public void start() {
        System.out.println("Двигатель запущен! Мощность: " + horsepower + " л.с.");
    }
}

public class Car {
    private String model;
    private Engine engine;  // Композиция: машина ИМЕЕТ двигатель

    public Car(String model, Engine engine) {
        this.model = model;
        this.engine = engine;
    }

    public void startCar() {
        System.out.println("Заводим " + model);
        engine.start();
    }

    public static void main(String[] args) {
        Engine powerfulEngine = new Engine(450);
        Car sportsCar = new Car("Porsche 911", powerfulEngine);
        sportsCar.startCar();
    }
}
Когда использовать наследование, а когда композицию? Общее правило такое: если между классами есть отношение "является", используй наследование. Если "имеет" — композицию.

Частые ошибки новичков

Давай разберём типичные косяки, которые делают начинающие:

Ошибка 1: Забыть вызвать super()


public class Animal {
    private String name;

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

public class Cat extends Animal {
    public Cat(String name) {
        // Ошибка! Забыли вызвать super(name)
        // Компилятор выдаст ошибку
    }
}
Если у родительского класса есть конструктор с параметрами, ты обязан вызвать super() с нужными аргументами в конструкторе наследника.

Ошибка 2: Пытаться обратиться к private полям родителя


public class Parent {
    private int secretNumber = 42;
}

public class Child extends Parent {
    public void revealSecret() {
        System.out.println(secretNumber);  // Ошибка! private поле не видно
    }
}
Если нужно, чтобы наследники видели поле, используй protected вместо private.

Ошибка 3: Бесконтрольное использование наследования

Не надо создавать наследование там, где его не должно быть. Классический антипример:

// ПЛОХОЙ пример!
public class Stack extends ArrayList {
    // ...
}
Стек — это не "разновидность" ArrayList! Это структура данных, которая ИСПОЛЬЗУЕТ список внутри. Правильно было бы использовать композицию, а не наследование.

Итак, что мы узнали?

Наследование — это мощный инструмент, который:
  • Помогает избежать дублирования кода
  • Создаёт логичную иерархию классов
  • Позволяет переиспользовать и расширять существующую функциональность
Основные моменты:
  • Используй extends для создания наследника
  • Вызывай super() в конструкторе наследника
  • Используй @Override для переопределения методов
  • Выбирай protected для полей, которые должны быть доступны наследникам
  • Помни: наследование для отношения "является", композиция для "имеет"

Что ещё почитать

Теперь, когда ты разобрался с наследованием, самое время изучить Оператор Instanceof — он помогает проверять типы объектов в иерархии наследования.