Освоюємо 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. Можете зробити це самостійно. Також було б непогано реалізувати бій між арміями, а не тільки між двома солдатами.

Абстрактні класи[ред.]

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

При описі абстрактного класу використовується модифікатор abstract. Нереалізовані методи також позначаються як абстрактні методи з використанням модифікатора abstract.

Потомок класу повинен реалізувати усі методи класу, або ж також бути оголошеним абстрактним.

Екземпляр абстрактного класу не можливо створити. Тобто ми не можемо створити об'єкт з використанням ключового слова new.

Опис абстрактного класу[ред.]

Для того, щоб позначити клас абстрактним, модифікатор abstract вписується в рядку-заголовку класу. Аналогічно даний модифікатор пишеться при оголошенні абстрактного методу.

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

На основі вищесказаного визначаємо наш базовий абстрактний клас із одним реалізованим методом і двома пустими (без тіла):

package ua.osvjava.classes;
/**
 * Абстрактний клас 
 * Список методів до реалізації
 */
public abstract class CarCost {
    
    /**
     * обчислення собівартості
     * @return - Собівартість автомобіля на заводі
     */  
     public double countPrimeCost() {
         //обчислення собівартості
         return 50000.0;
     }
    
    /**
     * Обчислення вартості перевезення
     * @param Country - країна 
     * @return - вартість перевезення
     */
    public abstract double countTransportationCosts(String Country); 

    /**
     * Обчислення вартості автомобіля в салонах продажів
     * @return - остаточна ціна автомобіля у певній країні
     */
    public abstract double countLocaleCost();                                       
}

В реальному прикладі, програміст змушений би був отримувати вихідні дані із бази даних і опрацьовувати їх. Ми не будемо ускладнювати і вдаватися у деталі. Наш метод countPrimeCost повертає 50000.0. Приймемо умовно, що це і є результат обрахунку собівартості. Перед заголовком методів описано призначення методу у вигляді javadoc коментаря. Якщо ви використовуєте інтегроване середовище розробки, то при зверненні до методу через оператор "." (крапка), буде спливати підказка із наведеними відомостями про метод та його параметри.

Розширення абстрактних класів[ред.]

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

Тож, обчислюємо транспортні витрати:

package ua.osvjava.classes;

public abstract class CarTransportationCosts extends CarCost {

    @Override
    public double countTransportationCosts(String country) {
        
        
        if (country.compareTo("Ukraine") == 0){
            //обчислення вартості транспортування в Україну
            return 3.000;
        }
        
        if (country.compareTo("England") == 0) {
            //обчислення вартості доставки в Англію
            return 2.000;
        }
        return -1.0;
    }

}

Тож і у вищенаведеному прикладі, клас CarTransportationCosts не реалізовує усі методи, тож він оголошений як abstract.

Нереалізованим залишився метод countLocaleCost. Він буде мати кілька реалізацій в залежності від країни. Реалізацію кожного класу можна доручити різним програмістам. Тож тепер необхідно розширити клас CarTransportationCosts, таким чином успадкуються як метод класу CarCost так і метод визначеного щойно класу CarTransportationCosts. Головне, щоб метод в кожній реалізації повертав те, що потрібно. Реалізація логіки всередині методу залежить від конкретних законів країни та програміста, що реалізовує метод.

package ua.osvjava.classes;
/**
 * Клас для обчислення остаточної ціни автомобіля в Англії
 * 
 * @author someEnglishMan
 */
public class EnglandPrice extends CarTransportationCosts {

public double countLocaleCost(){
    //складні обчислення із врахуванням податків, митних зборів та інших витрат
    double enLocaleCosts=15800.00;
    double price=super.countPrimeCost()+super.countTransportationCosts("Ukraine")+enLocaleCosts;
            
    return price;
    }
}

Інший клас для України:

package ua.osvjava.classes;
/**
 * Клас для обчислення остаточної ціни автомобіля в Україні
 * 
 * @author volodimirg 
 */
public  class UkrainePrice extends CarTransportationCosts {
    public double countLocaleCost(){
        //складні обчислення із врахуванням податків, митних зборів та інших витрат
        double someLocaleCosts=14000.00;
        double price=this.countPrimeCost()+this.countTransportationCosts("Ukraine")+someLocaleCosts;
        
        return price;
    }
}

Вищенаведені класи реалізували останній метод, що залишився не реалізований, тож оголошувати їх абстрактними немає потреби. Тепер, можна створити екземпляри класів UkrainePrice та EnglandPrice і обчислити ціни автомобілів в Україні та у Англії.

Використання реалізацій абстрактного класу[ред.]

package ua.osvjava.classes;

public class MainClass {
    UkrainePrice ukPrice = null;
    EnglandPrice enPrice = null;

    MainClass() {
        // ПОМИЛКИ! Екземпляри абстрактних класів неможливо створити
        // CarCost cs=new CarCost();
        // CarTransportationCosts pt=new CarTransportationCosts();

        // тож працюємо з неабстрактними класами, в яких усі методи
        // абстрактного класу CarCost реалізовані

        ukPrice = new UkrainePrice();
        enPrice = new EnglandPrice();
        System.out.println("Ціна автомобіля в Україні складає " 
                            + ukPrice.countLocaleCost());
        System.out.println("Ціна автомобіля в Англії складає " 
                             + enPrice.countLocaleCost());

    }

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

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

Ціна автомобіля в Україні складає 64003.0
Ціна автомобіля в Англії складає 65803.0
UML діаграма класів


Структуру нашого прикладення наведено на UML діаграмі. Абстрактні класи та методи в UML позначаються курсивом. Як бачимо тепер ми використовуємо кілька реалізацій нашого абстрактного класу. Їх може бути і десять, і більше. В залежності від країни ми створюємо екземпляр того класу, який нам потрібний. Крім того завдяки тому, що у всіх реалізаціях класу є одні і ті ж методи, будь-якому програмісту знайомому хоча б з однією реалізацією абстрактного класу, не складатиме труднощів використовувати інші класи даної ієрархії.

Звичайно, що проміжний клас CarTransportationCosts можна було б і не створювати, а зразу ж реалізувати метод в базовому класі CarCost або ж у UkrainePrice та EnglandPrice. Можна було б також здійснити успадкування класу UkrainePrice, безпосередньо від CarCost та реалізувати в UkrainPrice всі необхідні методи без посередництва класу CarTransportationCosts, а клас EnglandPrice реалізувати так як тут реалізовано в прикладі. Все залежить від того, який шлях реалізації абстрактних класів вибрали ті чи інші розробники.

В java існує чимало ієрархій класів, в основі яких лежать саме абстрактні класи.

Об'єктні змінні абстрактних класів[ред.]

Хоча не можливо створити екземляр абстрактного класу, проте об'єктну змінну можна оголосити і використовувати її для посилання на екземпляри класів нащадків.

У нашому прикладі, можна викликати методи таким чином:

    CarCost cc=new UkrainePrice();
    System.out.println("Ціна автомобіля в Україні складає " + cc.countLocaleCost());

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

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

package ua.osvjava.classes;

import java.io.Console;
import java.util.Scanner;

public class MainClass2 {
    
    Scanner in;
    CarCost cc;
    
    public MainClass2() {
        
        in = new Scanner(System.in);
        System.out.print("Введіть назву країни: ");
        String str=in.nextLine(); // читання рядка тексту з клавіатури
        
        //В залежності від країни створюється відповідний об'єкт,
        //якщо неправильна назва, вивести повідомлення про це і завершити виконання методу
        if (str.equalsIgnoreCase("Ukraine")||str.equalsIgnoreCase("Україна")){
            cc=new UkrainePrice();
        }
        else if (str.equalsIgnoreCase("England")||str.equalsIgnoreCase("Англія")){
            cc=new EnglandPrice();
        }else {
            System.out.println("Інформація про введену країну відсутня!");
            return; 
        }
        System.out.println("Ціна автомобіля ("+str+"): " + cc.countLocaleCost());
    }

    public static void main(String[] args) {
        MainClass2 class1 = new MainClass2();
    } 
}

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

Введіть назву країни: Україна
Ціна автомобіля (Україна): 64003.0

Ви можете допрацювати програму, наприклад, щоб вона надавала інформацію про ціну зазначеної користувачем марки авто у зазначеній користувачем країні.

Інтерфейси[ред.]

Інтерфейси (interfaces) в мові програмування Java – це посилальний тип даних (reference data type), що подібний до класу, проте може містити лише константи, сигнатуру методу та вкладені типи. По суті інтерфейси подібні до абстрактних класів, проте якщо останні можуть містити крім сигнатури методів, ще й їхню реалізацію, то інтерфейси можуть містити лише опис даних методів.

Визначення та реалізація інтерфейсу[ред.]

Інтерфейс визначається наступним чином:

public interface MyInterface <T> {
    public String MYCONST="It is constant"; // константа
    public int method(T o); // нереалізований метод
}

<T> - це частина механізму Generics(Узагальнень), що дозволяє ввести додаткову перевірку типу при роботі, в даному випадку вказується, що інтерфейс буде працювати з будь-яким типом. При створенні об'єктної змінної можна уточнити тип (наприклад, MyInterface<String> myintvar). Generics були введено у випуск Java 5. Таким чином усуваються деякі можливі помилки, пов'язані із типами даних і зокрема з приведенням типів(детальніші у відповідному розділі). Інтерфейси можна створювати і старим способом, без використання узагальнень:

public interface MyInterface {
    public String MYCONST="It is constant" // константа
    public int method1(Object o); // нереалізований метод1
    public int method2(String str); // нереалізований метод2
}

Поля у інтерфейсі є константами, по суті в звичайному класі вони б оголошувались би як

public static final тип НАЗВА_КОНСТАНТИ;

В інтерфейсі дані модифікатори можна не вказувати. Згідно офіційних рекомендацій Oracle не рекомендується писати зайві модифікатори до членів інтерфейсу і згідно тих же рекомендацій константи в Java пишуться з великої літери.

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

Деякі автори книг не рекомендують використовувати інтерфейси для збереження констант.

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

public class Car extends SimpleCar implements Moveable, Comparable{
    ........
}

Переваги інтерфейсів[ред.]

Перевагою інтерфейсів над абстрактними класами є те, що можна реалізувати кілька інтерфейсів в одному класі. Розглядаючи абстрактні класи, було сказано, що вони зручні при груповій роботі, коли частину методів робить один програміст, а іншу частину інший. Крім того можна ієрархію класів де різні реалізації одного абстрактного класу можуть виконувати схожі, проте в дечому відмінні задачі. В інтерфейсах дана ідея ще більше розширюється. Так, якщо один програміст розробляє клас(назвемо умовно FirstClass), який використовує інший клас(UseableClass), що повинен розробити інший програміст, то першому не потрібно чекати, коли другий програміст зробить необхідний йому UseableClass. Вони можуть узгодити, що клас повинен мати певні методи, описати інтерфейс для даного класу (наприклад, interface Useable) і далі працювати кожен над своїм класом паралельно. Один пише клас UseableClass реалізовуючи(implements) логіку методів інтерфейсу Useable. Iнший пише FirstClass так нібито клас UseableClass вже реалізовано. Єдине, що їм потрібне – це узгоджений між ними інтерфейс, і, зокрема, сигнатура майбутніх методів. Як і у випадку з абстрактними класами в пригоді стає поліморфізм: змінна інтерфейсного типу може певним чином замінити ще не реалізований клас.

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

В Java існує ряд широковживаних інтерфейсів. Щоб об’єкти можна було порівняти між собою, необхідно реалізувати інтерфейс Comparable і його метод compareTo(). По суті призначенням інтерфейсів є змусити програміста, що розробляє певний клас, реалізувати методи, щоб об’єкт правильно функціонував.

Реалізовуємо електронний словник[ред.]

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

Розглянемо інтерфейси більш детальніше на прикладі. Для цього розглянемо чорнову реалізацію наступної задачі: двом програмістам необхідно реалізувати мініатюрний словник. Один реалізовуватиме клас WordTranslation, який працюватиме з користувачем, інший реалізовуватиме клас Dictionary, що безпосередньо працюватиме із словами і реалізовуватиме інтерфейс Dict. Після розробки обох класів буде створено третій клас mainClass, який зв’яже реалізацію інтерфейсу із класом, що її використовуватиме. Узагальнена UML діаграма, того, що потрібно зробити зображено на рисунку.

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

Інтерфейс Dict[ред.]

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

package ua.osvjava;

public interface Dict {
    /**
     *Пошук перекладу
     * @param word слово для якого необхідно знайти переклад
     * @return  переклад слова, або ж рядок "переклад не знайдено"
     */
    String findTranslation(String word);
    
    /**
     *Додавання слова в словник
     * @param word  слово
     * @param translalion  переклад слова
     * @return  true, якщо слово додане успішно
     */
    boolean addWord(String word, String translalion);
    
    /**
     *Видаляє із словника слово разом із його перекладом
     * @param word  слово для видалення
     * @return  true, якщо слово успішно видалене
     */
    boolean delWord(String word);
}

Реалізація інтерфейсу Dict[ред.]

Інтерфейс реалізовуватиме клас Dictionary. Для спрощення розуміння прикладу, реалізовано лише метод findTranslation. Усі інші методи нічого не роблять:

package ua.osvjava;

public class Dictionary implements Dict {
    private String[] ukWord={"слово", "читати", "сонце", "робити", "йти"};
    private String[] enWord={"word", "read", "sun", "do", "go"};  
    public Dictionary() {
        
    }

    @Override
    public String findTranslation(String str) {
        for (int i = 0; i < ukWord.length; i++) {
            if (ukWord[i].compareToIgnoreCase(str)==0) return enWord[i];
            if (enWord[i].compareToIgnoreCase(str)==0) return ukWord[i];
        }

        return ">>ПЕРЕКЛАД НЕ ЗНАЙДЕНО!!!<<";
    }

    @Override
    public boolean addWord(String string, String string2) {
        // ....
        // деякі дії по додаванню слова
        return false;
    }

    @Override
    public boolean delWord(String string) {
        // деякі дії по видаленню слова
        //....
        return false;
    }
}

Як бачимо в класі присутні два масиви, які представляють собою зв’язку слово-переклад. Метод findTranslation при зверненні приймає слово і послідовно перебирає два масиви із словами в пошуку співпадіння. Якщо слово знайдено в масиві з українськими словами, то повертається англійський відповідник і навпаки. Якщо слово не знайдено, то повертається рядок тексту ">>ПЕРЕКЛАД НЕ ЗНАЙДЕНО!!!<<";

Насправді в Java існує колекція класів, які призначені для зберігання об’єктів різної природи у вигляді масивів, в тому числі і у вигляді пар ключ-значення (пакет java.util). В цих класах вже реалізовано ряд корисних методів для роботи з цими наборами об’єктів. Зокрема, переважно вони містить ряд методів для пошуку по ключовому слову, видалення об'єкту, вставки об'єктів, визначення розміру. Котрийсь із цих класів можна було б використати для нашої задачі, проте нашим основним завданням є розібратися з масивами.

Ви можете, згодом, спробувати самостійно реалізувати своєрідний електронний словник з використанням класу Dictionary, або іншого з колекції класів, або ж самому реалізувати повноцінний клас для збереження слів та роботи з ними.

Клас WordTranslation[ред.]

Що ж повертаємось до наших баранів, тобто основної задачі. Реалізуємо клас WordTranslation, який буде використовувати клас Dictionary і реалізує спілкування з користувачем через текстову консоль.

Таким чином маємо наступну реалізацію:

package ua.osvjava;

import java.util.Scanner;

public class WordTranslator {
    private Dict dict;

    public WordTranslator(Dict dict) {
        dict = dict;

        Scanner in = new Scanner(System.in);
        String word;
        do {
            System.out.print("Введіть слово для перекладу (ext - вихід): ");
            word = in.next();
            System.out.println(" - "+dict.findTranslation(word));
        } while (!word.equalsIgnoreCase("ext"));
    }

    public String getTranslation(String word) {
        return null;
    }
}

Як бачимо клас доволі простий, через конструктор отримується посилання на об’єкт, клас якого реалізовує інтерфейс Dict. Для збереження даного посилання використовується інтерфейсна змінна типу Dict. Після чого запускається цикл опитування користувача через текстову консоль. Користувач вводить слово, об’єкт класу WordTranslator викликає відповідний метод нашого словника для пошуку слова. Якщо б нам не була необхідна певна універсальність, то ми могли б це записати наступними інструкціями:

String word=do
Dict dict=new Dictionary();
dict.findTranslation(word)

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

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

Клас MainClass[ред.]

Тепер же у нас є усі доступні класи, щоб їх застосовувати. Лишилось написати клас із методом main, який пов’яже усе докупи і запустить наше прикладення.

package ua.osvjava;

public class MainClass {
    WordTranslator transl;
    Dictionary dictEnUk;

    MainClass() {
        //створюємо словник
        dictEnUk = new Dictionary();
        //запускаємо перекладач слів з текстовим інтерфейсом
        transl = new WordTranslator(dictEnUk);

    }

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

Як бачимо клас простий. Створюється об’єкт типу Dictionary, а далі об’єкту типу WordTranslator передається об’єктне посилання на створений словник.

Результат роботи:

Введіть слово для перекладу (ext - вихід): read
 - читати
Введіть слово для перекладу (ext - вихід): робити
 - do
Введіть слово для перекладу (ext - вихід): translator
 - >>ПЕРЕКЛАД НЕ ЗНАЙДЕНО!!!<<
Введіть слово для перекладу (ext - вихід): ext
 - >>ПЕРЕКЛАД НЕ ЗНАЙДЕНО!!!<<

Остаточну структуру класів та їхню взаємодію зображено на UML діаграмі.

UML діаграма класів електронного словника

Як видно з діаграми, класи WordTranslator та Dictionary між собою не пов’язані напряму, а отримують зв’язок через інтерфейс Dict. Можна скільки завгодно реалізувати версій класів на основі Dict. При цьому клас WordTranslator це не зачепить. Все це можна було б реалізувати через абстрактний клас, проте при використанні інтерфейсів клас Dictionary може запросто бути включеним в певну ієрархію класів, тобто розширювати (extends) інший клас і одночасно реалізовувати наш інтерфейс.

Інтерфейс Comparable[ред.]

Насправді, в java набагато частіше доводиться використовувати вже наявні в ній інтерфейси ніж створювати власні. Розглянемо приклад із застосуванням java інтерфейсу Comparable. Даний інтерфейс дозволяє порівняти два об’єкта і передбачає реалізацію методу compareTo. Даний метод необхідно реалізувати в об'єктах, які повинні між собою порівнюватись.

Інтерфейс Comparable в пакеті java.lang визначений наступним чином:

package java.lang;
import java.util.*;

public interface Comparable<T> {
    public int compareTo(T o);
}

Так, знайомий вже нам, клас String реалізовує інтерфейс Comparable. Таким чином в ньому присутній метод compareTo, який широко застосовують різноманітні класи, що працюють із рядками.

Порівняти чисельні змінні взагалі немає проблем. Так що такого в цьому інтерфейсі? Суть в тому, що інколи об’єкти необхідно порівняти по певним полям. Наприклад, відсортувати об’єкт Person чи Manager по імені або ж по прізвищу особи/менеджера, або ж по зарплаті і т.п. Тож, щоб це було можливо необхідно подбати про метод compareTo у класі об’єкти якого будуть порівнюватись. По замовчуванню метод compareTo порівнює об’єкти по хеш-коду об’єкта, який залежить від об’єктного посилання, а не від полів об’єкта.

Навіщо саме compareTo і навіщо реалізовувати інтерфейс Comparable? Чому не реалізувати метод з іменем comparePerson. Це звичайно ж працюватиме, проте в результаті втрачаємо ряд функціональних можливостей реалізованих у великій кількості класів Java, які можуть полегшити роботу над вашим власним прикладенням. Тобто існують уже наперед розроблені своєрідні "універсальні" класи, що орієнтовані на роботу з об’єктами типу Comparable. Правильніше казати, що їхні методи створені таким чином, щоб викликати метод compareTo. І в ряді випадків вони можуть бути доволі корисні. Наприклад, існує клас java.util.Arrays, в якому реалізовано метод sort, що дозволяє відсортувати масив будь-якого об’єктного типу. Тобто нам непотрібно реалізовувати вже власне сортування, достатньо на його вхід подати наш об'єктний масив.

Наприклад так:

        Person[] persons= new Person[3];

        persons[0]=new Person("Назаренко","Іван"); 
        persons[1]=new Person("Козаченко","Петро"); 
        persons[2]=new Person("Перебийніс","Василь"); 

        Arrays.sort (persons);
        
        for(Person p:persons){
            System.out.println (p);
        }

Щоб мати змогу зробити щось подібне (відсортувати об’єкти по прізвищу і далі вивести відсортований список) необхідно, щоб клас Person реалізував інтерфейс Comparable і метод сompareTo. Метод сompareTo повертає від’ємне значення, вказаний об’єкт менший, 0 – якщо об'єкти рівні, і додатну величину, якщо об'єкт більший. Результат повернення буде врахований методом sort класу Arrays, при сортуванні об’єктів згідно реалізованого в compareTo правила сортування. Переважно сортується по зростанню чи спаданню, в залежності від того, як ви реалізуєте порівняння об’єктів.

Тож маємо наступний клас Person.

package ua.osvjava.comp;

public class Person implements Comparable<Person>{

    private String firstName;
    private String surName;

    public Person(String sn, String fn) {
        this.surName = sn;
        this.firstName = fn;
    }

    public void setFirstName(String fn) {
        this.firstName = fn;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setSurName(String sn) {
        this.surName = sn;
    }

    public String getSurName() {
        return surName;
    }

    public String toString() {
        return getSurName() + " " + getFirstName();
    }

    @Override
    public int compareTo(Person o) {
        return getSurName().compareToIgnoreCase(o.getSurName());
    }
}

Клас простий, має два поля і методи доступу до даних полів. В ньому реалізовано метод compareTo, шляхом порівняння прізвищ у двох екземплярах класу Person. Один екземпляр - це поточний об'єкт, інший об'єкт Person передається по об'єктному посиланні. Таким чином поточний об'єкт порівнюється з іншими і по результату повернення, судиться чи даний об'єкт більший, менший або ж рівний іншому. В нашому випадку, в майбутньому, результат порівняння, буде врахований у класі Arrays у його алгоритмі сортування (або ж може бути використано у будь-якому іншому класі, який викликатиме метод compareTo для порівняння екземплярів класу Person. Це може бути виклик на зразок такого person1.compareTo(person2)).

В нашій реалізації методу compareTo, для порівняння прізвищ використано метод compareToIgnoreCase - це метод класу String, що порівнює два рядки символів (в даному випадку порівнюються прізвища).

Також заміщено метод toString, що дозволить нам виводити прізвище та ім’я при використанні інструкції:

System.out.println (p);

де p – об’єктна змінна, що посилається на екземпляр класу Person.


Ось клас, в якому використовується екземпляри класу Person і здійснюється вивід осіб в відсортованому порядку:

package ua.osvjava.comp;

import java.util.Arrays;


public class usingPerson {
    usingPerson() {
        Person[] persons= new Person[3];

        persons[2]=new Person("Перебийніс","Василь"); 
        persons[0]=new Person("Козаченко","Петро"); 
        persons[1]=new Person("Назаренко","Іван");

        Arrays.sort (persons);
        
        for(Person p:persons){
            System.out.println (p);
        }
    
    }
    public static void main(String[] args) {
        new usingPerson();
    }
}

Результат:

Козаченко Петро
Назаренко Іван
Перебийніс Василь 

Як бачимо реалізувавши Comparable, ми змогли посортувати об’єкти масиву persons за прізвищем. При цьому нам не прийшлось реалізовувати повноцінний алгоритм сортування. Було використано готову його реалізацію у класі Arrays. Щоб прізвища сортувалися у зворотному порядку, всього лиш достатньо змінити знак результату, що повертається:

return -getSurName().compareToIgnoreCase(o.getSurName());

Інтерфейс Comparable застосовується в стандартних класах мови Javа практично на кожному кроці.

Існують також так звані tagged interfaces (інтерфейси маркери), тіло яких повністю пусте (Cloneable, Serializable). Наприклад, щоб можна було об’єкт клонувати (зробити копію стану об’єкту), необхідно щоб клас, на основі якого створено об’єкт, вказував, що він реалізує інтерфейс Cloneable. Даний інтерфейс без методів. Якщо в класі вказано, що він реалізовує даний метод, то стає доступним використання методу clone() класу Object. В класі Object реалізована перевірка чи клас для клонування є екземпляром Cloneable.

if (obj instanceof Cloneable)....

Таке використання інтерфейсів в даний час не рекомендується. Метод clone() класу Object, проводить копіювання об’єкту неефективно і його усе рівно потрібно замістити у класі, об’єкт якого копіюється, або що проводить копіювання певного об’єкту.

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


Методи · Винятки