Назад Зміст Вперед

7.8. Поліморфізм

Поліморфізм - це засіб для надання різних значень одній і тій же події в залежності від типу оброблюваних даних. Тобто поліморфізм визначає різні форми реалізації однойменної дії.

Cлово поліморфізм має грецьке походження і переводиться як що "має багато форм".
Загальне визначення:
Поліморфізм - ця властивість, яка дозволяє одно і теж ім'я використати для вирішення декількох технічно різних завдань.
У загальному сенсі, концепцією поліморфізму є ідея "один інтерфейс, безліч методів". Це означає, що можна створити загальний інтерфейс для групи близьких за змістом дій.
Перевагою поліморфізму є те, що він допомагає знижувати складність програм, дозволяючи використання одного інтерфейсу для єдиного класу дій. Вибір конкретної дії, залежно від ситуації, покладається на компілятор.
Стосовно ТОП, метою поліморфізму, являється використання одного імені для завдання загальних для класу дій. На практиці це означає здатність об'єктів вибирати внутрішню процедуру(метод) виходячи з типу даних, прийнятих в повідомленні.
Уявіть, що треба відкрити замок і у нас є зв'язка ключів. І ось ми стоїмо перед замком і намагаємося його відкрити. Ми маємо зв'язку ключів, у кожного з яких є якісь параметри(форма, розмір і так далі). Для того, щоб відкрити двері ми перибираем один ключ за іншим доки не знайдемо відповідний. Т.е. коли шаблон замку співпадає з шаблоном параметрів ключа, замок відкривається. Аналогічно працює компілятор при наявність декількох функцій.Він послідовно перевіряє шаблони функцій з одним і тим же ім'ям доки не знайде відповідний.

І ще один приклад.
Приміром, у нас є клас "автомобіль", в якому описано як повинен пересуватися автомобіль, як він повертає, як подає сигнал і так далі. Там же описаний метод "перемикання передачі". Припустимо, що в цьому методі класу "автомобіль" ми описали автоматичну коробку передач. А тепер нам необхідно описати клас "спортивний автомобіль", у якого механічне(ручне) перемикання швидкостей.Звичайно, можна було б описати наново усі методи для класу "спортивний автомобіль". Але навіщо, якщо у нас вже практично все описано і відлагоджено ?! Для цього і існує механізм спадкоємства. Ми вказуємо, що клас "спортивний автомобіль" наслідує з класу "автомобіль", а отже він має усі властивості і методи, описані для класу-батька. Єдине, що нам потрібно зробити - це переписати метод "перемикання передач" для механічної коробки передач.В результаті, при виклику методу "перемикання передач" виконуватиметься метод не батьківського класу, а самого класу "спортивний автомобіль".

Механізм роботи ТОП в таких випадках можна описати приблизно так: при виклику того або іншого методу класу спочатку шукається метод у самого класу. Якщо метод знайдений, то він виконується і пошук цього методу на цьому завершується. Якщо ж метод не знайдений, то звертаємося до батьківського класу і шукаємо викликаний метод у нього.Якщо знайдений - поступаємо як при знаходженні методу в самому класі. А якщо немає - продовжуємо подальший пошук вгору по ієрархічному дереву. Аж до кореня(верхнього класу) ієрархії.
Цей приклад показує
, так званий, механізм раннього зв'язування, який детально розглянутий при описі віртуальних функцій.

Методи класів позначаються модифікатором static не випадково - для них при компіляції програмного коду діє статичне зв'язування. Це означає, що в контексті якого класу вказано ім'я методу у вихідному коді, на метод того класу в скомпільованому коді і ставиться посилання. Тобто, здійснюється зв'язування імені методу в місці виклику з виконуваним кодом цього методу. Іноді статичне зв'язування називають раннім зв'язуванням, так як воно відбувається на етапі компіляції програми. Статичне зв'язування в Java використовується ще в одному випадку - коли клас оголошений з модифікатором final ("фінальний", "остаточний").

Методи об'єктів в Java є динамічними, тобто для них діє динамічне зв'язування. Воно відбувається на етапі виконання програми безпосередньо під час виклику методу, причому на етапі написання даного методу заздалегідь невідомо, з якого класу буде проведений виклик. Це визначається типом об'єкта, для якого працює даний код – до якого класу належить об'єкт, з того класу викликається метод. Таке зв'язування відбувається набагато пізніше того, як було скомпільовано код методу. Тому, такий тип зв'язування часто називають пізнім зв'язуванням.

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

Для пояснення цих не надто зрозумілих при першому читанні слів розглянемо приклад - роботу методу moveTo. Недосвідченим програмістам здається, що цей метод слід перевизначати в кожному класі-спадкоємця. Це дійсно можна зробити, і все буде правильно працювати. Але такий код буде вкрай надлишковим - адже реалізація методу буде у всіх класах-спадкоємців Figure абсолютно однаковою:

public void moveTo (int x, int y) {
hide ();
this.x = x;
this.y = y;
show ();
};


Крім того, в цьому випадку не використовуються переваги поліморфізму. Тому, ми не будемо так робити.

Ще часто викликає подив, навіщо в абстрактному класі Figure писати реалізацію даного методу. Адже використовувані в ньому виклики методів hide і show, на перший погляд, повинні бути викликами абстрактних методів - тобто, здається, взагалі не можуть працювати!

Але методи hide і show є динамічними, а це, як ми вже знаємо, означає, що зв'язування імені методу і його виконуваного коду проводиться на етапі виконання програми. Тому те, що дані методи вказано в контексті класу Figure, зовсім не означає, що вони будуть викликатися з класу Figure! Більш того, можна гарантувати, що методи hide і show ніколи не будуть викликатися з цього класу. Нехай у нас є змінні dot1 типу Dot і circle1 типу Circle, і їм призначені посилання на об'єкти відповідних типів. Розглянемо, як поведуть себе виклики

dot1.moveTo (x1, y1) і
circle1.moveTo (x2, y2).

При виклику dot1.moveTo (x1, y1) відбувається виклик з класу Figure методу moveTo. Дійсно, цей метод в класі Dot НЕ перевизначений, а значить, він успадковується з Figure. У методі moveTo перший оператор - виклик динамічного методу hide. Реалізація цього методу береться з того класу, екземпляром якого є об'єкт dot1, що викликає даний метод. Тобто з класу Dot. Таким чином, ховається точка. Потім йде зміна координат об'єкту, після чого викликається динамічний метод show. Реалізація цього методу береться з того класу, екземпляром якого є об'єкт dot1, що викликає даний метод. Тобто з класу Dot. Таким чином, на новому місці показується точка.

Для виклику circle1.moveTo (x2, y2) все абсолютно аналогічно - динамічні методи hide і show викликаються з того класу, екземпляром якого є об'єкт circle1, тобто з класу Circle. Таким чином, ховається на старому місці і показується на новому саме коло.

Тобто якщо об'єкт є точкою, переміщається точка. А якщо об'єкт є колом - переміщається коло. Більше того, якщо коли-небудь хто-небудь напише, наприклад, клас Ellipse, є спадкоємцем Circle, і створить об'єкт
Ellipse ellipse = new Ellipse (...), то виклик ellipse.moveTo (...)
призведе до переміщення на нове місце еліпса.

І відбуватися це буде відповідно до того, яким чином в класі Ellipse реалізують методи hide і show. Зауважимо, що працювати буде давним-давно скомпільований поліморфний код класу Figure. Поліморфізм забезпечується тим, що посилання на ці методи в код методу moveTo в момент компіляції не ставляться - вони налаштовуються на методи з такими іменами з класу виклику об'єкта безпосередньо в момент виклику методу moveTo.

В об'єктно-орієнтованих мовах програмування розрізняють два різновиди динамічних методів - власне динамічні та віртуальні. За принципом роботи вони абсолютно аналогічні і відрізняються тільки особливостями реалізації. Виклик віртуальних методів швидше. Виклик динамічних повільніше, але службова таблиця динамічних методів (DMT - Dynamic Methods Table) займає трохи менше пам'яті, ніж таблиця віртуальних методів (VMT - Virtual Methods Table).

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