JavaRush /Java блог /Random UA /Поліморфізм у Java

Поліморфізм у Java

Стаття з групи Random UA
Запитання, присвячені ООП - невід'ємна частина технічного інтерв'ю на позицію Java-розробника в ІТ-компанію. У цій статті поговоримо про один із принципів ОВП – поліморфізм. Ми зупинимося на аспектах, які часто запитують на співбесідах, а також наведемо невеликі приклади для наочності.

Що таке поліморфізм?

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

Поліморфізм в Java на співбесіді - 1
Почати можна з того, що підхід ООП передбачає побудову Java-програми на основі взаємодії об'єктів, що базуються на класах. Класи– це заздалегідь написані креслення (шаблони), якими будуть створені об'єкти у програмі. Причому клас завжди має певний тип, який за гарного стилю програмування своєю назвою «підказує» про своє призначення. Далі можна відзначити, що оскільки Java відноситься до строго типізованих мов, у програмному коді завжди потрібно вказати тип об'єкта при оголошенні змінних. До цього додайте, що сувора типізація підвищує безпеку коду, надійність програми і дозволяє ще на стадії компіляції запобігти помилкам несумісності типів (наприклад, спробу розділити рядок на число). Природно, компілятор повинен «знати» тип, що оголошується – це може бути клас з JDK або створений нами власноруч. Зверніть увагу інтерв'юера, що під час роботи з програмним кодом ми можемо використовувати не лише об'єкти типу,Це важливий момент: ми можемо працювати з багатьма типами як з одним (за умови, що ці типи є похідними від базового типу). Також це означає, що, оголосивши змінну типу суперкласу, ми можемо надати їй значення одного зі спадкоємців. Інтерв'юеру сподобається, якщо ви наведете приклад. Виберіть якийсь об'єкт, який може бути загальним (базовим) для групи об'єктів і успадкуйте від нього кілька класів. Базовий клас:
public class Dancer {
    private String name;
    private int age;

    public Dancer(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void dance() {
        System.out.println(toString() + "Я танцюю як усі.");
    }

    @Override
    public String toString() {
        return "Я " + name + ", мені" + age + "Рік." ;
    }
}
У спадкоємцях перевизначте метод базового класу:
public class ElectricBoogieDancer extends Dancer {
    public ElectricBoogieDancer(String name, int age) {
        super(name, age);
    }
// Перевизначення методу базового класу
    @Override
    public void dance() {
        System.out.println( toString() + "Я танцюю електрик буги!");
    }
}

public class BreakDankDancer extends Dancer{

    public BreakDankDancer(String name, int age) {
        super(name, age);
    }
// Перевизначення методу базового класу
    @Override
    public void dance(){
        System.out.println(toString() + "Я танцюю брейк-данс!");
    }
}
Приклад поліморфізму в Java та використання об'єктів у програмі:
public class Main {

    public static void main(String[] args) {
        Dancer dancer = new Dancer("Антон", 18);

        Dancer breakDanceDancer = new BreakDankDancer("Олексій", 19);// висхідне перетворення до базового типу
        Dancer electricBoogieDancer = new ElectricBoogieDancer("Ігор", 20); // висхідне перетворення до базового типу

        List<Dancer> discotheque = Arrays.asList(dancer, breakDanceDancer, electricBoogieDancer);
        for (Dancer d : discotheque) {
            d.dance();// Поліморфний виклик методу
        }
    }
}
На коді методу mainпокажіть, що у рядках:
Dancer breakDanceDancer = new BreakDankDancer("Олексій", 19);
Dancer electricBoogieDancer = new ElectricBoogieDancer("Ігор", 20);
ми оголосабо змінну типу суперкласу, а надали їй значення одного зі спадкоємців. Швидше за все, вас запитають, чому компілятор не «лаятиметься» на невідповідність типів, оголошених ліворуч і праворуч від знака присвоювання, адже Java сувора типізація. Поясніть, що тут працює висхідне перетворення типів - посилання на об'єкт інтерпретується, як посилання на базовий клас. Причому компілятор, зустрівши в коді таку конструкцію, робить це автоматично та неявно. На основі коду прикладу можна показати, що тип класу, оголошений ліворуч від знака присвоювання Dancerмає кілька форм (типів), оголошених праворуч BreakDankDancer, ElectricBoogieDancer. Кожна з форм може мати власну унікальну поведінку для загальної функціональності, визначеної у суперкласі – методdance. Тобто метод, оголошений у суперкласі, може бути по-різному реалізований у спадкоємцях. В даному випадку ми маємо справу з перевизначенням методу, а це саме те, що створює різноманітність форм (поведінок). Побачити це можна, запустивши код методу main на виконання: Висновок програми Я Антон, мені 18 років. Я танцюю як усі. Я Олексій, мені 19 років. Я танцюю брейк-данс! Я Ігор, мені 20 років. Я танцюю електрик буги! Якщо не використовувати перевизначення у спадкоємцях, то ми не отримаємо різної поведінки. Наприклад, якщо для наших класів BreakDankDancerі ElectricBoogieDancerзакоментувати метод dance, то висновок програми буде таким: Я Антоне, мені 18 років. Я танцюю як усі. Я Олексій, мені 19 років. Я танцюю як усі. Я Ігор, мені 20 років. Я танцюю як усі. а це означає, що створювати нові класи BreakDankDancerі ElectricBoogieDancerпросто нема рації. А в чому ж, власне, проявляється принцип поліморфізму Java? Де заховано використання об'єкта у програмі без знання про його конкретний тип? У прикладі — це виклик методу d.dance()на об'єкті dтипу Dancer. Під поліморфізмом Java мається на увазі те, що програмі необов'язково знати якого саме типу буде об'єкт BreakDankDancerабо ElectricBoogieDancer. Головне, що він – нащадок класу Dancer. І якщо міркувати про нащадків, слід зазначити, що успадкування в Java - це не тільки extends, але іimplements. Тут саме час згадати, що в Java не підтримується множинне спадкування - кожен тип може мати одного з батьків (суперклас) і необмежену кількість спадкоємців (підкласів). Тому для додавання кількох функціональностей до класів використовуються інтерфейси. Інтерфейси зменшують зв'язаність об'єктів з батьком у порівнянні з успадкуванням і використовуються дуже широко. У Java інтерфейс є типом посилання, тому в програмі може бути оголошений тип зміною типу інтерфейсу. Тут саме час навести приклад. Створимо інтерфейс:
public interface Swim {
    void swim();
}
Для наочності візьмемо різні та не пов'язані між собою об'єкти та реалізуємо в них інтерфейс:
public class Human implements Swim {
    private String name;
    private int age;

    public Human(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void swim() {
        System.out.println(toString()+"Я плаваю за допомогою надувного кола.");
    }

    @Override
    public String toString() {
        return "Я " + name + ", мені" + age + "Рік.";
    }

}

public class Fish implements Swim{
    private String name;

    public Fish(String name) {
        this.name = name;
    }

    @Override
    public void swim() {
        System.out.println("Я риба" + name + ". Я пливу, рухаючи плавцями.");

    }

public class UBoat implements Swim {

    private int speed;

    public UBoat(int speed) {
        this.speed = speed;
    }

    @Override
    public void swim() {
        System.out.println("Підводний човен пливе, обертаючи гвинти, зі швидкістю" + speed + "вузлів.");
    }
}
Метод main:
public class Main {

    public static void main(String[] args) {
        Swim human = new Human("Антон", 6);
        Swim fish = new Fish("кит");
        Swim boat = new UBoat(25);

        List<Swim> swimmers = Arrays.asList(human, fish, boat);
        for (Swim s : swimmers) {
            s.swim();
        }
    }
}
p align="justify"> Результат виконання поліморфного методу, визначеного в інтерфейсі, дозволяє нам побачити відмінності в поведінці типів, що реалізують цей інтерфейс. Вони полягають у різних результатах виконання методу swim. Вивчивши наш приклад, інтерв'юер може запитати, чому при виконанні кодуmain
for (Swim s : swimmers) {
            s.swim();
}
для наших об'єктів викликаються методи, визначені у цих класах? Яким чином відбувається вибір необхідної реалізації методу під час виконання програми? Щоб відповісти на ці питання, необхідно розповісти про пізнє (динамічне) зв'язування. Під зв'язуванням розуміють встановлення зв'язку між викликом методу та його конкретною реалізацією у класах. По суті визначається код, якого з трьох методів, визначених у класах, буде виконано. У Java за замовчуванням використовується пізнє зв'язування (на стадії виконання програми, а не під час компіляції, як у випадку з раннім зв'язуванням). Це означає, що при компіляції коду
for (Swim s : swimmers) {
            s.swim();
}
компілятор ще не знає, код з якого класу - Human, Fishабо Uboatвін виконуватиме в методіswim. Це визначиться лише за виконання програми завдяки механізму динамічної диспетчеризації — перевірки типу об'єкта під час виконання програми та вибору потрібної реалізації методу цього типу. Якщо вас запитають, як це реалізовано, можете відповісти, що при завантаженні та ініціалізації об'єктів JVM будує таблиці в пам'яті, і в них пов'язує змінні з їх значеннями, а об'єкти з їх методами. Причому якщо об'єкт успадковується чи імплементує інтерфейс, насамперед перевіряється наявність перевизначених методів у його класі. Якщо такі є, вони прив'язуються до цього типу, якщо ні – шукається метод, визначений у класі на щабель вище (у батька) і так до кореня при багаторівневій ієрархії. Розмірковуючи про поліморфізм в ОВП та його реалізацію в програмному коді, зазначимо, що гарною практикою є використання абстрактних описів визначення базових класів з допомогою абстрактних класів, і навіть інтерфейсів. Ця практика заснована на використанні абстракції — виділення загальної поведінки та властивостей та укладання їх у рамки абстрактного класу, або виділення лише загальної поведінки – у такому разі ми створюємо інтерфейс. Побудова та проектування ієрархії об'єктів на основі інтерфейсів та успадкування класів є обов'язковою умовою для виконання принципу поліморфізму ООП. Торкаючись питання поліморфізму та нововведень у Java, можна згадати, що при створенні абстрактних класів та інтерфейсів, починаючи з Java 8, є можливість написання дефолтної реалізації абстрактних методів у базових класах за допомогою ключового слова і навіть інтерфейсів. Ця практика заснована на використанні абстракції — виділення загальної поведінки та властивостей та укладання їх у рамки абстрактного класу, або виділення лише загальної поведінки – у такому разі ми створюємо інтерфейс. Побудова та проектування ієрархії об'єктів на основі інтерфейсів та успадкування класів є обов'язковою умовою для виконання принципу поліморфізму ООП. Торкаючись питання поліморфізму та нововведень у Java, можна згадати, що при створенні абстрактних класів та інтерфейсів, починаючи з Java 8, є можливість написання дефолтної реалізації абстрактних методів у базових класах за допомогою ключового слова і навіть інтерфейсів. Ця практика заснована на використанні абстракції — виділення загальної поведінки та властивостей та укладання їх у рамки абстрактного класу, або виділення лише загальної поведінки – у такому разі ми створюємо інтерфейс. Побудова та проектування ієрархії об'єктів на основі інтерфейсів та успадкування класів є обов'язковою умовою для виконання принципу поліморфізму ООП. Торкаючись питання поліморфізму та нововведень у Java, можна згадати, що при створенні абстрактних класів та інтерфейсів, починаючи з Java 8, є можливість написання дефолтної реалізації абстрактних методів у базових класах за допомогою ключового слова або виділення лише загальної поведінки – у такому разі ми створюємо інтерфейс. Побудова та проектування ієрархії об'єктів на основі інтерфейсів та успадкування класів є обов'язковою умовою для виконання принципу поліморфізму ООП. Торкаючись питання поліморфізму та нововведень у Java, можна згадати, що при створенні абстрактних класів та інтерфейсів, починаючи з Java 8, є можливість написання дефолтної реалізації абстрактних методів у базових класах за допомогою ключового слова або виділення лише загальної поведінки – у такому разі ми створюємо інтерфейс. Побудова та проектування ієрархії об'єктів на основі інтерфейсів та успадкування класів є обов'язковою умовою для виконання принципу поліморфізму ООП. Торкаючись питання поліморфізму та нововведень у Java, можна згадати, що при створенні абстрактних класів та інтерфейсів, починаючи з Java 8, є можливість написання дефолтної реалізації абстрактних методів у базових класах за допомогою ключового словаdefault. Наприклад:
public interface Swim {
    default void swim() {
        System.out.println("Просто пливу");
    }
}
Іноді можуть поставити запитання щодо вимог до оголошення методів у базових класах, щоб не порушувався принцип поліморфізму. Тут все просто: ці методи не повинні бути static , private та final . private робить метод доступним тільки в класі, і ви не зможете його перевизначити в спадкоємці. Static робить метод надбанням класу, а не об'єкта, тому завжди буде викликатись метод суперкласу. Final же зробить метод незмінним та прихованим від спадкоємців.

Що нам дає поліморфізм у Java?

Питання, що дає нам використання поліморфізму, швидше за все, теж буде. Тут можна відповідати коротко, особливо не лазячи в нетрі:
  1. Дозволяє замінювати продажі об'єктів. На цьому ґрунтується тестування.
  2. Забезпечує розширюваність програми — набагато легше створювати заділ на майбутнє. Додавання нових типів на основі існуючих – найчастіший спосіб розширення функціональності програм, написаних в стилі ООП.
  3. Дозволяє об'єднувати об'єкти із загальним типом чи поведінкою в одну колекцію чи масив та керувати ними однаково (як у наших прикладах, змушуючи всіх танцювати – метод danceчи плисти – метод swim).
  4. Гнучкість під час створення нових типів: ви можете вибирати реалізацію методу з батька чи перевизначити їх у нащадку.

Побажання в дорогу

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