Полиморфизм в Java
Пишите гибкий Java-код с полиморфизмом времени компиляции (перегрузка) и полиморфизмом времени выполнения (переопределение).
Полиморфизм — это «один интерфейс, много реализаций». В Java он существует в двух видах:
- Полиморфизм времени компиляции (перегрузка): компилятор выбирает между методами с одинаковым именем на основании типов переданных аргументов.
- Полиморфизм времени выполнения (переопределение): JVM выбирает между реализациями метода на основании фактического объекта, для которого вызван метод.
Именно второй вид обычно имеют в виду, говоря о «полиморфизме» в контексте ООП, и именно он делает наследование по-настоящему полезным. Без него ссылка типа Cat была бы единственным способом вызвать Cat.speak() — нельзя было бы написать код, который перебирает смешанный список животных и просит каждое из них «говорить».
Полиморфизм времени компиляции — перегрузка
Два метода в одном классе могут иметь одинаковое имя, если их списки параметров различаются. Компилятор выбирает нужный метод на основании типов аргументов в точке вызова:
public class Printer {
void print(int n) { System.out.println("int: " + n); }
void print(double d) { System.out.println("double: " + d); }
void print(String s) { System.out.println("string: " + s); }
}
Printer p = new Printer();
p.print(5); // int
p.print(5.0); // double
p.print("hi"); // stringЭто решается исключительно на этапе компиляции. Выбранный метод записывается в байткод; во время выполнения ничего не меняется. Перегрузка методов подробно рассмотрена в разделе перегрузка методов в части 5.
Полиморфизм времени выполнения — переопределение и динамическая диспетчеризация
Это более интересный вид. Когда подкласс переопределяет метод, вызовы через ссылку родительского типа всё равно направляются к версии подкласса:
class Animal {
String speak() { return "(noise)"; }
}
class Cat extends Animal {
@Override String speak() { return "meow"; }
}
class Dog extends Animal {
@Override String speak() { return "woof"; }
}
Animal[] zoo = { new Cat(), new Dog(), new Animal() };
for (Animal a : zoo) {
System.out.println(a.speak());
}
// meow
// woof
// (noise)На каждой итерации a имеет тип Animal, но фактический объект является Cat, Dog или Animal. Вызов a.speak() не определяется во время компиляции — на этапе компиляции компилятор знает лишь, что a является некоторым Animal. Во время выполнения JVM смотрит на фактический объект и вызывает speak класса этого объекта.
Это называется динамической диспетчеризацией (иногда — виртуальной диспетчеризацией). Именно она делает приведённый цикл интересным: он написан обобщённо для Animal и работает для любого подкласса — в том числе для тех, которые не существовали на момент написания цикла.
Почему это важно
Полиморфизм — это та возможность ООП, которая делает код открытым для расширения без изменения. Функция, принимающая Shape и вызывающая area(), работает для каждой фигуры, которая существует сегодня, и для каждой фигуры, которую кто-то добавит завтра. Функции не нужна цепочка if (shape instanceof Circle).
double totalArea(List<Shape> shapes) {
double sum = 0;
for (Shape s : shapes) sum += s.area(); // dispatches to each subclass
return sum;
}Добавьте Triangle extends Shape, и totalArea будет работать со списками треугольников без каких-либо изменений. Это суть принципа открытости/закрытости — открыт для расширения, закрыт для изменения.
Приведение вверх и приведение вниз
Переход от типа подкласса к типу родителя называется приведением вверх (upcasting). Оно неявное и всегда безопасно:
Cat c = new Cat();
Animal a = c; // upcast — implicitОбратная операция — присвоение ссылки родительского типа переменной типа подкласса — называется приведением вниз (downcasting). Оно требует явного приведения типа, и JVM во время выполнения проверяет, действительно ли объект является экземпляром этого подтипа:
Animal a = new Cat();
Cat c = (Cat) a; // downcast — runtime check
Animal a2 = new Dog();
Cat c2 = (Cat) a2; // ClassCastException at runtimeБолее безопасная альтернатива — проверка instanceof, которую в современном Java часто совмещают с сопоставлением с образцом:
if (a instanceof Cat c) {
c.purr();
}Поля не полиморфны
Динамическая диспетчеризация применяется только к методам экземпляра. Поля, static-методы и private-методы связываются на этапе компиляции на основании объявленного типа ссылки:
class A {
String label = "A";
static String klass() { return "A"; }
}
class B extends A {
String label = "B";
static String klass() { return "B"; }
}
A a = new B();
System.out.println(a.label); // "A" — field, not polymorphic
System.out.println(a.klass()); // "A" — static, not polymorphicИменно поэтому поля принято делать приватными и обращаться к ним через методы — методы участвуют в полиморфизме, а поля — нет.
@Override и скрытые ошибки
Всегда помечайте переопределения аннотацией @Override. Она говорит компилятору: «это предназначено для переопределения метода родителя — сообщи об ошибке, если это не так». Без неё маленькая опечатка создаёт новый метод, который выглядит как переопределение, но им не является:
class Animal {
String speak() { return "(noise)"; }
}
class Cat extends Animal {
String Speak() { return "meow"; } // capital S — typo, new method
}
Animal a = new Cat();
System.out.println(a.speak()); // "(noise)" — Cat.Speak was never calledДобавление @Override заставляет компилятор сразу обнаружить такую ошибку.
Полиморфизм с интерфейсами
Наследование — не единственный способ. Интерфейс тоже является родительским типом — разные конкретные классы реализуют его, а код, принимающий тип интерфейса, работает со всеми ними:
interface Greeter {
String greet();
}
class English implements Greeter {
public String greet() { return "Hello"; }
}
class French implements Greeter {
public String greet() { return "Bonjour"; }
}
Greeter g = new French();
System.out.println(g.greet()); // "Bonjour" — dispatched to French.greetТа же идея — пишите код против абстракции, пусть среда выполнения выбирает реализацию. Глава про интерфейсы подробно разбирает механику.
Рабочий пример
Что дальше
Полиморфизм основан на одном механизме: замене методов, унаследованных подклассом. Этот механизм — что разрешено, что нет и аннотация @Override, которая не даёт ошибиться, — является темой следующей главы. Перейдите к разделу переопределение методов.