Полиморфизм — это возможность работать с объектами разных типов через один общий интерфейс: код вызывает send(), а что именно произойдет, зависит от типа данных. Под этим словом прячутся три разных механизма, и их важно не смешивать:
Содержание
| Вид | Как выглядит в Java | Кто выбирает реализацию |
|---|---|---|
| Подтиповой (полиморфизм подтипов) | переопределение метода в наследнике или реализации интерфейса | JVM во время выполнения, по реальному объекту |
| Ad hoc | перегрузка: несколько методов с одним именем и разными параметрами | компилятор, по объявленным типам аргументов |
| Параметрический | дженерики: List<T>, <T> T max(...) |
один код для многих типов, проверка при компиляции |
Когда в ООП говорят «полиморфизм» без уточнения, обычно имеют в виду первый вид. Ниже разбираем все три на запускаемых примерах. Все листинги проверены на Eclipse Temurin 25.0.4 (Java 25 LTS), 23.09.2026.
Подтиповой полиморфизм: один вызов, разное поведение
Аналогия: кнопка «Поделиться» в соцсети одна, но текст уходит сообщением, картинка — файлом, видео — ссылкой. Сначала полный пример, потом разбор.
import java.util.List;
abstract class Content {
private final String title;
Content(String title) {
this.title = title;
}
String title() {
return title;
}
// общий контракт: каждый вид контента обязан уметь отправляться
abstract String send(String to);
}
class Text extends Content {
Text(String title) { super(title); }
@Override
String send(String to) {
return "текст '" + title() + "' -> " + to + ": отправлен сообщением";
}
}
class Image extends Content {
Image(String title) { super(title); }
@Override
String send(String to) {
return "картинка '" + title() + "' -> " + to + ": сжата и приложена файлом";
}
}
class Video extends Content {
Video(String title) { super(title); }
@Override
String send(String to) {
return "видео '" + title() + "' -> " + to + ": отправлена ссылка на поток";
}
}
public class Main {
public static void main(String[] args) {
List<Content> feed = List.of(
new Text("Привет"),
new Image("cat.png"),
new Video("demo.mp4"));
for (Content c : feed) {
System.out.println(c.send("anna"));
}
}
}
Сохраните как Main.java, выполните javac Main.java и java Main. В консоли появятся три разные строки из одного и того же цикла:
текст 'Привет' -> anna: отправлен сообщением
картинка 'cat.png' -> anna: сжата и приложена файлом
видео 'demo.mp4' -> anna: отправлена ссылка на поток
Что здесь произошло:
Contentзадает контракт: методsend(String)есть у любого контента. Метод абстрактный, поэтому создать «просто контент» черезnew Content(...)нельзя.- Наследники переопределяют (override) метод: та же сигнатура, своя реализация. Аннотация
@Overrideпросит компилятор проверить, что переопределение действительно есть. - Переменная
cобъявлена какContent, но в ней лежатText,Image,Video. JVM выбирает реализацию по реальному объекту в момент вызова. Это называют динамической диспетчеризацией или поздним связыванием.
Главная польза: цикл не знает о конкретных классах. Добавите Audio — цикл менять не придется, достаточно нового наследника. Это и есть расширяемость, ради которой полиморфизм используют.
Наследование не обязательно
Частое упрощение звучит так: «для полиморфизма нужны абстрактный класс и наследование». Это не так. В Java достаточно интерфейса: interface Sendable { String send(String to); } могут реализовать классы из совершенно разных иерархий, и работать с ними можно через тип Sendable. В языках с утиной типизацией (Python) не нужен даже общий интерфейс — важен только метод с нужным именем.
Ошибка: забыли реализовать метод
Добавим наследника без send:
class Audio extends Content {
Audio(String title) { super(title); }
}
Результат — код не компилируется (javac из JDK 25):
Main.java:45: error: Audio is not abstract and does not override abstract method send(String) in Content
class Audio extends Content {
^
1 error
Исправление: реализовать String send(String to) с @Override или объявить Audio абстрактным, если он сам служит базой для других классов. Абстрактный метод полезен именно этим: забыть реализацию невозможно, компилятор не даст.
Ошибка: сигнатура не совпала
Если в наследнике написать String send(String to, boolean silent), это уже не переопределение, а новый метод с другими параметрами. С @Override javac сразу это ловит (минимальный файл Err2.java, первые строки вывода):
Err2.java:5: error: Text is not abstract and does not override abstract method send(String) in Content
Err2.java:6: error: method does not override or implement a method from a supertype
Без абстрактного метода в базе и без @Override такой промах пройдет молча: вызов через базовый тип выполнит родительскую версию. Поэтому @Override стоит ставить на каждое переопределение.
Что не участвует в полиморфизме
Переопределяются только методы экземпляра. Поля и статические методы с тем же именем в наследнике лишь скрывают родительские, и выбор идет по объявленному типу:
class Base {
String kind = "base";
String name() { return "Base.name"; }
}
class Child extends Base {
String kind = "child"; // поле скрывает, а не переопределяет
@Override
String name() { return "Child.name"; }
}
public class Hide {
public static void main(String[] args) {
Base b = new Child();
System.out.println(b.name()); // выбор по объекту
System.out.println(b.kind); // выбор по типу переменной
}
}
Child.name
base
Статические методы с той же сигнатурой ведут себя так же: наследник их скрывает, вызов привязан к классу. Также не переопределяются private и final методы.
Ad hoc полиморфизм: перегрузка методов
Перегрузка (overloading) — несколько методов с одним именем и разными списками параметров. Какой из них вызвать, решает компилятор по объявленным типам аргументов. Поэтому перегрузку называют статическим полиморфизмом, а переопределение — динамическим.
public class Overload {
static String describe(Object o) { return "Object: " + o; }
static String describe(String s) { return "String длиной " + s.length(); }
static String describe(int n) { return "int, квадрат = " + n * n; }
public static void main(String[] args) {
System.out.println(describe("hello"));
System.out.println(describe(7));
System.out.println(describe(3.5));
Object o = "hello"; // объект String, но тип переменной Object
System.out.println(describe(o));
System.out.println(o.toString().length()); // а вот toString() - переопределение
}
}
String длиной 5
int, квадрат = 49
Object: 3.5
Object: hello
5
Четвертая строка — типичная ловушка. В o лежит строка, но перегрузка выбрана по типу переменной Object. Для 3.5 подходящей версии нет, и double упаковывается в Double, а тот подходит под Object. Вызов o.toString() при этом работает по реальному объекту: это переопределение, а не перегрузка.
Исправление, если нужен выбор по реальному типу во время выполнения: один метод и switch с шаблонами типов (финальная возможность языка с Java 21).
static String describe(Object o) {
return switch (o) {
case String s -> "String длиной " + s.length();
case Integer n -> "int, квадрат = " + n * n;
default -> "Object: " + o;
};
}
Для Object o = "hello" теперь печатается String длиной 5. Если же типы ваши собственные, чаще правильнее не разбирать их через switch, а вынести поведение в переопределяемый метод, как в примере с Content.
Параметрический полиморфизм: дженерики
Параметрический полиморфизм — один и тот же код для многих типов, где тип передается параметром. В Java это дженерики. Метод ниже находит максимум в списке чисел, строк и дат:
import java.time.LocalDate;
import java.util.List;
public class Generic {
// один код для любого T, у которого есть порядок сравнения
static <T extends Comparable<? super T>> T max(List<T> items) {
if (items.isEmpty()) {
throw new IllegalArgumentException("пустой список");
}
T best = items.get(0);
for (T item : items) {
if (item.compareTo(best) > 0) {
best = item;
}
}
return best;
}
public static void main(String[] args) {
System.out.println(max(List.of(3, 11, 7)));
System.out.println(max(List.of("java", "kotlin", "c")));
System.out.println(max(List.of(LocalDate.of(2025, 9, 16), LocalDate.of(2026, 3, 17))));
}
}
11
kotlin
2026-03-17
Строки сравниваются лексикографически, поэтому «kotlin» больше «java». Ограничение T extends Comparable<? super T> — это граница: код не для абсолютно любого типа, а для типов с порядком. Передадим список Object:
GenErr.java:7: error: method max in class GenErr cannot be applied to given types;
required: List<T>
found: List<Object>
Ошибка ловится при компиляции, а не падением программы. Оговорка: дженерики в Java реализованы через стирание типов, поэтому во время выполнения List<String> и List<Integer> — один и тот же класс ArrayList, а примитивы (int) в параметр типа не подставить, только обертки (Integer).
Полиморфизм среди принципов ООП
В примере с Content видны все четыре принципа: инкапсуляция прячет поле title за методом, абстракция выделяет контракт send, наследование задает отношение «Video является Content», а полиморфизм позволяет вызывать контракт, не зная конкретного класса.
Связь не жесткая: полиморфизм подтипов работает и через интерфейсы без наследования реализации. Глубокое наследование ради полиморфизма — частая причина хрупкого кода (изменение базы ломает наследников), поэтому в Java-проектах часто предпочитают интерфейсы и композицию.
Как выбрать механизм
| Задача | Что взять | Когда иначе |
|---|---|---|
| Много видов объектов с общим действием, список видов будет расти | интерфейс или абстрактный класс + переопределение | если виды фиксированы и их мало, подойдет sealed interface и switch |
| Одно действие для разных входных типов, удобство вызова | перегрузка | если тип известен только во время выполнения, перегрузка не поможет |
| Одинаковый алгоритм для многих типов (коллекции, поиск, сортировка) | дженерики с нужной границей extends |
если логика отличается по типам, это уже переопределение |
Выводы
- Полиморфизм в ООП — работа с разными типами через общий интерфейс; в Java он бывает подтиповым, ad hoc и параметрическим.
- Переопределение выбирается JVM по реальному объекту во время выполнения, перегрузка — компилятором по объявленным типам.
@Overrideи абстрактные методы переводят ошибки полиморфизма в ошибки компиляции.- Поля, статические,
privateиfinalметоды не переопределяются. - Наследование не обязательно: интерфейсы дают полиморфизм без общей иерархии реализации.
Где применяется / связь с практикой
Освойте тему на практике
Полиморфизм встречается в любом Java-проекте: коллекции работают через List и Map, веб-фреймворки вызывают ваши реализации интерфейсов, в тестах зависимости подменяют тестовыми реализациями. Чтобы уверенно проектировать классы и интерфейсы и разбирать такой код, пригодится базовый курс Java Developer. Basic. Попробовать формат и задать вопросы преподавателям можно на бесплатных открытых уроках Otus.
Смежные темы: Java для новичков: class и object, отношения между классами в Java.
FAQ
Можно ли переопределить конструктор?
Нет. Конструкторы не наследуются, их можно только перегружать и вызывать родительский через super(...), как в классах Text и Image.
Замедляет ли динамическая диспетчеризация программу?
Накладные расходы есть, но JIT-компилятор HotSpot часто убирает их, когда видит, что в месте вызова встречается один или два типа. Для обычного прикладного кода это редко узкое место, решать стоит по профилировщику.
Есть ли полиморфизм в языке C?
Встроенного нет: в C нет классов и перегрузки. Похожее поведение собирают вручную через структуры с указателями на функции, как устроены таблицы виртуальных методов в C++.



