Наследование
Класс-наследник получает поля и методы родителя и может добавлять свои.
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 и счетчик получился бы неверным. Композиция избегает таких ловушек.
