W3docs

Полиморфизм в 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

Та же идея — пишите код против абстракции, пусть среда выполнения выбирает реализацию. Глава про интерфейсы подробно разбирает механику.

Рабочий пример

java— editable, runs on the server

Что дальше

Полиморфизм основан на одном механизме: замене методов, унаследованных подклассом. Этот механизм — что разрешено, что нет и аннотация @Override, которая не даёт ошибиться, — является темой следующей главы. Перейдите к разделу переопределение методов.

Практика

Практика
Animal a = new Cat(); a.speak(); — какой speak() выполняется на самом деле?
Animal a = new Cat(); a.speak(); — какой speak() выполняется на самом деле?
Was this page helpful?