Освоюємо Java/Об'єкти і класи

Матеріал з Вікіпідручника

Об'єктно-орієнтоване програмування (ООП) - значно змінило підхід до програмування увівши у вжиток цілий ряд понять і зокрема такі поняття як клас та об'єкти (екземпляри класу).

Парадигма ООП[ред.]

Парадигма(ідеологія) об’єктно-орієнтованого програмування (ООП) в даний час стала домінувати в програмному світі. Вона прийшла на зміну структурній техніці програмування, що була розроблена в 1970. Java є повністю об’єктно-орієнтованою мовою, тому потрібно засвоїти принципи ООП якомога краще.

В структурному програмуванні передбачалася розробка окремих алгоритмів та процедур для вирішення конкретної задачі. Такий підхід виправдує себе для невеликих задач, проте для великих проектів ООП більш виправдане. В літературі можна знайти приклад, що для реалізації простого веб-браузера необхідно близько 2000 процедур при структурному програмування. При використанні ж ООП можна створити 100 класів з приблизно 20-ма процедурами(далі методами) в кожному з них. Таким чином набагато простіше шукати помилку серед 20-ти методів одного класу ніж шукати її серед 2000 методів.

Як уже говорилося раніше: клас – це певний шаблон, який слугує для створення об’єктів. Ви можете розробити власний клас, а можете отримати його від інших розробників. В інтернеті зараз є чимало бібліотек класів різноманітного призначення як безкоштовних так і платних. Наприклад, бібліотеки для промальовування різноманітних діаграм у своїх програмах і т.п.

ООП в java базується на ряді понять (або ж концепцій)[1]:

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

Насправді доволі часто різні автори по різному виділяють головні концепції ООП. Причиною є те, що в ООП справді доволі багато понять. І з плином часу та розвитком програмування їх лише більшає. Різноманітні поняття доволі тісно взаємопов'язані між собою. В літературі часто виділяють наступні три концепції і навіть вказують, що вони основні для ООП ("три кити"):

  • інкапсуляція (incapsulation) - концепція побудови класів через закриття(капсулювання) їхньої реалізації.
  • успадкування (inheritance) - створення одних класів на основі інших
  • поліморфізм (polymorphism) - можливість використання батьківських класів замість класів нащадків. По суті є частиною реалізованої в мові концепції успадкування.

Деякі теоретики додають до цих трьох ще "абстрагування". Власне коли програміст створює клас, він створює певну абстракцію, модель чогось із реального світу. Ряд теоретичних книг побудовані на тому, як потрібно створювати класи, їхні ієрархії, зв'язки між ними. Проте переважно вони настільки теоретичні, що практичні програмісти часто питання абстрагування вирішують виходячи із конкретної задачі, яку потрібно вирішити. Щоправда є і винятки. В даний час виділяють так звані Патерни проектування, ряд шаблонів чи то зразків того, як найбільш ефективно вирішити деякі задачі в ООП. Проте для того, щоб їх можна було освоїти, необхідне ґрунтовне вивчення об'єктно-орієнтованого програмування і зокрема наведених вище понять. Поняття з обох вищенаведених списків є важливими для ООП і потрібно розуміти, що є що і як працює. Саме всім цим поняттям та їхній реалізації в мові програмування Java і присвячений даний розділ.

Об'єкти та об'єктні змінні[ред.]

Дані в об’єкті (тобто глобальні змінні даного об’єкту) називаються полями екземпляру класу. Процедури, які оперують даними називаються методами. Специфічний об’єкт, який є екземпляром певного класу матиме специфічні значення екземплярних полів. Сукупність цих значень називається станом об’єкта. Коли викликається певний метод стан об’єкту може змінюватися. Згідно принципу інкапсуляції намагаються, щоб доступ до полів екземпляру здійснювався лише за допомогою певних методів.

Об’єкт – це конкретна реалізація певного класу. На основі одного класу може бути створено безліч об’єктів. При цьому в об’єктах виділяють:

  • Поведінку об’єкту – що можна зробити з даним об’єктом, або які методи можна застосовувати до нього
  • Стан об’єкту – те як об’єкт змінюється, коли Ви застосовуєте його методи
  • Ідентичність об’єкту – відмінність об’єкту від інших об’єктів. Об’єкти можуть мати однаковий стан, проте все рівно вони ідентифікуються як різні об’єкти.

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

Щоб створити об’єкт певного класу програміст викликає один з конструкторів даного класу. Спеціальні методи, які покликані задати об’єкту початковий стан і носять його ім’я. Наприклад стандартна бібліотека джави містить клас Date, який описує момент часу. Як то “December 31, 1999, 23:59:59 GMT”

new Date(); // створюємо об’єкт, який містить поточний час

Ми можемо зразу ж вивести поточний час системи на екран:

System.out.println(new Date());

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

Так для нашого випадку, наступний рядок створить об’єктну змінну типу Date, яка посилатиметься на створюваний нами об'єкт.

Date curDate = new Date();

Значення об’єктної змінної може бути змінене, так що буде посилатися на інший об’єкт, або ж взагалі встановлене в null, тобто не вказувати на жодний об'єкт. Через об'єктні змінні можна здійснювати доступ до полів об'єкта та його методів.

Приклади створення і роботи з класами були наведені у: "Освоюємо Java/Вступ в класи та методи"

Пакети[ред.]

Джава дозволяє групувати класи в своєрідні колекції, які називаються пакетами. Це дозволяє, зокрема розмежувати класи з одним і тим же ім’ям. Так якщо ви розробили клас Car, то цілком логічно, що такий клас вже не раз розробляли.

Стандартна бібліотека Java поширюється з набором пакетів. Наприклад, java.lang, java.util, java.net і т.п. Все що необхідно для створення пакету, це створити звичайну папку і в ній розмістити ваші класи. Якщо папка вкладена в іншу утворюється ієрархія класів. Так всі стандартні класи Джава розміщені в каталогах java та javax.

Розробники Джава рекомендують розміщувати пакети по принципу доменних імен, лише в зворотньому порядку, наприклад: com.horstmann.corejava. Якщо ви або ваша фірма має доменне ім’я – це доволі зручний спосіб розміщення, оскільки забезпечує унікальність найменування пакету.

Імпортування пакетів[ред.]

Ваш клас може використовувати класи з власного пакету або всі відкриті класи з іншого пакету. Проте, якщо класи знаходяться в іншому пакеті, їх потрібно імпортувати. Це можна зробити двома способами:

  • вказувати повну назву пакету перед використовуваним класом:
java.util.Date today = new java.util.Date();

Такий спосіб доволі незручний.

  • Другий спосіб – це використати інструкцію import, яка ставиться на початку сирцевого коду програми.
import java.util.Date; // імпортуємо класс

можна також імпортувати цілий пакет, таким чином не лише класс Date буде доступний, але й усі класи, які знаходять в одному каталозі з ним:

import java.util.*;

Таким чином в коді програми можна писати:

 Date today = new Date();

Щоб визначити, який пакет вам потрібно імпортувати можна скористатися документацією класів. Деякі середовища розробки дозволяють зробити це автоматизовано при натисненні певного пункту меню. В NetBeans наприклад можна клацнути правою кнопкою миші будь-де у вікні редагування коду і вибрати пункт “Fix Import”. Щоправда дана команда також підчищає рядки з імпортом і забирає імпорт тих пакетів, які не використовуються.

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

Імпорт статичних класів[ред.]

Починаючи з Java SE 5.0 можливості використання імпорту були розширені. Тепер можна імпортувати не тільки класи, але й поля і методи статичних класів.

Наприклад, можна зробити так:

import static java.lang.System.*;
Таким чином можна уже не писати назву класу System:
out.println("Goodbye, World!"); // i.e., System.out
exit(0); // i.e., System.exit

Щоправда для класу System це є лишнім, оскільки код може стати менш читабельним. Проте використання такого імпорту для таких статичних класів як Math є цілком виправданим, оскільки замість:

Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))

Тепер можна писати:

sqrt(pow(x, 2) + pow(y, 2))

Компілювання і запуск програми, змінна CLASSPATH[ред.]

Після створення класів в пакеті, їх можна відкомпілювати так, як це ви робили раніше. Проте, якщо спробуєте запустити, то Ви отримаєте помилку інтерпретатора Джави, що не може знайти ваш клас. Причина в тому, що тепер потрібно запускати створений клас знаходячись в вершині ієрархії пакету. Крім того тепер при запуску на виконання потрібно задавати повну назву пакету. Так, якщо ваш клас знаходиться у пакеті test під назвою PackTest. То Вам необхідно вийти з каталогу test і звідти скористатися командою:

java test.PackTest

Інший спосіб – це встановлення змінної середовища CLASSPATH. Так у віндовзі вона встановлюється схожим чином як змінна path, де необхідно вказати ваш робочий каталог в якому знаходяться пакети. Так якщо ви працюєте в каталозі c:\work. То встановлюємо в CLASSPATH наступне:

.; C:\work 

Тепер команду java test.PackTest можна використовувати з будь-якого каталогу.

Можна також задавати змінну classpath при запуску компілятора та інтерпретатора:

java -classpath c:\classdir;.;c:\archives\archive.jar MyProg.java

Саме так здійснюють виклики віртуальної машини інтегровані середовища розробки (IDE). Якщо ви використовуєте певне середовище розробки, то в ньому є внутрішні можливості прописування classpath і вносити зміни в змінні середовища системи немає потреби.

(В даному розділі бракує прикладів програм)

Керування доступом в пакеті[ред.]

Розглянемо як впливають модифікатори доступу на видимість елементів класів. Так при модифікаторі public усе може бути використано будь-яким класом. Приватні елементи (private) можуть бути використані тільки класами даного пакету. Якщо явно не вказаний модифікатор, то використовується модифікатор по замовчуванню, який надає ширші можливості доступу ніж private проте вужчі ніж public. Дивіться таблицю доступу до членів класу.

Private Без модифікатора Protected Public
Той же клас Так Так Так Так
Підклас класу в цьому ж пакеті Ні Так Так Так
Клас цього ж пакету, що не є підкласом Ні Так Так Так
Підклас класу в іншому пакеті Ні Ні Так Так
Клас в другому пакеті, що не є підкласом класу даного пакету Ні Ні Ні Так

Інкапсуляція[ред.]

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

Інкапсуляція — один з основних принципів об'єктно-орієнтованого програмування. Йдеться про те, що об’єкт вміщує не тільки дані, але і правила їх обробки, оформлені в вигляді виконуваних фрагментів(методів). Доступ до стану об'єкта напряму заборонено, і ззовні з ним можна взаємодіяти виключно через заданий інтерфейс, що дозволяє контролювати правильність звернення до полів та їхню ініціалізацію. Також інкапсулюються методи, які виконують допоміжну роль і не бажано, щоб користувач-програміст мав доступ до них. Оскільки користувачі-програмісти працюють лише через відкриті елементи класів, то розробники класу можуть як-завгодно змінювати всі закриті елементи і навіть перейменовувати та видаляти їх, не турбуючись, що десь хтось їх використовує у своїх програмах.


Наприклад, вам потрібно зробити певну програму по роботі із списком автомобілів. Для цього потрібний відповідний клас Car:

package org.wikibooks.uk.osvjava;

public class Car {
    public static int count;
    public int id;

    public String _maker;
    public double _price;
    public String _year;
    public String _color;

    //конструктор без параметрів

    public Car() {
        count++;
        id = count;

    }

    //конструктор з параметрами, який ініціалізує всі поля класу

    public Car(String maker, String color, double price, String year) {
        _maker = maker;
        _price = price;
        _year = year;
        _color = color;

        count++;
        id = count;

    }

    //заміщення (перевизначення) методу toString() класу Object
    //замість дескриптора об'єкту, він виводитиме інформацію по автомобілю

    @Override
    public String toString() {
        return "Авто " + id + " " + _maker + " " + _color + " " + _price + " " + _year + " ";

    }

    //тестовий метод main
    public static void main(String[] args) {
       //створюємо об'єкт car1 конструктором без параметрів  
        Car car1 = new Car();
        car1._maker = "Audi";
        car1._price = 10000;
        car1._year = "2000";
        car1._color = "red";

       //створюємо об'єкт car2 конструктором з параметрами
        Car car2 = new Car("BMW", "black", 12000, "2001");

        //вивід інформації про автомобілі
        //при цьому застосовуватиметься заміщений в цьому класі метод toString
        System.out.println(car1); 
        System.out.println(car2);

    }
}

Результат виконання:

Авто 1: Audi red 10000.0 2000 
Авто 2: BMW black 12000.0 2001 

У вищенаведеному прикладі для того, щоб рахувати кількість об'єктів ми створюємо статичну змінну count, яка буде спільною для всіх об'єктів car. В методі main ми створюємо екземпляри класу Car двома способами. Спочатку вручну ініціалізуємо кожне поле car1, а поля car2 ініціалізуємо через конструктор. Крім того, що другий спосіб є більш простий, перший спосіб може слугувати ще й джерелом ряду помилок. Наприклад, ми можемо забути ініціалізувати певне поле. Крім того ініціалізуючи через конструктор ми можемо здійснити попередню перевірку на правильність введення даних. Наприклад на правильність введення назви виробника і т.п. Також, маємо два службових поля: count та id, які ініціалізуються в конструкторах, проте ми можемо задати значення і напряму, що може зашкодити логіці функціонування об'єкту класу. Ми запросто можемо вказати, що є 100 автомобілів, хоча насправді їх 66. І якщо будемо десь використовувати цикл з перебору автомобілів з врахування змінної count, це викличе помилку. Тому в ООП і придумано інкапсуляцію — приховування внутрішньої реалізації класу. Рекомендується оголошувати поля та методи з самого початку закритими і лише в разі необхідності надавати до них більший доступ.

Модифікуємо дещо нашу програму:

package org.wikibooks.uk.osvjava;

public class Car {
    private static int count=0;
    private int id;

    private String _maker;
    private double _price;
    private String _year;
    private String _color;


    //конструктор з параметрами, який ініціалізує всі поля класу
    public Car(String maker, String color, double price, String year) {
        _maker = maker;
        _price = price;
        _year = year;
        _color = color;

        count++;
        id = count;
    }

    //заміщення (перевизначення) методу toString() класу Object
    //замість дескриптора об'єкта, він виводитеме інформацію по автомобілю

    @Override
    public String toString() {
        return id+". "+ _maker + " " + _color + " " + _price + " " + _year + " ";

    }

    //метод для отримання значення поля id
    public int getId() {
        return id;
    }
    //метод для отримання кількості автомобілів
    public static int getCount() {
        return count;
    }
    
    //тестовий метод main

    public static void main(String[] args) {
        Car car[]=new Car[5];
        
        car[0] = new Car("Audi","red",10000,"2000" );
        car[1] = new Car("BMW", "black", 12000, "2001");     
        car[2] = new Car("Daewoo", "white", 8000, "2001");
        car[3] = new Car("Reno", "black", 12000, "2001");
    
        for (int i = 0; i < Car.getCount(); i++) {
            System.out.println(car[i]);
        }
    }
}

Результат виконання:

1. Audi red 10000.0 2000 
2. BMW black 12000.0 2001 
3. Daewoo white 8000.0 2001 
4. Reno black 12000.0 2001 

Як бачимо усі поля у нас тепер приватні. Робота з закритими полями можлива лише через відповідні методи. Для доступу до полів count та id створено методи getCount та getId. В разі необхідності можна створити методи для доступу до інших полів. Також можна створити певні методи модифікації окремих полів (setId і т.п.) та передбачити в них попередню перевірку значень, що вводяться. Тож ми усунули можливість появи помилок при програмуванні пов'язаних з неправильним використанням полів та методів класу. Іншим програмістам буде значно легше використовувати клас Car, їм не потрібно вникати в особливості реалізації даного класу.

Необхідно зазначити, що методи, які читають значення полів прийнято називати з використанням префіксу get з наступним вказанням назви змінної, а для тих які модифікують значення використовують префікс set. В англомовній термінології можна зустріти терміни getter та setter методи або метод accesor та метод mutator.

Зверніть увагу як здійснюється звернення до змінної count. Насправді до методу можна звернутися з використанням об'єкту car[i].getCount(), але оскільки даний метод та змінна є статичними, тобто вона та метод спільно використовуються усіма об'єктами, то більш логічним є зверненням до неї через назву клас Car.getCount(). Даний метод можна викликати до створення будь-яких об'єктів. В такому разі ми просто отримаємо 0. Якщо б змінна не була приватною, то доступ до неї можна було б робити без застосування методу, безпосередньо: Car.count.

Успадкування[ред.]

Успадкування або ж спадкоємство(англ. inheritance) — це ще один важливий механізм об'єктно-орієнтованого програмування, який дозволяє створювати нові класи на основі вже існуючих. Клас на базі якого створюється підклас називають надкласом, суперкласом або ж батьківським класом. Новий клас, що розширює (extends) батьківський клас називають підкласом або дочірнім класом. На відміну від С++ де клас може мати кілька батьківських класів, мова програмування Java підтримує одинарне успадкування, тобто може бути лише один безпосередній надклас. У надкласу звичайно може бути свій надклас, проте також лише один безпосередній і т.д. Множинне успадкування доволі складне у застосуванні і вимагає обережного підходу, тому творці Java вирішили відмовитися від нього.

Загальні основи[ред.]

Допустимо у вас є клас SimpleRoom, який містить поля width (ширина) та length (довжина) кімнати та методи виведення інформації про кімнату.

package ua.wikibooks.oj;

public class SimpleRoom {

    protected double width=0.0;
    protected double length=0.0;

    public SimpleRoom(double width, double length) {
        this.width=width;
        this.length=length;
        System.out.println("SimpleRoom створено");
    }
    
    public void info (){
        System.out.println("Кімната: ширина = "+width+", довжина = "+length);
        System.out.println("Площа кімнати: "+width*length);
    }
    
    public static void main(String[] args) {
        SimpleRoom s=new SimpleRoom(5, 5);
        s.info();
    }
}

Результат виконання:

SimpleRoom створено
Кімната: ширина = 5.0, довжина = 5.0 
Площа кімнати: 25.0

Допустимо вам необхідний клас, який би містив ще й інформацію про висоту кімнати і видав, ще й об'єм кімнати. Можна створити повністю новий клас, можна модифікувати вже існуючий клас, а можна створити клас розширивши базовий клас SimpleRoom. Якщо клас SimpleRoom вже використовується в інших програмах, то змінювати його прийдеться обережно. Для даного прикладу прийдеться створити ще один конструктор, а не модифікувати існуючий. В складніших випадках може знадобитися набагато більше дій. Крім того може бути, що клас SimpleRoom вже стандартизований, задокументований, його використовує чимало інших розробників і змінювати його просто так ви не маєте права. Тож виходом є створення нового класу під ваші потреби. Завдяки ж можливості успадкування, нам не потрібно повністю переписувати клас. На основі класу SimpleRoom можна створити новий клас SimpleRoom2. Для цього достатньо вказати ключове слово extends (що означає "розширює") і вказати назву батьківського класу. Новий, дочірній клас отримує доступ до публічних і захищених полів та методів батьківського класу.


package ua.wikibooks.oj;

public class SimpleRoom2 extends SimpleRoom {
    protected double height;
    public SimpleRoom2(double w, double l, double h) {
        super(w, l);
        height=h;
        System.out.println("SimpleRoom2 створено");
        
    }
    
    public void info2(){
        System.out.println("Кімната: ширина = "+super.width+", довжина = "+super.length+", висота= "+this.height);
        System.out.println("Площа кімнати: "+width*length); // якщо немає конфлікту з іменами, то можна і пропустити super
        System.out.println("Об'єм кімнати: "+width*length*height);
    }

    public static void main(String[] args) {
        SimpleRoom2 s2 = new SimpleRoom2(5, 5, 3);
        System.out.println("Метод info SimpleRoom");
        s2.info();
        System.out.println();
        System.out.println("Метод info2 SimpleRoom2");
        s2.info2();
    }
}

Розберемо вищенаведений приклад. При створенні класу ми зазначили, який клас ми розширюємо. В класі введене нове поле height. Далі ми створили конструктор, в якому іде звернення до батьківського конструктора. Якщо б не було конструкторів з параметрами, то неявні виклики конструкторів викликалися б у такій же послідовності. Спочатку викликається конструктор дочірнього класу, з нього викликається батьківський конструктор, створюється батьківський об'єкт, далі іде завершення конструктора дочірнього класу і створюється дочірній об'єкт. Для виклику конструктора суперкласу ми скористалися методом super з відповідними аргументами для батьківського конструктора:

super(w, l);

Також зверніть увагу як іде звернення до полів батькіського класу. Якщо поля не приватні, то вони доступні з дочірнього класу. Тож до них можна звертатися безпосередньо по імені, або ж скористатися для доступу ключовим словом super (взамін об'єктної змінної). Якщо б у нашому дочірньому класі існували б однойменні поля з батьківським класом (наприклад, і там і там width), то поля батьківського класу були б доступні лише з допомогою super.

Результат виконання SimpleRoom2:

SimpleRoom створено
SimpleRoom2 створено
Метод info SimpleRoom
Кімната: ширина = 5.0, довжина = 5.0
Площа кімнати: 25.0

Метод info2 SimpleRoom2
Кімната: ширина = 5.0, довжина = 5.0, висота= 3.0
Площа кімнати: 25.0
Об'єм кімнати: 75.0

Зверніть увагу, що ми без проблем через об'єктну змінну класу SimpleRoom2 викликаємо метод info класу SimpleRoom:

s2.info();

Тож крім полів дочірньому класу також доступні неприватні методи батьківського класу.

Насправді, у дочірньому класі (SimpleRoom2) можна було б створити однойменний клас info і він би замістив відповідний метод батьківського класу. Даний механізм так і називається "заміщення методів" (англ. method overriding)

Заміщення методів[ред.]

Для розбору механізму заміщення, розглянемо дещо інший приклад. Допустимо ви маєте клас Room, в якому є метод calculateCapacity, що обчислює об'єм кімнати і видає значення в кубічних метрах.

package org.wikibooks.uk.osvjava.inheritance;

public class Room {
    private double width=0.0;
    private double height=0.0;
    private double length=0.0;
        
    public Room(double width, double length, double height) {
        this.width=width;
        this.length=length;
        this.height=height;
        
    }

    public double calculateCapacity(){
        return width*length*height; 
    }
    
    //тестовий метод main
    public static void main(String[] args) {
        Room r1 = new Room(6, 4, 2.7);
        System.out.println("Об'єм кімнати="+r1.calculateCapacity()+" м. куб.");
        
    }
}

Результат виконання:

Об'єм кімнати = 64.80000000000001 м. куб.

У вас нова задача, яка по якійсь причині вимагає частого переведення об'єму в кубічні дециметри. Як і попереднього разу по певним причинам, ми не можемо просто модифікувати метод Room. Тож на основі класу Room визначимо клас Room2.

Розглянемо все вищесказане. Для початку створимо просто клас, який успадкує метод calculateCapacity() від свого предка.

package org.wikibooks.uk.osvjava.inheritance;

public class Room2 extends Room {
    Room2(double width, double length, double height) {
        super(width, length, height);
    }
}

Як бачимо він поки що не надто корисний. В конструкторі через super(width, length, height), ми викликаємо конструктор нашого батьківського класу.

А ось клас, що використовує наш новий клас Room2.

package org.wikibooks.uk.osvjava.inheritance;

public class App1 {
    private Room2 r2;
    
    App1(){
        Room2 r2 = new Room2 (5.1,5.2,5.2);
        System.out.println("Об'єм кімнати = "+r2.calculateCapacity()+" м. куб");
    }

    public static void main(String[] args) {
        // створюємо об'єкт класу App1, щоб не працювати в статичному контексті 
        // який має свої особливості роботи (детальніше про static - далі)
      
        new App1(); 
    }
}

Як бачимо ми можемо викликати публічний метод calculateCapacity(), який був описаний в класі Room. Проте тепер ми використовуємо для його виклику екземпляр класу Room2.

Результат виконання App1:

Об'єм кімнати = 137.904 м. куб

Далі дещо модифікуємо Room2, так щоб наш метод calculateCapacity() повертав кубічні дециметри, а не кубічні метри.

Тож нам необхідно перевизначити метод calculateCapacity() у новому класі і таким чином буде здійснене заміщення (overriding) старого методу новим.

Ось нові дещо модифіковані класи. Для ясності забрано тестовий метод main() з класу Room.

Клас Room:

package org.wikibooks.uk.osvjava.inheritance;

public class Room {
    private double width=0.0;
    private double height=0.0;
    private double length=0.0;

    Room(double width, double length, double height) {
        this.width=width;
        this.length=length;
        this.height=height;    
    }
    
    public double calculateCapacity(){
        return width*length*height;
            
    }
}

Клас Room2:

package org.wikibooks.uk.osvjava.inheritance;

public class Room2 extends Room {
    Room2(double width, double length, double height) {
        super(width, length, height);
    }


    public double calculateCapacity() {
        return super.calculateCapacity()*1000;
    }
    
}

Класс App1:

package org.wikibooks.uk.osvjava.inheritance;

public class App1 {
    private Room2 r2;
    
    App1(){
        Room2 r2 = new Room2 (5.1,5.2,5.2);
        System.out.println("Об'єм кімнати = "+r2.calculateCapacity()+" дм. куб");
    }


    public static void main(String[] args) {
        new App1();
    }
}

Результат виконання:

Об'єм кімнати = 137904.0 дм. куб

Як бачимо тепер при виклику r2.calculateCapacity() у нас запускається новий метод класу Room2. Який в свою чергу викликає метод суперкласу і результат обчислення площі множить на 1000.

Звичайно початковий метод Room у нас дещо сирий. Так основні параметри кімнати у нас закриті згідно принципу інкапсуляції і тому наш підклас Room2 не має доступу до них. Якщо б у ньому були б методи, що повертають довжину, ширину і висоту (а такі методи мали б там бути обов'язково), то ми легко могли б могли створити у класі Room2 додаткові методи роботи з даними параметрами. Як то обчислення площі підлоги, стін і т.п. Інший спосіб це оголосити їх не приватними(private), а захищеними (protected). Підклас може звертатися до захищених полів і методів безпосередньо через ключове слово super. До приватних полів та методів суперкласу, як вже було сказано, підклас доступу немає і змушений працювати лише через відкриті методи надкласу.

Тож зробимо наші класи дещо більш кориснішими і продуманими.

Клас Room:

package org.wikibooks.uk.osvjava.inheritance;

public class Room {
    private double width=0.0;
    private double height=0.0;
    private double length=0.0;

    
    Room(double width, double length, double height) {
        this.width=width;
        this.length=length;
        this.height=height;
        
    }
    
    public double calculateCapacity(){
        return width*length*height;
            
    }

    public double getWidth() {
        return width;
    }

    public double getHeight() {
        return height;
    }

    public double getLength() {
        return length;
    }
}

Клас Room2:

package org.wikibooks.uk.osvjava.inheritance;

public class Room2 extends Room {
    Room2(double width, double length, double height) {
        super(width, length, height);
    }

    @Override
    public double calculateCapacity() {
        return super.calculateCapacity() * 1000;
    }

    public double calculateSquare() {
        return super.getWidth() * super.getLength();
    }

    //заміщуємо метод toString класу Object
    @Override
    public String toString() {
        return "Об'єм кімнати ="+calculateCapacity()+"\n"+"Площа кімнати = "+calculateSquare();
        
    }
}

Клас App1:

UML діаграма класів
package org.wikibooks.uk.osvjava.inheritance;

public class App1 {
    private Room2 r2;
    
    App1(){
        Room2 r2 = new Room2 (5.1,5.2,5.2);
//      System.out.println
//("Об'єм кімнати = "+r2.calculateCapacity()+" дм.куб");
        System.out.println(r2);
        
    }


    public static void main(String[] args) {
        new App1();
    }
}

Як бачимо з прикладів в метод Room додано методи, що отримують виміри кімнати, а в метод Room2 додано метод, що обчислює площу кімнати. Також заміщено метод toString класу Object, що є предком усіх класів Java. Даний метод автоматично викликається до об'єктів, якщо вони знаходяться всередині System.out.println().

При роботі з ієрархією класів зручно користуватися UML діаграмами. UML діаграма вищенаведеного прикладу наведено на рисунку. Звичайна стрілка на діаграмі позначає об'єктне посилання (який клас використовується), а стрілка з незафарбованим трикутником на кінці показує, який клас Room2 розширює(extends). Знак "+" перед назвами методів та полів означає, що член класу публічний(public), "-" — приватний(private), "#" — захищений(protected). Підкреслений текст позначає статичний (static) елемент (поле, клас або ж метод).

Можна створювати цілі ланцюжки успадкування. Наприклад, далі можна розширити клас Room2 створивши клас Room3 і т.д.

Фінальні класи та методи[ред.]

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

В Java фінальним оголошено клас String.

Поліморфізм[ред.]

Поліморфізм — важливий механізм в програмуванні, що дозволяє використовувати спільний інтерфейс для обробки даних різних спеціалізованих типів. Прикладом може слугувати перевантаження методів. З появою ООП концепція поліморфізму розширилась. В контексті об'єктно-орієнтованого програмування, найпоширенішим різновидом поліморфізму є здатність екземплярів підкласу грати роль об'єктів батьківського класу, завдяки чому екземпляри підкласу можна використовувати там, де використовуються екземпляри батьківського класу[2]. І як уже зрозуміло з визначення, поліморфізм тісно пов'язаний з успадкуванням.

Вступ у поліморфізм об'єктів[ред.]

Так, уявімо, у нас є клас Солдат та на його основі створено класи Генерал та Сержант. Логічно, що кожен Генерал є солдатом і кожен Сержант є солдатом, проте не кожен солдат є Генералом чи Сержантом. Тож Генерал може виконувати функції звичайного солдата, а солдат функції Генерала не зможе. Все вищесказане в ООП реалізовується через об’єктні змінні, які є поліморфними. Тому в коді ми можемо писати наступні інструкції:

Soldier s = new Soldier("Солдат");  // звичайне створення об'єкту Soldier
Soldier s2 = new General("Генерал");  // об'єктна змінна типу Soldier посилається на об'єкт типу General

Проте якщо б ми зробили б навпаки, спробували із солдата зробити генерала, то наступний рядок викликав би помилку на етапі компіляції:

General g=new Soldier("Солдат");    // !!! Помилка приведення типу (солдат не генерал)

Можна спробувати здійснити приведення до типу General, компілятор пропустить, проте під час виконання програми знову ж виникне помилка приведення типу (виняток виду: java.lang.ClassCastException: osvjava.ua.Soldier cannot be cast to osvjava.ua.General ):

General g=(General)new Soldier("Солдат");   // ПОМИЛКА ВИКОНАННЯ!!!

Проте коли ми звертаємося до Генерала як до Солдата, то і функціональні можливості Генерала звужуються. Ми можемо викликати методи класу Солдат, проте з методами класу Генерал будуть проблеми. Наприклад, в класі Soldier є метод getHealth(), а в класі General є метод getSlogan():

Soldier sg= new General("Генерал");  //змінна sg посилається на об'єкт типу General
sg.getHealth();                      //методи класу Soldier доступні
//   sg.getSlogan();                 //методи класу General недоступні

Проте, якщо б метод getSlogan був би у класі Soldier, то викликана була б версія методу getSlogan класу General, оскільки при поліморфізмі заміщення методів все ж відбувається (крім статичних методів).

Якщо нам все ж необхідно звернутися до специфічних методів класу General, які притаманні лише йому, то необхідно створити відповідну об’єктну змінну типу General та здійснити явне приведення типу:

General general=(General)sg;    // наш Генерал тепер повноцінний
general.getSlogan();            // метод класу General доступний

Якщо виникає необхідність визначити, до якого класу належить відповідний об’єкт, то можна використати метод getClass(), який може викликати будь-який об’єкт оскільки він дістається йому від прабатька усіх класів Object:

 System.out.println(sg.getClass());
// результат: class osvjava.ua.General

Власне об’єктні змінні класу Object доволі часто застосовуються з метою збереження посилань на інші класи. Це дозволяє одночасно працювати з різнотипними об’єктами. Наприклад, можна, тримати різнотипні об’єкти в одному масиві типу Object. Також використання поліморфних об’єктних змінних дозволяє створювати своєрідні універсальні класи та методи (узагальнене програмування). Таким чином непотрібне створення великої кількості перевантаження методів з різним типом параметрів. Власне практично всі внутрішні бібліотеки Java спочатку будувалися таким чином. Щоправда, використання поліморфних змінних може слугувати джерелом багатьох помилок, тому в Java починаючи з JSE 5.0 у мову введено так звані Узагальнення (Generics), які дозволяють більш краще будувати такі універсальні засоби (дивіться детальніше відповідний розділ даного вікіпідручника).

Приклади використання поліморфізму[ред.]

Вище наведено основні правила використання поліморфних об’єктних змінних. Проте, щоб зрозуміти усе вищесказане та користь від поліморфізму розглянемо повноцінний приклад. Тож розробимо певну заготовку для своєрідної гри-стратегії. Наша заготовка гри повинна оперувати трьома об'єктами Soldier, General, Sergeant та проводитиме бій між двома військовими до загибелі одного із них.

Клас Солдат[ред.]

Спочатку створимо батьківський клас Soldier. Наш солдат як і у всіх іграх матиме певний рівень здоров’я, певний рівень захисту від ударів (броню), отримуватиме удари (поранення) та наноситиме удари іншому солдату.

package osvjava.ua;
import java.util.Random;

public class Soldier {
    protected int health; // здоров'я солдата
    protected boolean alive = false; // стан (живий чи мертвий)
    protected int defense = 0; // захист від ударів
    protected static int count = 0; // лічильник створених об’єктів
    private int id = 0; // кожен солдат матиме порядковий номер (П№)
    protected String rank; // ранг солдата ("Солдат", "Генерал", "Сержант" тощо)

    /** Конструктор
     * @param rank - ранг солдата 
     */
    public Soldier(String rank) {
        
        this.rank=rank;

        id = ++count; // збільшити count на 1 та присвоїти id;
        health = 100; // встановлюємо рівень здоров'я
        alive = true; // оживляємо солдата

        //надаємо солдату рівень захисту випадковим чином (від 0 до 50)
        Random randomGen = new Random();
        defense = randomGen.nextInt(50);
        System.out.println(rank+" П№" + id + " is created: health=" + health + ", defense=" + defense);
    }

    /**
     * @return здоров'я солдата
     */
    public int getHealth() {
        return health;
    }
    
    /** Дозволяє солдату отримувати пошкодження  
     *  @param hit - сила удару
     *  Метод приватний оскільки отримання удару можливе лише через метод hit()
     */
    private void receiveHit(int hit) {
        if (isAlive() == true) {

            // обчислюємо пошкодження
            int damage = hit - defense;
            // якщо удар пробив захист солдат отримує пошкодження
            if (damage > 0) {
                health = health - damage;

            } else {
                return; // вийти з методу
            }

            //якщо солдат загинув, то вивести відповідне повідомлення
            //в іншому випадку вивести рівень його здоров'я
            if (health <= 0) {
                alive = false;
                System.out.println("[X] "+rank+" П№" + id + " отримав пошкодження " + damage + " і героїчно загинув");
            } else {
                System.out.println(rank+" П№" + id + " отримав пошкодження " + damage + ". Залишилось здоров'я " +
                                   health);
            }
        }
    }

    /** Метод для нанесення удару
     * @param targetSoldier - кого вдарити
     * @param hit - сила удару
     */
    public void hit(Soldier targetSoldier, int hit) {
        targetSoldier.receiveHit(hit);
    }

    /**
     * Перевіряємо стан солдата
     * @return живий(true), ні (false)
     */
    public boolean isAlive() {
        return alive;
    }

    /** 
     * Повертає порядковий номер солдата
     * @return id
     */
    public int getId() {
        return id;
    }

    /** 
     * Заміщення методу toString класу Object
     * @return опис солдата
     */
    @Override
    public String toString() {
        return rank+" П№" + id + ": здоров'я=" + health + ", захист=" + defense;
    }
}

Як бачимо в програмі визначено конструктор Soldier(String rank), який проводить початкову ініціалізацію об’єкту, встановлює рівень здоров’я, оживляє, випадковим чином надає захист та присвоює порядковий номер.

Для того, щоб об’єкт когось вдарив у нас є публічний метод hit(Soldier targetSoldier, int hit), при використанні якого потрібно вказати кого вдарити і з якою силою. Даний метод отримавши аргументи викликає метод reciveHit(int hit) того солдата(інший екземпляр класу Soldier), якому наноситься удар вказаної сили. Метод reciveHit(int hit) визначає, яке пошкодження нанесено. Якщо сила удару більша рівня захисту, то наноситься пошкодження, яке рівне hit – defense (сила удару мінус захист). Якщо здоров’я солдата стає менше або рівне 0, то солдат вмирає.

Також у нас є додаткові інформаційні методи getHealth, getId та toString, для отримання інформації про стан нашого об’єкту (тобто солдата).

Для підрахунку кількості об’єктів використана статична змінна count, яка спільно використовується усіма екземплярами класу Soldier.

Більшість полів оголошені з модифікатором доступу protected, таким чином до них матимуть доступ лише об’єкти класу Soldier, його підкласи та об’єкти класів даного пакету. Таким чином клас Soldier певним чином інкапсульовано, проте при створенні класу з оголошенням, що він належить до даного пакету принцип інкапсуляції порушується. Ніщо не завадить напряму змінити health чи інше поле. Краще б було оголосити дані поля приватними, проте тоді б код програми ускладнився б, прийшлось би оголошувати більше методів доступу і відповідно розуміння програми ускладнилось би та й створення нащадків також ускладнюється. Ви можете самі пізніше переробити даний клас.

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

Для імітації бою у класі TestBattle визначено метод battle (Soldier s1, Soldier s2), в якому солдат 1 та солдат 2 наносять почергово один одному удари із випадковою силою. Бій проводиться до загибелі одного з них, після чого виводить інформація про переможця.

//TestBattle.java
package test.ua;

import java.util.Random;

import osvjava.ua.Soldier;

public class TestBattle {
    Soldier s1=new Soldier("Солдат");
    Soldier s2=new Soldier("Солдат");
    
    public TestBattle() {
        battle (s1, s2);
    }
    
    public void battle(Soldier s1, Soldier s2) {

        // бій допоки не виживе хтось один,
        // сила удару встановлюється випадковим чином
        Random gen = new Random();
        while ((s1.isAlive() == true) && (s2.isAlive() == true)) {
            s1.hit(s2, gen.nextInt(100));
            if (s2.isAlive()) { //якщо другий загинув, то мертві не воюють
                s2.hit(s1, gen.nextInt(100));
            }
        }

        //виводимо переможця
        if (!s1.isAlive()) {
            //   idWinner = soldiers[0].getId();
            System.out.println("***** Кінець бою. Переміг " + s2 + " *****");
        } else
            System.out.println("***** Кінець бою. Переміг  " + s1 + " *****");

    }
  
    public static void main(String[] args) {
      // створюємо об’єкт даного класу, виконання продовжиться з конструктора
       new TestBattle(); 
    }
}

Результат:

Солдат П№1 is created: health=100, defense=34
Солдат П№2 is created: health=100, defense=44
Солдат П№2 отримав пошкодження 8. Залишилось здоров'я 92
Солдат П№1 отримав пошкодження 14. Залишилось здоров'я 86
Солдат П№2 отримав пошкодження 53. Залишилось здоров'я 39
Солдат П№1 отримав пошкодження 33. Залишилось здоров'я 53
Солдат П№2 отримав пошкодження 37. Залишилось здоров'я 2
[X] Солдат П№2 отримав пошкодження 20 і героїчно загинув
***** Кінець бою. Переміг  Солдат П№1: здоров'я=53, захист=34 *****

Сержанти та Генерали[ред.]

Тепер же ускладнимо гру. У нас з’являються нові персонажі Генерали та Сержанти. Відмінність у них від звичайних солдат у кількості здоров’я, плюс генерали матимуть свій лозунг та метод його одержання.

// Sergeant.java
package osvjava.ua;

public class Sergeant extends Soldier {
    public Sergeant(String rank){
        super(rank); // спочатку створюється солдат, на основі якого створюється наш сержант
        super.health=super.health*10; // збільшуємо здоров'я сержанта у 10 раз
        System.out.println("Здоров'я сержанта збільшено в 10 раз");
    } 
}

Як бачимо клас сержанта доволі простий. При оголошенні класу ми написали, що він розширює клас Soldier. Далі визначили конструктор даного методу, який в свою чергу викликає конструктор суперкласу передаючи йому ранг. Якщо б ми б не визначили б конструктори у даних класах, а існували б лише конструктори по замовчуванню. То виклик конструкторів по замовчуванню відбувався б у тій же послідовності. При виклику конструктора Sergeant із нього б викликався конструктор Soldier і лише після створення Солдата відбувалось би створення Сержанта. Такий же механізм реалізовано в даному прикладі явно.

При створенні сержанта інструкцією new Sergeant(“Sergeant”), ми отримуємо наступне:

Сержант П№1 is greated: health=100, defense=45
Здоров'я сержанта збільшено в 10 раз 

Тепер же опишемо нашого Генерала із кількома власними методами і своїм лозунгом.

//General.java
package osvjava.ua;

public class General extends Soldier {

    private String slogan = "Ніколи не здаватись"; // Лозунг генерала

    /** Конструктор
     * @param rank - ранг солдата ("Генерал")
     */
    public General(String rank) {
        super(rank); // спочатку створюється солдат, на основі якого створюється наш генерал
        super.health = super.health * 100; // збільшуємо здоров'я генерала у 100 раз
        System.out.println("Здоров'я генерала збільшено у 100 раз");
    }

    /** Тепер заміщується метод toString класу Soldier
     * @return  Стан генерала із лозунгом
     */
    @Override
    public String toString() {
        return "Генерал із здоров'ям " + super.health + " його лозунг: " + slogan;
    }
    
    /**Отримати лозунг генерала
     * @return лозунг
     */
    public String getSlogan() {
        return slogan;
    }
}

Давайте заставимо нашого генерала битися із сержантом. Оскільки вони розширяють клас Soldier, то нам непотрібно створювати новий метод battle() з параметрами типу General та Sergeant. Створений попередньо метод цілком підходить

Sergeant ser=new Sergeant ("Сержант");
General gen=new General ("Генерал");
battle (ser, gen):

Результат виконання:

Сержант П№1 is greated: health=100, defense=12
Здоров'я сержанта збільшено в 10 раз
Генерал П№2 is greated: health=100, defense=29
Здоров'я генерала збільшено у 100 раз
Сержант П№1 отримав пошкодження 1. Залишилось здоров'я 999
Генерал П№2 отримав пошкодження 4. Залишилось здоров'я 9996
...
Сержант П№1 отримав пошкодження 12. Залишилось здоров'я 4
Генерал П№2 отримав пошкодження 20. Залишилось здоров'я 5173
Генерал П№2 отримав пошкодження 6. Залишилось здоров'я 5167
[X] Сержант П№1 отримав пошкодження 8 і героїчно загинув
***** Кінець бою. Переміг Генерал із здоров'ям 5167 його лозунг: Ніколи не здаватись ***** 

Як і можна було сподіватись, сили були занадто нерівні:). Генерал переміг

Таким чином завдяки тому, що наш метод battle використовує об’єктні змінні типу Soldier він без проблем приймає змінні типу Sergiant та General. Звичайно, що при цьому дані дочірні класи повинні битися через інтерфейс описаний у батьківському класі. Якщо вони реалізовуватимуть бій власним чином, то метод battle можливо не працюватиме правильно.

Крім того згадаймо метод hit(Soldier targetSoldier, int hit) класу Soldier. Там використовується посилання на об’єкт класу Soldier проте після успадкування даного методу він продовжує правильно працювати і з об’єктами Sergeant та General. Це також можливо завдяки поліморфізму.

Створюємо Армію[ред.]

Також ми можемо зібрати усіх переможців в одну армію і підрахувати загальне здоров’я даної армії[3].

Ось клас Army, доволі простий, лише додає солдат до армії і в ньому існує метод, який підраховує здоров’я армії:

package osvjava.ua;

public class Army {
    protected int num=99;
    protected Soldier[] soldiers;
    static int count=0;

    public Army() {
        soldiers=new Soldier[num];
    }

    /** Метод зарахування солдата в армію
     * @param soldier
     * @return true - солдата додано, false - помилка
     */
    public boolean addSoldier(Soldier soldier) {
        if (count>=num) return false;
        this.soldiers[count]=soldier;
        count++;

        return true;
    }

    /** Підрахунок здоров'я армії
     * @return Сумарне здоров'я усіх солдат в армії
     */
    public int calcArmyHealth() {
        int armyHealth=0;
        for (int i = 0; i < count; i++) {
           armyHealth+=soldiers[i].getHealth();
        }
               
        return armyHealth;
    }
}

А ось клас для тестування:

package test.ua;
import java.util.Random;

import osvjava.ua.Army;
import osvjava.ua.General;
import osvjava.ua.Sergeant;
import osvjava.ua.Soldier;


public class TestBattle2 {
    Sergeant ser=new Sergeant ("Сержант");
    General gen=new General ("Генерал");
    Soldier[] s= new Soldier[100];
    Army army=new Army();
    
    public TestBattle2() { 
        
        s[0] = new Soldier("Солдат");
        s[1] = new Soldier("Солдат");
        s[2] = new Soldier("Солдат");
        s[3] = new Soldier("Солдат");
    
        army.addSoldier(battle (ser, gen));
        army.addSoldier(battle (s[0], s[1]));
        army.addSoldier(battle (s[2], s[3]));
        
        System.out.println("Сумарне здоров'я армії "+ army.calcArmyHealth());

    }
    
    public Soldier battle(Soldier s1, Soldier s2) {

        // бій допоки не вижеве хтось один,
        // сила удару встановлюється випадковим чином
        Random gen = new Random();
        while ((s1.isAlive() == true) && (s2.isAlive() == true)) {
            s1.hit(s2, gen.nextInt(100));
            if (s2.isAlive()) { //якщо другий загинув, то мертві не воюють
                s2.hit(s1, gen.nextInt(100));
            }
        }

        //виводимо переможця
        if (!s1.isAlive()) {
            //   idWinner = soldiers[0].getId();
            System.out.println("***** Кінець бою. Переміг " + s2 + " *****");
            return s2;
        } else{
            System.out.println("***** Кінець бою. Переміг  " + s1 + " *****");
            return s1;
        }

    }
    
    public static void main(String[] args) {
       new TestBattle2();
    }
}

Як бачимо у класі Army для того, щоб просумувати здоров’я усіх солдат, нам не знадобилося три масиви типу Soldier, Sergeant та General, замість цього ми використали один масив Soldier, у який поміщаємо як рядових солдат так і генералів та сержантів. Саме це і є пряме використання поліморфізму в ООП — грубо кажучи: "надклас підмінює свої підкласи".

Метод army.calcArmyHealth() підбиває усе здоров’я армії простим перебором елементів масиву. Зверніть увагу як модифіковано метод battle у класі TestBattle2, тепер він повертає посилання на переможця: тип повернення Soldier.

Як вже зазначалося у вступі до поліморфізму, якщо ми хочемо викликати методи дочірніх класів, то прийдеться здійснити явне приведення до типу дочірнього класу, інакше методи General будуть недоступні ззовні, хоча всередині себе об’єкт без проблем застосовує власні методи та поля. Також, якщо в дочірньому класі перевизначено метод батьківського класу(в даному випадку метод toString), то навіть при використанні об’єктного посилання батьківського класу, метод усе рівно буде заміщено (override) (крім статичних методів). Нестатичні методи можна перевизначити у дочірньому класі, проте при поліморфізмі вони не будуть заміщатися. При виклику статичного метода об'єкту типу General, через об’єктну змінну типу Soldier буде викликаний метод Soldier, а не визначений в General одноіменний статичний метод. Все це потрібно враховувати при створенні власних програм. Тому виділіть час і поекспериментуйте з механізмами успадкування і поліморфізму, хоча б на даних прикладах.

Структура створених вищенаведених класів зображено на UML діаграмі.

UML діаграма класів

Власне, щоб не повторювати постійно метод battle при тестуванні, його б було доцільно розмістити у відповідному класі Battle. Можете зробити це самостійно. Також було б непогано реалізувати бій між арміями, а не тільки між двома солдатами.


Примітки[ред.]


Методи · Абстрактні класи