— Аміго, ти любиш китів?

— Китів? Ні, не чув про таке.

— Це як корова, тільки більше й плаває. До речі, кити походять від корів. Ну, чи мали спільного з ними предка. Це не суттєво.

— Так ось. Хочу розповісти тобі про ще один дуже потужний інструмент ООП – це поліморфізм. Він має особливості.

1) Перевизначення методу.

Уяви, що ти для гри написав клас «Корова». У ньому є багато полів та методів. Об'єкти цього класу можуть робити різні речі: йти, їсти, спати. Ще корови дзвонять у дзвіночок, коли ходять. Припустимо, ти реалізував у класі все до дрібниць.

Поліморфізм та перевизначення - 2

А тут приходить замовник проєкту та каже, що хоче випустити новий рівень гри, де всі дії відбуваються у морі, а головним героєм буде кит.

Ти почав проєктувати клас «Кит» і зрозумів, що він лише трохи відрізняється від класу «Корова». Логіка роботи обох класів дуже схожа, і ти вирішив використати успадкування.

Клас «Корова» ідеально підходить на роль класу-батька, там є всі необхідні змінні та методи. Достатньо лише додати киту можливість плавати. Але є проблема: у твого кита є ноги, роги та дзвіночок. Адже ця функціональність реалізована усередині класу «Корова». Що тут можна зробити?

До нас на допомогу приходить перевизначення (заміна) методів. Якщо ми успадкували метод, який робить не зовсім те, що потрібно в нашому новому класі, ми можемо замінити цей метод на інший.

Як же це робиться? У нашому класі-нащадку ми оголошуємо такий самий метод, як і метод класу батька, який хочемо змінити. Пишемо в ньому новий код. І все — начебто старого методу в класі-батьку і не було.

Ось як це працює:

Код Опис
class Cow {
  public void printColor() {
    System.out.println("Я - біла");
  }

  public void printName() {
    System.out.println("Я - корова");
  }
}

class Whale extends Cow {
  public void printName() {
    System.out.println("Я - кит");
  }
}
Тут визначено два класи Cow і WhaleWhale успадкований від Cow.








У класі Whale перевизначено метод printName();

public static void main(String[] args) {
  Cow cow = new Cow();
  cow.printName();
}
Цей код виведе на екран напис «Я – корова»
public static void main(String[] args) {
  Whale whale = new Whale();
  whale.printName();
}
Цей код виведе на екран надпис «Я – кит»

Після успадкування класу Cow і перевизначення методу printName, клас Whale фактично містить такі дані та методи:

Код Опис
class Whale {
  public void printColor() {
    System.out.println("Я - біла");
  }

  public void printName() {
    System.out.println("Я - кит");
  }
}
Про жодний старий метод ми й не знаємо.

— Відверто кажучи, очікувано.

2) Але це ще не все.

— Припустимо у класі Cow є метод printAll, який викликає два інших методи. Тоді код працюватиме так:

На екран буде виведено надпис. Я – білий Я – кит

Код Опис
class Cow {
  public void printAll() {
    printColor();
    printName();
  }

  public void printColor() {
    System.out.println("Я - біла");
  }

  public void printName() {
    System.out.println("Я - корова");
  }
}

class Whale extends Cow {
  public void printName() {
    System.out.println("Я - кит");
  }
}
public static void main(String[] args) {
  Whale whale = new Whale();
  whale.printAll();
}
На екран буде виведено надпис
Я – білий
Я – кит

Зверніть увагу, коли викликається метод printAll() написаний у класі Cow, в об'єкта типу Whale, буде використовуватися метод printName класу Whale, а не Cow.

Головне не в якому класі написаний метод, а який тип (клас) об'єкта, у якого цей метод викликаний.

— Зрозуміло.

— Успадковувати і перевизначати можна лише нестатичні методи. Статичні методи не успадковуються і, отже, не перевизначаються.

Ось як виглядає клас Whale після застосування успадкування та перевизначення методів:

Код Опис
class Whale {
  public void printAll() {
    printColor();
    printName();
  }

  public void printColor() {
    System.out.println("Я - біла");
  }

  public void printName() {
    System.out.println("Я - кит");
  }
}
Ось як виглядає клас Whale після застосування успадкування та перевизначення методу. Про жодний старий метод printName ми і не знаємо.

3) Приведення типів.

Тут є ще цікавіший момент. Оскільки клас при успадкуванні отримує всі методи та дані класу батька, об'єкт цього класу дозволено зберігати (привласнювати) у змінні класу батька (і батька батька, аж до Object). Приклад:

Код Опис
public static void main(String[] args) {
  Whale whale = new Whale();
  whale.printColor();
}
На екран буде виведено надпис
Я – біла
public static void main(String[] args) {
  Cow cow = new Whale();
  cow.printColor();
}
На екран буде виведено напис
Я – біла
public static void main(String[] args) {
  Object o = new Whale();
  System.out.println(o.toString());
}
На екран буде виведено напис
Whale@da435a
Метод toString() успадкований від класу Object.

— Дуже цікаво. А навіщо це може знадобитися?

— Це цінна властивість. Згодом ти зрозумієш, що дуже, дуже цінна.

4) Виклик методу об'єкта (динамічна диспетчеризація методів).

Ось як це виглядає:

Код Опис
public static void main(String[] args) {
  Whale whale = new Whale();
  whale.printName();
}
На екран буде виведено надпис
Я – кит.
public static void main(String[] args) {
  Cow cow = new Whale();
  cow.printName();
}
На екран буде виведено надпис
Я – кит.

Зверни увагу, що на те, який саме метод printName викличеться, від класу Cow чи Whale, впливає не тип змінної, а тип об'єкту, на який вона посилається.

У змінній типу Cow збережене посилання на об'єкт типу Whale, і викличеться метод printName, описаний в класі Whale.

— Це непросто для розуміння.

— Так, це не дуже очевидно. Запам'ятай головне правило:

Набір методів, які можна викликати у змінної, визначається типом змінної. А який саме метод/яка реалізація викликається, визначається типом/класом об'єкта, посилання на який зберігає змінна.

— Спробую.

— Ти постійно стикатимешся з цим, так що скоро зрозумієш і більше ніколи не забудеш.

5) Розширення та звуження типів.

Для посилальних типів, тобто класів, приведення типів працює не так, як для примітивних типів. Хоча у посилальних типів теж є розширення і звуження типу. Наприклад:

Розширення типу Опис
Cow cow = new Whale();
Класичне розширення типу. Тепер кита узагальнили (розширили) до корови, але в об'єкта типу Whale можна викликати лише ті методи, які описано в класі Cow.

Компілятор дозволить викликати у змінної cow тільки ті методи, які є у її типу — класу Cow.

Звуження типу Опис
Cow cow = new Whale();
if (cow instanceof Whale) {
  Whale whale = (Whale) cow;
}
Класичне звуження типу з перевіркою. Змінна cow типу Cow, зберігає посилання на об'єкт класу Whale.
Ми перевіряємо, що це так і є, і потім виконуємо операцію перетворення (звуження) типу. Або як її ще називають – downcast.
Cow cow = new Cow();
Whale whale = (Whale) cow; //exception
Посилальне звуження типу можна провести і без перевірки типу об'єкта.
При цьому, якщо у змінній cow зберігався об'єкт не класу Whale, буде згенеровано виняток – InvalidClassCastException.

6) А тепер ще на закуску. Виклик оригінального методу

Іноді тобі хочеться не замінити успадкований метод своїм при перевизначенні методу, а лише трохи доповнити його.

У цьому випадку дуже хочеться виконати в новому методі свій код і викликати той самий метод, але базового класу. І така можливість у Java є. Робиться це так: super.method().

Приклади:

Код Опис
class Cow {
  public void printAll() {
    printColor();
    printName();
  }

  public void printColor() {
    System.out.println("Я – білий");
  }

  public void printName() {
    System.out.println("Я – корова");
  }
}

class Whale extends Cow {
  public void printName() {
    System.out.print("Це неправда: ");
    super.printName();

    System.out.println("Я – кит");
  }
}
public static void main(String[] args){
  Whale whale = new Whale();
  whale.printAll();
}
На екран буде виведено надпис
Я – білий
Це неправда: Я – корова
Я – кит

— Гм. Оце так лекція. Мої робо-вуха мало не розплавилися.

— Так, це непростий матеріал, він один із найскладніших. Професор обіцяв підкинути посилання на матеріали інших авторів, щоб ти, якщо все ж таки щось не зрозумієш, міг заповнити ці пробіли.