Поліморфізм — один з основних принципів об'єктно-орієнтованого програмування. Він дозволяє використовувати всю потужність строгої типізації Java і писати зручний та підтримуваний код. Про нього сказано багато, але сподіваюся, що з цього огляду кожен зможе винести щось нове для себе.![Поліморфізм і його друзі - 1]()
Наприклад, оголошуючи метод public void method(Object o), сигнатурою буде назва method і тип параметра Object. Тип повернутого значення НЕ входить у сигнатуру. Це важливо!
Далі проведемо компіляцію нашого початкового коду. Як ми знаємо, для цього код треба зберегти у файл з іменем класу та з розширенням java.
Код мовою Java компілюється за допомогою компілятора "javac" у певний проміжний формат, який вміє виконувати віртуальна машина Java (JVM). Цей проміжний формат називається байткодом і міститься у файлах з розширенням .class.
Виконаємо команду для компіляції: javap -c MusicPlayer:
З байткоду ми можемо побачити, що виклик методу через об'єкт, типом якого був вказаний клас виконується за допомогою ? Тому що йде виклик (invoke перекладається як викликати) віртуального методу.
Що таке віртуальний метод? Це такий метод, тіло якого може бути перевизначено в момент виконання програми. Уявіть просто, що у вас є певний список відповідностей деякого ключа (сигнатури методу) і тіла (коду) методу. І ця відповідність ключа і тіла методу під час виконання програми може змінюватися. Тому метод віртуальний.
За замовчуванням у Java методи, які НЕ static, НЕ final і НЕ private, є віртуальними. Завдяки цьому Java підтримує такий принцип об'єктно-орієнтованого програмування як поліморфізм. Як ви вже могли зрозуміти, про це наш сьогоднішній огляд.
Перевизначення (Overriding) або динамічний поліморфізм.
Отже, почнемо з того, що збережемо файл HelloWorld.java зі наступним вмістом:
Як видно, у байткоді для рядків із викликом методу вказана однакова посилання на метод для виклику
З перевизначенням також пов’язане таке поняття, як "коваріантність" (Covariance). Розглянемо приклад:
![Поліморфізм і його друзі - 7]()
![Поліморфізм та його друзі - 8]()
Тобто під час перевантаження компілятор перевіряє коректність. Це важливо.
Але як саме компілятор визначає, який метод потрібно викликати? Він використовує правило "the Most Specific Method", описане в специфікації мови Java : "15.12.2.5. Choosing the Most Specific Method".
Щоб продемонструвати його роботу, візьмемо приклад з Oracle Certified Professional Java Programmer:
Виходить, під час компіляції буде використана інформація про типи та кількість аргументів (яка доступна на момент компіляції), щоб визначити сигнатуру методу. Якщо метод належить до методів об'єкта (тобто instance method), реальний виклик методу буде визначено у runtime, використовуючи dynamic method lookup (тобто динамічне зв'язування).
Щоб стало зрозуміліше, візьмемо приклад, який схожий на раніше розглянутий:
Як зазначено, компілятор визначив, що в майбутньому буде викликаний деякий віртуальний метод. Тобто тіло методу буде визначено в runtime. Але на момент компіляції з усіх трьох методів компілятор вибрав найвідповідніший, тому вказав номер:
А що це за methodref такий? Це посилання на метод. Грубо кажучи, це деякий ключ, за яким під час виконання віртуальна Java машина зможе дійсно визначити, який метод потрібно шукати для виконання.
Детальніше можна ознайомитися в супер статті: "How Does JVM Handle Method Overloading And Overriding Internally".

Вступ
Думаю, всі ми знаємо, що мова програмування Java належить компанії Oracle. Тому, наш шлях починається з сайту: www.oracle.com. На головній сторінці є "Menu". У ньому в розділі "Documentation" є підрозділ "Java". Все, що стосується базових функцій мови належить до "Java SE documentation", тому обираємо цей розділ. Розділ документації відкриється для останньої версії, але наразі в "Looking for a different release?" обираємо варіант: JDK8. На сторінці ми побачимо багато різних варіантів. Але нас цікавить Learn the Language : "Java Tutorials Learning Paths". На цій сторінці ми знайдемо ще один розділ: "Learning the Java Language". Це - святая святих, tutorial по основам Java від Oracle. Java — об'єктно-орієнтована мова програмування (ООП), тому вивчення мови навіть на сайті Oracle починається з обговорення основних концепцій "Object-Oriented Programming Concepts". З самої назви зрозуміло, що Java орієнтований на роботу з об'єктами. Із підрозділу "What Is an Object?" зрозуміло, що об'єкти в Java складаються зі стану та поведінки. Уявіть, що у нас є рахунок у банку. Кількість грошей на рахунку - це стан, а методи роботи з цим станом - це поведінка. Об'єкти треба якось описувати (розповідати, яке у них може бути стан і поведінка) і цим описом є клас. Коли ми створюємо об'єкт якогось класу, то ми вказуємо цей клас і це називається "типом об'єкту". Саме тому й кажуть, що Java є строго типізованою мовою, про що сказано в специфікації Java в розділі "Chapter 4. Types, Values, and Variables". Мова Java слідує концепціям ООП і підтримує наслідування (Inheritance), використовуючи ключове слово extends (тобто розширення типу). Чому розширення? Бо при наслідуванні дочірній клас успадковує поведінку і стан батьківського класу і може їх доповнити, тобто розширити функціональність базового класу. Також в описі класу може бути вказаний інтерфейс (Interface) за допомогою ключового слова implements. Коли клас реалізує інтерфейс, це означає, що клас відповідає певному контракту - декларації програміста оточенню, що клас має певну поведінку. Наприклад, у плеєра є різні кнопки. Ці кнопки - інтерфейс для управління поведінкою плеєра, а поведінка змінюватиме внутрішній стан плеєра (наприклад, гучність). При цьому стан і поведінка як опис дадуть клас. Якщо клас реалізує інтерфейс, то об'єкт створений за цим класом може бути описаний типом не лише за класом, але і за інтерфейсом. Давайте вже подивимось на приклад:
public class MusicPlayer {
public static interface Device {
public void turnOn();
public void turnOff();
}
public static class Mp3Player implements Device {
public void turnOn() {
System.out.println("Увімкнено. Готовий до mp3.");
}
public void turnOff() {
System.out.println("Вимкнено");
}
}
public static class Mp4Player extends Mp3Player {
@Override
public void turnOn() {
System.out.println("Увімкнено. Готовий до mp3/mp4.");
}
}
public static void main(String []args) throws Exception{
// Якийсь пристрій (Тип = Device)
Device mp3Player = new Mp3Player();
mp3Player.turnOn();
// У нас є mp4 програвач, але нам від нього потрібен тільки mp3
// Користуємось ним як mp3 програвачем (Тип = Mp3Player)
Mp3Player mp4Player = new Mp4Player();
mp4Player.turnOn();
}
}
Тип — це дуже важливий опис. Він розповідає, як ми збираємося працювати з об'єктом, тобто яку поведінку від об'єкта очікуємо. Поведінка - це методи. Тому, давайте розбиратись з методами. На сайті Oracle методам відведено свій розділ в Oracle Tutorial : "Defining Methods".
Перше, що варто винести із статті: Сигнатура методу — це назва методу та типи параметрів:

javac MusicPlayer.java
Після того, як java код скомпільований, ми можемо його виконувати. Використовуючи утиліту "java" для запуску буде запущений процес віртуальної машини java для виконання переданого у class файлі байткоду.
Виконаємо команду для запуску додатку: java MusicPlayer.
Ми побачимо на екрані текст, зазначений у вхідному параметрі методу println. Цікаво, що маючи байткод у файлі з розширенням .class ми можемо його переглянути за допомогою утиліти "javap".
Виконаємо команду 
invokevirtual, а компілятор обчислив, яку сигнатуру методу треба використовувати.
Чому invokevirtualПоліморфізм
На сайті Oracle в їхньому офіційному Tutorial є окремий розділ: "Polymorphism". Скористаємося Java Online Compiler'ом щоб побачити, як працює поліморфізм у Java. Наприклад, у нас є деякий абстрактний клас Number, який представляє число в Java. Що він дозволяє? У нього є деякі базові методи, які будуть у всіх нащадків. Той, хто наслідується від Number, буквально каже - "Я число, зі мною можна працювати як із числом". Наприклад, для будь-якого нащадка можна за допомогою методу intValue() отримати його Integer значення. Якщо подивитися java api для Number, то видно, що метод abstract, тобто цей метод кожен нащадок Number зобов’язаний реалізувати сам. Але що нам це дає? Подивимося на приклад:
public class HelloWorld {
public static int summ(Number first, Number second) {
return first.intValue() + second.intValue();
}
public static void main(String []args){
System.out.println(summ(1, 2));
System.out.println(summ(1L, 4L));
System.out.println(summ(1L, 5));
System.out.println(summ(1.0, 3));
}
}
Як видно з прикладу, завдяки поліморфізму, ми можемо написати метод, який на вхід прийматиме аргументи будь-якого типу, який буде нащадком Number (Number ми не можемо отримати, оскільки це абстрактний клас). Як було в прикладі з плеєром, у цьому випадку ми говоримо, що хочемо працювати з чимось, як із Number. Ми знаємо, що будь-хто, хто є Number, зобов’язаний уміти надати своє integer значення. І нам цього достатньо. Ми не хочемо вдаватися в деталі реалізації конкретного об'єкта і хочемо працювати з цим об'єктом через загальні для всіх нащадків Number методи.
Список методів, які нам будуть доступні, визначатиметься за типом під час компіляції (як це ми бачили раніше у байткоді). У цьому випадку у нас тип буде Number. Як видно з прикладу, ми передаємо різні числа різного типу, тобто на вхід метод summ отримуватиме і Integer, і Long, і Double. Але всіх їх об’єднує те, що вони є нащадками від абстрактного Number, а відповідно перевизначили у себе поведінку в методі intValue, оскільки кожен конкретний тип знає, як цей тип потрібно приводити до Integer.
Такий поліморфізм реалізований через так зване перевизначення, англійською Overriding.

public class HelloWorld {
public static class Parent {
public void method() {
System.out.println("Parent");
}
}
public static class Child extends Parent {
public void method() {
System.out.println("Child");
}
}
public static void main(String[] args) {
Parent parent = new Parent();
Parent child = new Child();
parent.method();
child.method();
}
}
Виконаємо javac HelloWorld.java і javap -c HelloWorld:

invokevirtual (#6).
Виконаємо java HelloWorld. Як ми бачимо, змінні parent і child оголошені з типом Parent, проте сама реалізація викликана згідно з тим, який об’єкт був присвоєний змінній (тобто об’єкт якого типу). Під час виконання програми (ще кажуть у рантаймі) JVM, залежно від об’єкта під час виклику методів за однією і тією ж сигнатурою, виконувала різні методи. Тобто за ключем відповідної сигнатури спочатку отримали одне тіло методу, а потім отримали інше. Залежно від того, який об’єкт лежить у змінній.
Таке визначення під час виконання програми того, який метод буде викликаний, називається ще пізнім зв'язуванням або Dynamic Binding. Тобто відповідність сигнатури і тіла методу виконується динамічно, залежно від об’єкта, для якого викликається метод.
Природно, не можна перевизначити статичні члени класу (Class member), а також члени класу з типом доступу private або final.
На допомогу розробникам також приходять анотації @Override. Вона допомагає компілятору зрозуміти, що в цьому місці ми збираємося перевизначити поведінку метод предка. Якщо ми помилилися у сигнатурі методу, то компілятор нам одразу про це скаже. Наприклад:
public static class Parent {
public void method() {
System.out.println("parent");
}
}
public static class Child extends Parent {
@Override
public void method(String text) {
System.out.println("child");
}
}
Не скомпілюється з помилкою: error: method does not override or implement a method from a supertype

public class HelloWorld {
public static class Parent {
public Number method() {
return 1;
}
}
public static class Child extends Parent {
@Override
public Integer method() {
return 2;
}
}
public static void main(String[] args) {
System.out.println(new Child().method());
}
}
Незважаючи на зовнішню складність, сенс зводиться до того, що під час перевизначення ми можемо повернути не тільки той тип, який був вказаний у предку, але й більш конкретний тип.
Наприклад, предок повертав Number, а ми можемо повернути Integer - нащадка від Number. Те ж саме стосується і винятків, оголошених у throws у методі. Нащадки можуть перевизначити метод і уточнити виняток, що кидається. Але не можуть розширити. Тобто якщо батько кидає IOException, то ми можемо кинути більш точний EOFException, але не можемо кинути Exception.
Аналогічно, не можна звужувати область видимості і не можна накладати додаткові обмеження. Наприклад, не можна додавати static.

Сокриття (Hiding)
Є ще таке поняття, як "сокриття". Приклад:
public class HelloWorld {
public static class Parent {
public static void method() {
System.out.println("Parent");
}
}
public static class Child extends Parent {
public static void method() {
System.out.println("Child");
}
}
public static void main(String[] args) {
Parent parent = new Parent();
Parent child = new Child();
parent.method();
child.method();
}
}
Це досить очевидна річ, якщо задуматись. Статичні члени класу належать до класу, тобто до типу змінної. Тому, логічно, що якщо child має тип Parent, то і метод буде викликаний у Parent, а не у child.
Якщо ми подивимося байткод, як ми вже робили раніше, то побачимо, що виклик статичного методу здійснюється за допомогою invokestatic. Це пояснює JVM, що треба дивитися на тип, а не за таблицею методів, як це робив invokevirtual чи invokeinterface.

Перевантаження методів (Overloading)
Що ми ще бачимо в Java Oracle Tutorial? У раніше вивченому розділі "Defining Methods" є щось про Overloading. Що це таке? Українською це "перевантаження методів", а такі методи називаються "перевантаженими". Отже, перевантаження методів. На перший погляд, все просто. Відкриємо онлайн-компілятор Java, наприклад tutorialspoint online java compiler.
public class HelloWorld {
public static void main(String []args){
HelloWorld hw = new HelloWorld();
hw.say(1);
hw.say("1");
}
public static void say(Integer number) {
System.out.println("Integer " + number);
}
public static void say(String number) {
System.out.println("String " + number);
}
}
Отже, тут все здається простим. Як і сказано в tutorial від Oracle, перевантажені методи (у даному випадку це метод say) відрізняються за кількістю і типом аргументів, переданих у метод.
Не можна оголосити однакове ім'я і однакову кількість однакових типів аргументів, бо компілятор не зможе їх розрізнити.
Тут варто відразу зазначити дуже важливу річ:

public class Overload{
public void method(Object o) {
System.out.println("Object");
}
public void method(java.io.FileNotFoundException f) {
System.out.println("FileNotFoundException");
}
public void method(java.io.IOException i) {
System.out.println("IOException");
}
public static void main(String args[]) {
Overload test = new Overload();
test.method(null);
}
}
Приклад взятий тут: https://github.com/stokito/OCPJP/blob/master/src/ru/habrahabr/blogs/java/OCPJP1/question1/Overload.j...
Як видно, ми передаємо в метод null. Компілятор намагається визначити найбільш специфічний тип. Object не підходить, бо від нього успадковуються всі. Йдемо далі. Є 2 класи винятків. Подивимося на java.io.IOException і побачимо, що в "Direct Known Subclasses" є FileNotFoundException. Тобто виходить, що FileNotFoundException найбільш специфічний тип.
Тому, результатом буде виведення рядка "FileNotFoundException".
А от якщо замінити IOException на EOFException, то вийде, що у нас два методи знаходяться на одному рівні ієрархії за деревом типів, тобто для них обох IOException є батьком.
Компілятор не зможе вибрати, який метод потрібно викликати, і видасть помилку компіляції: reference to method is ambiguous.
Ще один приклад:
public class Overload{
public static void method(int... array) {
System.out.println("1");
}
public static void main(String args[]) {
method(1, 2);
}
}
Виведе 1. Тут питань немає. Тип int... є vararg https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html і насправді є не більше ніж "синтаксичним цукром" і насправді int... array можна читати як int[] array.
Якщо ми тепер додамо метод:
public static void method(long a, long b) {
System.out.println("2");
}
То стане виводитись не 1, а 2, бо ми передаємо 2 числа, і 2 аргументи більш точне співпадіння, ніж один масив.
Якщо ми додамо метод:
public static void method(Integer a, Integer b) {
System.out.println("3");
}
То ми все одно будемо бачити 2. Тому що в даному випадку примітиви більш точне співпадіння, ніж боксинг у Integer.
Проте, якщо ми виконаємо method(new Integer(1), new Integer(2)); то буде виведено 3.
Конструктори в Java схожі на методи, а оскільки за ними теж можна отримати сигнатуру, то для них діють ті ж правила "overloading resolution", що і для перевантажених методів. Специфікація мови Java нам так і повідомляє в "8.8.8. Constructor Overloading".
Перевантаження методів = Раннє зв'язування (воно ж Static Binding)
Часто можна почути про раннє та пізнє зв'язування, яке ще називають Static Binding або Dynamic Binding. Різниця між ними дуже проста. Рано — це компіляція, пізно — це момент виконання програми. Тому, раннє зв'язування (static binding) — визначення того, який метод у кого буде викликаний у момент компіляції. Ну а пізнє зв'язування (dynamic binding) — визначення того, який метод викликати, безпосередньо у момент виконання програми.
Як ми бачили раніше (коли заміняли IOException на EOFException), якщо ми перевантажимо методи так, що компілятор не зможе зрозуміти, де який виклик виконувати, то ми отримаємо помилку під час компіляції: reference to method is ambiguous. Слово ambiguous у перекладі з англійської — двозначний або невизначений, неточний. Виходить, що перевантаження — це раннє зв'язування, бо перевірка виконується у момент компіляції.
Щоб підтвердити свої міркування, відкриємо Java Language Specification на розділі "8.4.9. Overloading" :

public class HelloWorld {
public void method(int intNumber) {
System.out.println("intNumber");
}
public void method(Integer intNumber) {
System.out.println("Integer");
}
public void method(String intNumber) {
System.out.println("Number is: " + intNumber);
}
public static void main(String args[]) {
HelloWorld test = new HelloWorld();
test.method(2);
}
}
Збережемо цей код у файл HelloWorld.java і скомпілюємо його за допомогою javac HelloWorld.java
Тепер подивимося, що там написав наш компілятор у байткоді, виконавши команду: javap -verbose HelloWorld.

"invokevirtual #13"

ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ