Статьи

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

Полиморфизм, абстрактные классы и интерфейсы

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

Полиморфизм

Переменная родительского типа может ссылаться на объект любого наследника. Какой именно метод вызвать, JVM определяет во время выполнения.

public class Shape {    public double area() {        return 0;    }    public String name() {        return "Фигура";    }}
public class Circle extends Shape {    private final double r;    public Circle(double r) {        this.r = r;    }    @Override    public double area() {        return Math.PI * r * r;    }    @Override    public String name() {        return "Круг";    }}
public class Rect extends Shape {    private final double w;    private final double h;    public Rect(double w, double h) {        this.w = w;        this.h = h;    }    @Override    public double area() {        return w * h;    }    @Override    public String name() {        return "Прямоугольник";    }}
public class Main {    public static void main(String[] args) {        Shape[] shapes = {new Circle(2), new Rect(3, 4), new Circle(1)};        double total = 0;        for (Shape s : shapes) {            System.out.printf("%s: %.2f%n", s.name(), s.area());            total += s.area();        }        System.out.printf("Общая площадь: %.2f%n", total);    }}

Код цикла не знает конкретных типов. Добавить новую фигуру можно без изменения этого кода.

Полиморфны только методы экземпляра. Поля и статические методы определяются по типу переменной.

Абстрактные классы

Абстрактный класс нельзя инстанцировать. Он может содержать методы без реализации.

public abstract class Employee {    private final String name;    protected Employee(String name) {        this.name = name;    }    public String getName() {        return name;    }    public abstract double salary();    public void printPayslip() {        System.out.printf("%s получает %.2f%n", name, salary());    }}
public class Manager extends Employee {    private final double base;    private final double bonus;    public Manager(String name, double base, double bonus) {        super(name);        this.base = base;        this.bonus = bonus;    }    @Override    public double salary() {        return base + bonus;    }}
public class Developer extends Employee {    private final double hourly;    private final int hours;    public Developer(String name, double hourly, int hours) {        super(name);        this.hourly = hourly;        this.hours = hours;    }    @Override    public double salary() {        return hourly * hours;    }}
public class Main {    public static void main(String[] args) {        Employee[] team = {            new Manager("Анна", 3000, 500),            new Developer("Борис", 25, 160)        };        for (Employee e : team) {            e.printPayslip();        }    }}

Абстрактный класс удобен, когда есть общее состояние и общая часть реализации.

Интерфейсы

Интерфейс задает контракт: набор методов, которые обязан реализовать класс.

public interface Drawable {    void draw();}
public interface Resizable {    void resize(double factor);}
public class Icon implements Drawable, Resizable {    private double size = 16;    @Override    public void draw() {        System.out.println("Рисуем иконку размером " + size);    }    @Override    public void resize(double factor) {        size *= factor;    }}

Класс может реализовать сколько угодно интерфейсов. Методы интерфейса по умолчанию public abstract, поля — public static final.

default и static методы

Начиная с Java 8 интерфейс может содержать реализацию:

public interface Greeter {    String name();    default String greet() {        return "Привет, " + name() + "!";    }    static Greeter of(String name) {        return () -> name;    }}
public class Main {    public static void main(String[] args) {        Greeter g = Greeter.of("мир");        System.out.println(g.greet());    }}

Методы default позволяют расширять интерфейс, не ломая существующие реализации.

Если класс реализует два интерфейса с одинаковым default-методом, он обязан переопределить его сам:

public interface A {    default void hello() {        System.out.println("A");    }}
public interface B {    default void hello() {        System.out.println("B");    }}
public class C implements A, B {    @Override    public void hello() {        A.super.hello();        B.super.hello();    }}

Функциональные интерфейсы

Интерфейс с одним абстрактным методом можно реализовывать лямбдой:

@FunctionalInterfacepublic interface Operation {    int apply(int a, int b);}
public class Main {    public static void main(String[] args) {        Operation add = (a, b) -> a + b;        Operation mul = (a, b) -> a * b;        System.out.println(add.apply(3, 4));        System.out.println(mul.apply(3, 4));    }}

Подробнее о лямбдах — в главе про Stream API.

Интерфейс против абстрактного класса

ИнтерфейсАбстрактный класс
Множественная реализацияданет, только один родитель
Состояние (поля экземпляра)нетда
Конструкторнетда
Назначениечто объект умеетчто объект собой представляет

Если нужен только контракт, берите интерфейс. Если нужна общая реализация и поля, берите абстрактный класс. Можно сочетать: интерфейс задает контракт, абстрактный класс дает базовую реализацию.

Программирование на уровне интерфейсов

import java.util.ArrayList;import java.util.List;public class Main {    interface Notifier {        void send(String message);    }    static class EmailNotifier implements Notifier {        @Override        public void send(String message) {            System.out.println("Email: " + message);        }    }    static class SmsNotifier implements Notifier {        @Override        public void send(String message) {            System.out.println("SMS: " + message);        }    }    static void notifyAll(List<Notifier> notifiers, String message) {        for (Notifier n : notifiers) {            n.send(message);        }    }    public static void main(String[] args) {        List<Notifier> notifiers = new ArrayList<>();        notifiers.add(new EmailNotifier());        notifiers.add(new SmsNotifier());        notifyAll(notifiers, "Заказ отправлен");    }}

Метод notifyAll зависит от интерфейса, а не от конкретных классов. Это упрощает расширение и тестирование.

Sealed-типы

Запечатанные типы ограничивают круг наследников (Java 17+):

public sealed interface Payment permits Card, Cash {}
public record Card(String number) implements Payment {}
public record Cash(double amount) implements Payment {}

Компилятор знает все варианты, поэтому switch по такому типу не требует default:

public class Main {    static String describe(Payment payment) {        return switch (payment) {            case Card c -> "Карта " + c.number();            case Cash c -> "Наличные " + c.amount();        };    }    public static void main(String[] args) {        System.out.println(describe(new Card("1234")));        System.out.println(describe(new Cash(50)));    }}