Полиморфизм – способность объекта принимать множество различных форм. Наиболее распространенное использование полиморфизма в ООП происходит, когда ссылка на родительский класс используется для ссылки на объект дочернего класса. Постараемся разобраться с понятием полиморфизма в Java простыми словами, так сказать для чайников.

Любой объект в Java, который может пройти более одного теста IS-A считается полиморфным. В Java все объекты полиморфны, так как любой объект пройдёт тест IS-A для своего собственного типа и для класса Object.

Важно знать, что получить доступ к объекту можно только через ссылочную переменную. Ссылочная переменная может быть только одного типа. Будучи объявленной, тип ссылочной переменной изменить нельзя.

Ссылочную переменную можно переназначить к другим объектам, которые не объявлены как final. Тип ссылочной переменной определяет методы, которые она может вызвать на объекте.

Ссылочная переменная может обратиться к любому объекту своего объявленного типа или любому подтипу своего объявленного типа. Ссылочную переменную можно объявить как класс или тип интерфейса.

Пример 1

Рассмотрим пример наследования полиморфизм в Java.

public interface Vegetarian{}
public class Animal{}
public class Deer extends Animal implements Vegetarian{}

Теперь класс Deer (Олень) считается полиморфным, так как он имеет множественное наследование. Следующие утверждения верны для примера выше:

  • A Deer IS-A Animal (олень - это животное);
  • A Deer IS-A Vegetarian (олень - это вегетарианец);
  • A Deer IS-A Deer (олень - это олень);
  • A Deer IS-A Object (олень - это объект).

Когда мы применяем факты ссылочной переменной к ссылке на объект Deer (Олень), следующие утверждения верны:

Пример 2

Deer d = new Deer();
Animal a = d;
Vegetarian v = d;
Object o = d;

Все переменные (d, a, v, o) ссылаются к тому же объекту Deer (Олень).

Виртуальные методы

В этом разделе рассмотрим, как поведение переопределённых методов в Java позволяет воспользоваться преимуществами полиморфизма при оформлении классов.

Мы уже рассмотрели переопредение методов, где дочерний класс может переопределить метод своего «родителя». Переопределённый метод же скрыт в родительском классе и не вызван, пока дочерний класс не использует ключевое слово super во время переопределения метода.

Пример

/* File name : Employee.java */
public class Employee {
   private String name;
   private String address;
   private int number;

   public Employee(String name, String address, int number) {
      System.out.println("Собираем данные о работнике");
      this.name = name;
      this.address = address;
      this.number = number;
   }

   public void mailCheck() {
      System.out.println("Отправляем чек " + this.name + " " + this.address);
   }

   public String toString() {
      return name + " " + address + " " + number;
   }

   public String getName() {
      return name;
   }

   public String getAddress() {
      return address;
   }

   public void setAddress(String newAddress) {
      address = newAddress;
   }

   public int getNumber() {
      return number;
   }
}

Теперь предположим, что мы наследуем класс Employee следующим образом:

/* File name : Salary.java */
public class Salary extends Employee {
   private double salary; // Годовая заработная плата
   
   public Salary(String name, String address, int number, double salary) {
      super(name, address, number);
      setSalary(salary);
   }
   
   public void mailCheck() {
      System.out.println("Внутри mailCheck класса Salary ");
      System.out.println("Отправляем чек " + getName()
      + " с зарплатой " + salary);
   }
   
   public double getSalary() {
      return salary;
   }
   
   public void setSalary(double newSalary) {
      if(newSalary >= 0.0) {
         salary = newSalary;
      }
   }
   
   public double computePay() {
      System.out.println("Вычисляем заработную плату для " + getName());
      return salary/52;
   }
}

Теперь, внимательно изучите программу и попытайтесь предугадать её вывод:

/* File name : VirtualDemo.java */
public class VirtualDemo {

   public static void main(String [] args) {
      Salary s = new Salary("Олег Петров", "Минск, Беларусь", 3, 3600.00);
      Employee e = new Salary("Иван Иванов", "Москва, Россия", 2, 2400.00);
      System.out.println("Вызываем mailCheck, используя ссылку Salary --");   
      s.mailCheck();
      System.out.println("Вызываем mailCheck, используя ссылку Employee --");
      e.mailCheck();
   }
}

После запуска программы будет выдан такой результат:

Собираем данные о работнике
Собираем данные о работнике

Вызываем mailCheck, используя ссылку Salary ––
Внутри mailCheck класса Salary 
Отправляем чек Олег Петров с зарплатой 3600.0

Вызываем mailCheck, используя ссылку Employee ––
Внутри mailCheck класса Salary
Отправляем чек Иван Иванов с зарплатой 2400.0

Итак, мы создали два объекта Salary. Один использует ссылку Salary, то есть s, а другой использует ссылку Employee, то есть e.

Во время вызова s.mailCheck(), компилятор видит mailCheck() в классе Salary во время компиляции, а JVM вызывает mailCheck() в классе Salary при запуске программы.

mailCheck() в e совсем другое, потому что e является ссылкой Employee. Когда компилятор видит e.mailCheck(), компилятор видит метод mailCheck() в классе Employee.

Во время компиляции был использован mailCheck() в Employee, чтобы проверить это утверждение. Однако во время запуска программы JVM вызывает mailCheck() в классе Salary.

Это поведение называется вызовом виртуальных методов, а эти методы называются виртуальными. Переопределённый метод вызывается во время запуска программы вне зависимости от того, какой тип данных был использован в исходном коде во время компиляции.

Оглавление