JavaRush /Java блог /Random UA /Поліморфізм та його друзі
Viacheslav
3 рівень

Поліморфізм та його друзі

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

Вступ

Думаю, всі ми знаємо, що мова програмування 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("On. Ready for mp3.");
        }
        public void turnOff() {
            System.out.println("Off");
        }
    }

    public static class Mp4Player extends Mp3Player {
        @Override
        public void turnOn() {
            System.out.println("On. Ready for 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 ". Перше, що варто винести зі статті: Сигнатура методу - це назва методу та типи параметрів :
Поліморфізм та його друзі - 2
Наприклад, оголошуючи метод public void method(Object o), сигнатурою буде назва method та тип параметра Object. Тип значення, що повертається НЕ входить в сигнатуру. Це важливо! Далі виконаємо компіляцію нашого вихідного коду. Як ми знаємо, для цього код треба зберегти у файл з ім'ям класу та з розширенням java. Код на мові Java компілюється за допомогою компілятора " javac " у певний проміжний формат, який може виконувати віртуальна машина Java (JVM). Цей проміжний формат називається байткодом і міститься у файлух із розширенням .class. Виконаємо команду для компіляції: javac MusicPlayer.java Після того, як код java скомпільований, ми можемо його виконувати. Використовуючи утиліту .для запуску буде запущено процес віртуальної машини java для виконання переданого в class файлі байткоду. Виконаємо команду для запуску програми: . Ми побачимо на екрані java MusicPlayerтекст, вказаний у вхідному параметрі методу println. Цікаво, що маючи байткод у файлі з розширенням .class ми можемо його подивитися за допомогою утиліти " javap ". Виконаємо команду <ocde>javap -c MusicPlayer:
Поліморфізм та його друзі - 3
З байткод ми можемо побачити, що виклик методу через об'єкт, типом якого був зазначений клас виконується за допомогою invokevirtual, а компілятор обчислив, яку сигнатуру методу треба використовувати. Чомуinvokevirtual? Тому що йде виклик (invoke перекладається як викликати) віртуального методу. Що таке віртуальний метод? Це такий метод, тіло якого може бути перевизначено на момент виконання програми. Уявіть просто, що у вас є певний список відповідностей деякого ключа (сигнатури методу) та тіла (коду) методу. І ця відповідність ключа та тіла методу під час виконання програми може змінюватись. Тому метод віртуальний. За замовчуванням Java методи, які НЕ static, НЕ final і НЕ private, є віртуальними. Завдяки цьому Java підтримує такий принцип об'єктно-орієнтованого програмування, як поліморфізм. Як Ви вже могли зрозуміти, про це сьогоднішній огляд.

Поліморфізм

На сайті 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.
Поліморфізм та його друзі - 4
Перевизначення (Overriding) чи динамічний поліморфізм. Отже, почнемо з того, що збережемо файл HelloWorld.java з таким змістом:
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:
Поліморфізм та його друзі - 5
Як видно, в байткод для рядків з викликом методу вказано однакове посилання на метод для виклику 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
Поліморфізм та його друзі - 6
З перевизначенням як і пов'язане таке поняття, як " ковариантность " (Covariance). Розглянемо приклад:
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.
Поліморфізм та його друзі - 7

Приховування (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.
Поліморфізм та його друзі - 8

Перевантаження методів (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) відрізняються за кількістю та типом аргументів, переданих у метод. Не можна оголосити однакові ім'я та однакову кількість однакових типів аргументів, т.к. компілятор зможе їх відрізнити друг від друга. Тут варто відразу відзначити дуже важливу річ:
Поліморфізм та його друзі - 9
Тобто під час перевантаження компілятор перевіряє коректність. Це важливо. Але як насправді компілятор визначає, що потрібно викликати певний метод? Він використовує правило "the Most Specific Method", описаного в специфікації мови Java: " 15.12.2.5. Choosing the Most Specific Method ". Щоб продемонструвати його роботу, візьмемо приклад із Oracle Certified Professional Java Programmer:
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що навантаження - це раннє зв'язування, т.к. перевірка виконується на момент компіляції. Щоб підтвердити свої висновки відкриємо Java Language Specification на чолі "8.4.9. Overloading " :
Поліморфізм та його друзі - 10
Виходить, під час компіляції буде використано інформацію про типи та кількість аргументів (яка доступна на момент компіляції), щоб визначити сигнатуру методу. Якщо метод відноситься до методів об'єкта (тобто instance method), реальний виклик методу буде визначено в runtime, використовуючи dynamic method lookup (тобто динамічне зв'язування). Щоб стало зрозумілішим, візьмемо приклад, який схожий на раніше розглянутий:
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);
    }
}
Тепер подивимося javac HelloWorld.java , що там написав компілятор наш у байткоді, виконавши команду: javap -verbose HelloWorld.
Поліморфізм та його друзі - 11
Як зазначено, компілятор визначив, що у майбутньому буде викликаний певний віртуальний метод. Тобто тіло методу буде визначено в runtime. Але на момент компіляції з усіх трьох методів компілятор вибрав найкращий, тому вказав номер:"invokevirtual #13"
Поліморфізм та його друзі - 12
А що це за методдраф такий? Це посилання метод. Грубо кажучи, це деякий ключ, яким під час виконання віртуальна Java машина зможе дійсно визначити, який метод потрібно шукати для виконання. Докладніше можна ознайомитися в супер статті: " How Does JVM Handle Method Overloading And Overriding Internally ".

Підбиття підсумків

Отже, ми з'ясували, що Java як об'єктно-орієнтована мова підтримує поліморфізм. Поліморфізм буває статичним (Static Binding) та динамічним (Dynamic Binding). При статичному поліморфізмі, він раннє зв'язування, компілятор визначає, який метод і де потрібно викликати. Це дозволяє використовувати такий механізм як перевантаження. При динамічному поліморфізмі, він пізніше зв'язування, по раніше обчисленої сигнатурі методу в рантаймі буде обчислено метод на основі того, який об'єкт використовується (тобто метод якого об'єкта викликається). Те, як ці механізми працюють, можна побачити за допомогою байткоду. Перевантаження дивиться на сигнатури способів, а при дозволі навантаження вибирається більш специфічний (найточніший) варіант. При перевизначенні дивиться на тип визначення, які доступні методи, самі методи викликаються з урахуванням об'єкта. А також матеріали на тему: #Viacheslav
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ