Полиморфизм
Переменная родительского типа может ссылаться на объект любого наследника. Какой именно метод вызвать, 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))); }}