Лабораторна робота 3

Використання успадкування та поліморфізму в Java

1 Завдання на лабораторну роботу

1.1 Індивідуальне завдання

Створити ієрархію класів, які представляють сутності відповідно до завдання 1.5 попередньої лабораторної роботи. Перша сутність повинна бути представлена ієрархією з абстрактного та неабстрактних похідних класів.

Клас, який представляє другу сутність індивідуального завдання, повинен містити необхідні поля, конструктори, методи доступу, а також перевизначення функцій equals() і hashCode(), а також повинен реалізовувати інтерфейс Comparable для природного порівняння об'єктів під час сортування за однією з ознак.

Базовий абстрактний клас, який представляє першу з сутностей індивідуального завдання, не повинен містити даних про послідовність елементів типу другого класу. Слід визначити такі функції:

  • функції для доступу до даних;
  • абстрактні функції для доступу до послідовності елементів типу другого класу;
  • перевизначення методу equals() для перевірки еквівалентності об'єктів;
  • перевизначення методу hashCode() для отримання хеш-кодів об'єктів.

Для пошуку необхідних даних і сортування відповідно до завдання та форматованого виведення даних на консоль слід створити окремі допоміжні класи. Функції пошуку повинні відтворювати завдання попередньої лабораторної роботи.

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

№№ Перша ознака Друга ознака
1, 17 За зменшенням температури За алфавітом коментаря
2, 18 За збільшенням кількості студентів За збільшенням довжини теми
3, 19 За збільшенням кількості пасажирів За алфавітом коментаря
4, 20 За збільшенням кількості слів у темі За алфавітом теми
5, 21 За збільшенням температури За зменшенням довжини коментаря
6, 22 За збільшенням кількості учасників За алфавітом назви
7, 23 За збільшенням кількості відвідувачів За алфавітом коментаря
8, 24 За зменшенням кількості пасажирів За зменшенням довжини коментаря
9, 25 За датою у зворотному порядку За збільшенням кількості відвідувачів
10, 26 За збільшенням кількості концертів За алфавітом міста
11, 27 За номером зміни За збільшенням кількості комп'ютерів
12, 28 За збільшенням кількості відвідувачів За алфавітом коментаря
13, 29 За зменшенням кількості пасажирів За алфавітом назви
14, 30 За зменшенням кількості покупців За алфавітом коментаря
15, 31 За датою у зворотному порядку За збільшенням кількості глядачів
16, 32 За зменшенням кількості хвилин розмов За збільшенням кількості коштів, що використано на розмови

Для сортування застосувати стандартну функцію Arrays.sort(). Одне з сортувань повинне бути забезпечене реалізацією інтерфейсу Comparable для сутності, об'єкти якої зберігаються в масиві. Друге сортування забезпечується створенням окремого класу, який реалізує інтерфейс Comparator. Рекомендовано скористатися лямбда-виразом.

Від створеного абстрактного класу необхідно створити два похідних класи:

  • з представленням послідовності об'єктів другої сутності за допомогою масиву;
  • з представленням послідовності об'єктів другої сутності за допомогою однобічно зв'язаного списку, створеного вручну (див. приклад 3.5).

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

Слід додати коментарі Javadoc до сирцевого коду.

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

1.2 Ієрархія класів

Реалізувати класи "Людина", "Громадянин", "Студент", "Співробітник". Створити масив посилань на різні об'єкти ієрархії. Для кожного об'єкта вивести на екран рядок даних про нього.

Примітка: слід створити класи зі змістовними іменами.

1.3 Мінімум функції

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

Реалізувати п'ять варіантів розв'язання:

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

Перевірити роботу програми на двох різних функціях.

1.4 Реалізація масиву точок через масиви дійсних чисел

Реалізувати функціональність абстрактного класу AbstractArrayOfPoints, наведеного в прикладі 3.2, у два способи:

  • через використання двовимірного масиву дійсних чисел: кожен рядок масиву має відповідати точці;
  • через використання одновимірного масиву дійсних чисел: кожна пара чисел у масиві має відповідати точці.

Здійснити тестування створених класів.

Примітка: не слід вносити зміни у код класу AbstractArrayOfPoints (за винятком імені пакета).

1.5 Реалізація інтерфейсу Comparable

Створити клас Circle, який реалізує інтерфейс Comparable. Більшим вважається коло з більшим радіусом. Здійснити сортування масиву об'єктів типу Circle за допомогою функції Arrays.sort().

1.6 Реалізація інтерфейсу Comparator

Створити клас Triangle. Трикутник визначати довжинами сторін. Площа трикутника в цьому випадку може бути обчислена за формулою Герона:

Heron

де a, b і c – довжини сторін трикутника. За допомогою функції Arrays.sort()здійснити сортування масиву трикутників за зменшенням площі. Для визначення ознаки сортування використовувати об'єкт, який реалізує інтерфейс Comparator.

1.7 Обчислення визначеного інтеграла (додаткове завдання)

Створити інтерфейс Integrable, який містить опис абстрактної функції, що приймає аргумент типу double і повертає результат того ж типу. Інтерфейс повинен містити метод integral() з усталеною реалізацією (з модифікатором default) обчислення визначеного інтеграла. Метод повинен отримувати як параметри початок, кінець інтервалу і точність обчислень. Усталена реалізація обчислення інтеграла використовує метод прямокутників.

Створити клас, який перевизначає функцію integral(), реалізуючи метод трапецій обчислення визначеного інтеграла.

Обчислити визначений інтеграл за допомогою обох алгоритмів для різних математичних функцій класу java.lang.Math (див. приклад 3.3). Порівняти результати для різних алгоритмів і різних значень точності обчислення.

2 Методичні вказівки

2.1 Успадкування

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

На відміну від C++, у Java дозволяється тільки одиничне успадкування класів – клас може мати тільки один базовий клас. Успадкування завжди відкрите. У Java також немає захищеного і закритого успадкування. Припустимо, є базовий клас:

class Shape {
    private double x, y;  // координати центру фігури

    public double getX() {
        return x;
    }

    public void setX(double x) {
        this.x = x;
    }

    public double getY() {
        return y;
    }

    public void setY(double y) {
        this.y = y;
    }

    public double distance() {// відстань від початку координат
        return Math.sqrt(x * x + y * y);
    }
}

Успадкування має такий синтаксис:

class DerivedClass extends BaseClass {
    // тіло класу
}

У нашому випадку можна створити похідний клас:

class Circle extends Shape {
    private double radius;

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        this.radius = radius;
    }
}

Функції похідного класу мають доступ до елементів, описаних у як public і protected (захищені). Члени класу, оголошені як захищені, можуть використовуватися класами-нащадками, а також у межах пакета. Закриті (приватні, private) члени класу недоступні навіть для його нащадків.

Усі класи Java безпосередньо чи опосереднено походять від класу java.lang.Object. Цей клас надає набір корисних методів, таких як toString() для отримання даних будь-якого об'єкта у вигляді рядка, equals() для перевірки еквівалентності об'єктів тощо. Базовий клас Object не вказують явно.

Клас успадковує всі елементи базового класу, крім конструкторів. До початку виконання конструктора похідного класу автоматично викликається усталений конструктор базового класу. Не слід явно здійснювати ініціалізацію полів базового класу з конструктора похідного класу, навіть, якщо ці поля доступні (public або protected).

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

  • для виклику перекритого методу базового класу;
  • для передачі параметрів конструктору базового класу.

Якщо в попередньому прикладі до класу Shape додати єдиний конструктор з параметрами,

class Shape {
    private double x, y;  // координати центру фігури

    public Shape(double x, double y) {
        this.x = x;
        this.y = y;
    }

    //...
}

код класу Circle не зможе бути скомпільований. Цей конструктор треба явно викликати з конструкторів похідного класу за допомогою super. Крім того, можна додати конструктор, параметри якого необхідні для ініціалізації базової частини об'єкта і надсилаються безпосередньо в конструктор базового класу:

class Circle extends Shape {

    private double radius;

    public Circle(double radius) {
        super(0, 0); // виклик конструктора базового класу
        this.radius = radius;
    }

    public Circle(double x, double y, double radius) {
        super(x, y); // виклик конструктора базового класу
        this.radius = radius;
    }

    public Circle() {
        this(10); // виклик іншого конструктора поточного класу
    }

    // ...
}

Примітка: в тілі конструктора не можна одночасно використовувати виклики super() і this().

Доступ до базового класу з використанням super дозволений тільки в конструкторах і нестатичних методах.

Класи можуть бути визначені з модифікатором final (фінальний). Фінальні класи не можуть використовуватися як базові. Наприклад, можна оголосити клас Circle як фінальний:

final class Circle extends Shape {
    // ...
}

class Ellipse extends Circle { // Синтаксична помилка.
                               // Не можна успадкувати від Circle
}

Методи з модифікатором final не можуть бути перевизначені. Наприклад:

class Shape {
    
    // ...

    public final double distance() {
        return Math.sqrt(x * x + y * y);
    }
}

class Circle extends Shape {
    public double distance() { // Синтаксична помилка.
                               // distance() не можна перекрити
        // ...
    }

    // ...
}

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

Shape shape = new Circle();

Зворотне приведення необхідно робити явно:

Shape shape = new Circle();
Circle circle = (Circle) shape;

Якщо перетворення неможливе, виникає виняток ClassCastException:

Shape shape = new Shape(1, 2);
Circle circle = (Circle) shape; // виняток ClassCastException

Завдяки правилам приведення типів можна створювати масиви посилань на об'єкти різних типів однієї ієрархії успадкування. Наприклад, попередньо створену ієрархію класів Shape і Circle можна розширити класом Square (квадрат):

class Shape {
    private double x, y;

    public Shape(double x, double y) {
        this.x = x;
        this.y = y;
    }

    // ...

    public final double distance() {
        return Math.sqrt(x * x + y * y);
    }
}

class Circle extends Shape {
    private double radius;

    public Circle(double x, double y, double radius) {
        super(x, y);
        this.radius = radius;
    }

    // ...
}

class Square extends Shape { // Квадрат
    private double side;

    public Square(double x, double y, double side) {
        super(x, y);
        this.side = side;
    }

    // ...

}

Тепер у масив посилань на Shape можна записати посилання на різні фігури, а потім у циклі обчислити та вивести відстань фігур від початку координат:

Shape shapes[] = new Shape[3];
shapes[0] = new Shape(3, 4);
shapes[1] = new Circle(1, 1, 0.3);
shapes[2] = new Square(4, 3, 2);
for (int i = 0; i < shapes.length; i++) {
    System.out.println(shapes[i].distance());
}

У Java є ключове слово instanceof, яке дозволяє перевірити, чи є об'єкт екземпляром певного типу (або похідних типів). Вираз

об'єкт instanceof клас

повертає значення типу boolean, яке може бути використане для перевірки, чи можна викликати метод цього класу:

if (shape instanceof Circle) {
    ((Circle)shape).setRadius(20);
}

Записи (record) не підтримують явного успадкування.

2.2 Sealed-класи

Починаючи з версії JDK 17 до синтаксису Java додана можливість обмежувати перелік похідних класів. Це зроблено для того, щоб краще контролювати коректність створення конкретних реалізацій похідних класів. У попередніх версіях для обмеження потенційних похідних класів треба було робити базовий клас пакетним (непублічним). Але такий підхід унеможливлював не тільки успадкування, але й будь-яке використання класу поза пакетом. Крім того, іноді є необхідність дозволити успадкування для класів, розташованих в інших пакетах.

Нова можливість визначати такі обмеження передбачає використання так званих "запечатаних" (sealed) класів. Після імені sealed-класу розташовують список дозволених похідних класів:

public sealed class SealedBase permits FirstDerived, SecondDerived {
    protected int data;
}

Перелічені дозволені похідні класи повинні бути доступними компілятору. Такі класи описують з модифікаторами final або sealed. В останньому випадку створюється додаткова гілка дозволених класів:

final class FirstDerived extends SealedBase {

}

sealed class SecondDerived extends SealedBase permits SomeSubclass {

}

final class SomeSubclass extends SecondDerived {
    {
        data = 0;
    }
}

Спроба створити інші похідні класи призводить до помилки:

class AnotherSubclass extends SealedBase { // Помилка компіляції

}

Існує ще один модифікатор для дозволеного похідного класу – non-sealed. Від такого класу можна створювати будь-які похідні:

non-sealed class SecondDerived extends SealedBase {

}

class PlainSubclass extends SecondDerived {
    {
        data = 0;
    }
}

Дозволені похідні класи можуть бути розташовані в інших пакетах.

2.3 Анотації (метадані)

Анотації дозволяють включити в програмний код додаткову інформацію, яка не може бути визначена за допомогою засобів мови. У тексті програми анотації починаються із символу @. Типовий приклад анотації – @Override. Завдяки цій анотації компілятор може перевірити, чи дійсно відповідний метод був створений у базовому класі.

public class MyClass {
  
    @Override
    public String toString() {  // Метод toString() визначено в класі Object
        return "My overridden method!";
    }

}

Можна навести інші приклади анотацій:

  • @SuppressWarnings("ідентифікатор_попередження") – попередження компілятора повинні бути замовчані в анотованому елементі
  • @Deprecated – використання анотованого елемента не є більше бажаним

Java дозволяє визначати власні анотації.

2.4 Поліморфізм

2.4.1 Загальні концепції

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

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

У мовах об'єктно-орієнтованого програмування пізнє зв'язування реалізоване через механізм віртуальних функцій. Віртуальна функція (віртуальний метод, virtual method) – це функція, визначена в базовому класі, та перевизначена (перекрита) у похідних, так, що конкретна реалізація функції для виклику визначатиметься під час виконання програми. Вибір реалізації віртуальної функції залежить від реального (а не оголошеного під час опису) типу об'єкта. Оскільки посилання на базовий тип може містити адресу об'єкта будь-якого похідного типу, поведінка раніше створених класів може бути змінена пізніше шляхом перевизначення віртуальних методів. Перевизначення передбачає відтворення імені, списку параметрів та специфікатора доступу. Фактично поліморфними є класи, які містять віртуальні функції.

У C++ для позначення віртуальної функції використовують модифікатор virtual. У Java всі методи є віртуальними, за винятком конструкторів, статичних (static), фінальних (final) і закритих (private) методів. На відміну від C++, слово virtual не використовується.

Починаючи з Java 5, перед перевизначеними віртуальними методами розміщують анотацію @Override, яка дозволяє компілятору здійснити додаткову перевірку синтаксису – відповідність сигнатури нової функції сигнатурі перекритої функції базового класу. Використання @Override є бажаним, але не обов'язковим.

Усі класи Java є поліморфними, оскільки таким є клас java.lang.Object. Зокрема, завдяки поліморфізму кожен клас може визначити свою віртуальну функцію toString(), яка буде викликана для автоматичного отримання даних про об'єкт у вигляді рядка:

public class SingleInteger {
    int i = 10;
	
    @Override
    public String toString() {
        return "i = " + i; 
    }

    public static void main(String[] args) {
        SingleInteger mc = new SingleInteger();
        System.out.println(mc); // i = 10;
    }
}

2.4.2 Абстрактні класи та методи

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

abstract class SomeConcept {
    // ...
}

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

Наприклад, абстрактний клас Shape (геометрична фігура) реалізує поля і методи, що можуть бути використані різними похідними класами. До таких полів можна, наприклад, віднести поточну позицію і метод переміщення по екрану moveTo(). У класі Shape також оголошені абстрактні методи, такі як draw(), що повинні бути реалізовані у всіх похідних класах, але по-різному. Усталена реалізація не має сенсу. Наприклад:

abstract class Shape {
    int x, y;
    // ...
    void moveTo(int newX, int newY) {
        // ...
    }
    abstract void draw();
}

Конкретні класи, створені від Shape, такі як Circle або Rectangle, визначають реалізацію методу draw().

class Circle extends Shape 
{
    @Override
    void draw() {
        // ...
    }
}

class Rectangle extends Shape {
    @Override
    void draw() {
        // ...
    }
}

Абстрактні методи аналогічні суто віртуальним функціям у C++.

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

2.5 Загальні відомості про інтерфейси. Упорядкування об'єктів

2.5.1 Створення та реалізація інтерфейсів

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

interface Printable {
    void print(String printerName);
    String getPreview();
}

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

Інтерфейси формально можуть містити поля, але вони є фінальними та статичними (константами часу компіляції). Вони повинні бути ініціалізовані під час створення:

interface MathConstants {
    double PI = 3.14159265359;
    double E = 2.71828182846;
}

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

Для того, щоб указати, що клас реалізує інтерфейс, ім'я інтерфейсу вказують у списку реалізованих інтерфейсів. Такий список розташовують у заголовку класу після ключового слова implements. Методи, визначені в інтерфейсі, є абстрактними та відкритими. У класі, що реалізує інтерфейс, такі методи повинні бути оголошені як public:

interface Printable {
    void print(String printerName);
    String getPreview();
}

class Document implements Printable {
    @Override
    public void print(String printerName) {

    }

    @Override
    public String getPreview() {
        String preview = "";
        //...
        return preview;
    }
}

Примітка: використання анотації @Override є бажаним, але не обов'язковим.

Записи (record) можуть реалізовувати інтерфейси.

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

interface Printable {
    void print(String printerName);
    String getPreview();
}

interface Editable {
    void edit();
}

interface Serviceable extends Printable, Editable {
    String getPreview(); // оголошення може повторюватися
}

Тепер клас, який реалізує інтерфейс Serviceable, повинен визначати три функції:

class Document implements Serviceable {
    @Override
    public void print(String printerName) {

    }

    @Override
    // Одна реалізація використовується для базового і похідного інтерфейсів:
    public String getPreview() {
        String preview = "";
        //...
        return preview;
    }

    @Override
    public void edit() {

    }
}

Клас може реалізувати кілька інтерфейсів. Це – більш розповсюджений шлях, ніж створення похідного інтерфейсу:

class AnotherDocument implements Printable, Editable {
    @Override
    public void print(String printerName) {

    }

    @Override
    public String getPreview() {
        String preview = "";
        //...
        return preview;
    }

    @Override
	   public void edit() {
        //...
    }
}

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

public static void main(String[] args) {
    Serviceable serviceable = new Document(); 
}

Інтерфейси не походять від класу java.lang.Object. Не можна створювати новий об'єкт типу інтерфейсу. Навіть для порожнього інтерфейсу треба створити клас, який його реалізує:

interface Empty {

}

class EmptyImplementation implements Empty {

}

// ...

public static void main(String[] args) {
    Empty empty1 = new Empty(); // Помилка
    Empty empty2 = new EmptyImplementation(); // Коректне створення об'єкта
}

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

2.5.2 Упорядкування об'єктів

JDK надає велику кількість стандартних інтерфейсів. Розглянемо застосування інтерфейсів Comparable і Comparator для сортування масивів.

У найпростішому випадку сортування всього масиву за зростанням здійснюється за допомогою функції sort() з одним параметром – посиланням на відповідний масив. Статична функція sort() класу java.util.Array реалізована для масивів усіх примітивних типів. Аналогічно можна реалізувати сортування об'єктів класів, для яких визначене натуральне порівняння, тобто реалізований інтерфейс Comparable. Єдиний метод цього інтерфейсу - compareTo():

public int compareTo(Object o)

Метод повинен повернути від'ємне значення (наприклад, -1), якщо об'єкт, для якого викликаний метод, менше об'єкта o, нульове значення, якщо об'єкти рівні, і додатне значення в протилежному випадку.

До класів, що реалізують інтерфейс Comparable, відносяться класи оболонки Double, Integer, Long і т.д., а також String. Наприклад, у такий спосіб можна розсортувати масив об'єктів типу Integer:

public class SortIntegers {
    public static void main(String[] args) {
        Integer[] a = {7, 8, 3, 4, -10, 0};
        java.util.Arrays.sort(a);
        System.out.println(java.util.Arrays.toString(a));
    }
}

Примітка. Ім'я інтерфейсу Comparable – це приклад найбільш коректного імені інтерфейсу. Бажано, щоб імена інтерфейсів закінчувалися суфіксом -able (Printable, Editable, Runnable, Drawable, Comparable тощо). Але це правило дуже часто порушується навіть для стандартних інтерфейсів.

У Java 5 Comparable – це узагальнений інтерфейс. Узагальнення в Java за своїм синтаксисом та використанням схожі на шаблони C++, але реалізовані повністю в інший спосіб. Створення та використання узагальнень буде розглянуто пізніше. Завдяки узагальненням у функціях, які оголошені в інтерфейсі, замість параметрів типу Object, можна використовувати параметри інших типів. В нашому випадку функція compareTo() повинна приймати аргумент типу елементу масиву.

Можна самостійно створити клас, що реалізує інтерфейс Comparable. Наприклад, масив прямокутників сортується за площею:

class Rectangle implements Comparable<Rectangle> {
    private double width, height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public double area() {
        return width * height;
    }

    public double perimeter() {
        return 2 * (width + height);
    }
    
    @Override
    public int compareTo(Rectangle rect) {
        return Double.compare(area(), rect.area());
    }

    @Override
    public String toString() {
        return "[" + width + ", " + height + ", area = " + area() + ", perimeter = " + perimeter() + "]";
    }

}

public class SortRectangles {

    public static void main(String[] args) {
        Rectangle[] a = {new Rectangle(2, 7), new Rectangle(5, 3), new Rectangle(3, 4)};
        java.util.Arrays.sort(a);
        System.out.println(java.util.Arrays.toString(a));
    }

}

У наведеному прикладі використовується статична функція compare() класу Double. Ця функція повертає значення, необхідні методу sort().

Якщо ми не хочемо (чи не можемо) визначити функцію compareTo(), можна створити клас, що реалізує інтерфейс Comparator. Посилання на об'єкт такого класу передаються як другий (четвертий) параметр функції sort() (наведено опис без узагальнень):

public static void sort(Object[] a, Comparator c);
public static void sort(Object[] a, int fromIndex, int toIndex, Comparator c);

Інтерфейс містить опис методу compare() з двома параметрами. Функція повинна повернути від'ємне число, якщо перший об'єкт під час сортування необхідно вважати меншим, чим інший, значення 0, якщо об'єкти еквівалентні, і додатне число в протилежному випадку.

Примітка. Починаючи з Java 5 Comparator – це також узагальнений інтерфейс. Під час його реалізації після його імені слід вказувати в кутових дужках тип об'єктів, які ми порівнюємо. Якщо використовувати узагальнення, функція compare() повинна приймати два аргументи типу параметра узагальнення.

Завдяки використанню класу, який реалізує інтерфейс Comparator, можна додатково здійснити сортування за периметром прямокутників:

class CompareByPerimeter implements java.util.Comparator<Rectangle>
{

    @Override
    public int compare(Rectangle r1, Rectangle r2) {
        return Double.compare(r1.perimeter(), r2.perimeter());
    }
}

public class SortRectangles {

    public static void main(String[] args) {
        Rectangle[] a = {new Rectangle(2, 7), new Rectangle(5, 3), new Rectangle(3, 4)};
        java.util.Arrays.sort(a);                           // сортування за площею
        System.out.println(java.util.Arrays.toString(a));
        java.util.Arrays.sort(a, new CompareByPerimeter()); // сортування за периметром
        System.out.println(java.util.Arrays.toString(a));
    }

}

2.6 Вкладені класи

2.6.1 Загальні концепції

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

class Outer {
    class Inner {
        int i;
    }

    Inner inner = new Inner();
}

class Another {
    Outer.Inner i;
}

Вкладені класи можуть бути оголошені зі специфікаторами public, private або protected.

Локальні класи створюють всередині блоків. Існує також спеціальний різновид локальних класів – безіменні класи.

2.6.2 Статичні вкладені класи

Використання статичних вкладених класів аналогічне вкладеним класам C++ і C#. Фактично це тип, описаний в області видимості іншого типу:

class Outer {
    static class Inner {
    }
}

Наприклад, можна створити власні класи-обгортки для цілих і дійсних чисел і визначити ці класи як вкладені:

public class Wrappers {
    public static class Integer {
        private int value;

        public Integer(int value) {
            this.value = value;
        }

        public int get() {
            return value;
        }

        public void set(int value) {
            this.value = value;
        }
    }

    public static class Double {
        private double value;

        public Double(double value) {
            this.value = value;
        }

        public double get() {
            return value;
        }

        public void set(double value) {
            this.value = value;
        }
    }
    
    // ...

}

Об'єкти таких класів можуть бути створені як у зовнішньому класі, так і поза ним. У класі Wrappers:

public class Wrappers {
    
    // ...
    
    protected void test() {
        Integer i = new Integer(3);
        System.out.println(i.get());
        Double d = new Double(3.6);
        System.out.println(d.get());
    }
}

В іншому класі:

public class WrappersTest {
    public static void main(String[] args) {
        Wrappers.Integer i = new Wrappers.Integer(10);
        System.out.println(i.get());
        Wrappers.Double d = new Wrappers.Double(10.5);
        System.out.println(d.get());
    }
}

Вкладені класи можна використовувати для створення типу якоїсь частини складного об'єкта, або частини його опису. Наприклад, всередині класу Circle можна створити окремий вкладений клас Center для представлення координат центру:

public class Circle {
    public static class Center {
        private double x, y; // координати центру кола

        public Center() {
        }

        public Center(double x, double y) {
            this.x = x;
            this.y = y;
        }

        public double getX() {
            return x;
        }

        public void setX(double x) {
            this.x = x;
        }

        public double getY() {
            return y;
        }

        public void setY(double y) {
            this.y = y;
        }

        double distance() {
            return Math.sqrt(x * x + y * y);
        }
    }

    private double radius;
    private Center center = new Center(); // координати центру кола

    //...

}

Об'єкт класу Center автоматично створюється під час створення об'єкта класу Circle, оскільки поле відповідного типу Center присутнє в класі Circle та ініціалізується необхідним об'єктом. Інакше клас був би просто статичним класом, вкладеним в інший.

Поза класом можна встановлювати та читати координати центру. Можна також окремо створити об'єкт класу і потім замінити цим об'єктом наявний об'єкт-центр:

public class CircleTest {
    public static void main(String[] args) {
        Circle circle = new Circle();
        circle.getCenter().setX(1);
        Circle.Center center = new Circle.Center(2, 3);
        circle.setCenter(center);
    }
}

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

public class Circle {
    private static String entityName = "Коло";

    public static String getEntityName() {
        return entityName;
    }

    public static class Center {

        public static String getClassInformation() {
            return "Внутрішній клас: Центр. Зовнішній клас: " + entityName;
        }
        
        // ...
    }

    //...
}

Якщо виникає конфлікт імен, необхідно використовувати префікс – ім'я класу:

public class Circle {
    private static String entityName = "Коло";

    public static String getEntityName() {
        return entityName;
    }

    public static class Center {
        private static String entityName = "Центр";

        public static String getClassInformation() {
            return "Внутрішній клас: " + entityName + ". Зовнішній клас: " + Circle.entityName;
        }

        // ...
    }

    //...
}

Статичні вкладені класи не мають доступу до нестатичних елементів зовнішніх класів. Але такий об'єкт можна створити або отримати як параметр. Тоді приватні нестатичні елементи теж доступні:

public class Circle {
    private double radius;
    
    public static class Center {
        private static String entityName = "Центр";

        public String getObjectInformation(Circle circle) {
            return "Координати центру кола: " + x + ", " + y +
                   ". Радіус кола: " + circle.radius;
        }

        // ...
    }

    // ...
}

Класи можна створювати всередині інтерфейсів. Такі класи автоматично є статичними. Усередині класів також можна створювати інтерфейси, які є також статичними.

2.6.3 Внутрішні класи

Нестатичні вкладені класи називають також внутрішніми. Головною відмінністю внутрішніх класів у Java є те, що об'єкти цих класів отримують посилання на об'єкт зовнішнього класу. З цього факту випливає два важливих висновки:

  • об'єкти внутрішніх класів мають прямий доступ до даних об'єкта зовнішнього класу;
  • для створення об'єкта внутрішнього класу обов'язково мати в наявності об'єкт зовнішнього класу.

Крім того, нестатичні внутрішні класи не можуть містити статичних елементів.

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

package ua.inf.iwanoff.java.third;

public class Circle {
    private static String entityName = "Коло";

    public static String getEntityName() {
        return entityName;
    }

    public class Center {
        private String entityName = "Центр";

        public String getClassInformation() {
            return "Внутрішній клас: " + entityName + ". Зовнішній клас: " + Circle.entityName;
        }

        public String getObjectInformation() {
            return "Координати центру кола: " + x + ", " + y + ". Радіус кола: " + radius;
        }

        // ... далі без змін
    }

    private double radius;
    private Center center = new Center(); // координати центру кола

    //...
}

Для створення об'єктів внутрішніх класів поза зовнішніми класами в Java запропонований спеціальний синтаксис:

об'єкт_зовнішнього_класу.new Констурктор_внутрішнього_класу();

В нашому випадку треба буде внести зміни у функцію main():

public class CircleTest {
    public static void main(String[] args) {
        Circle circle = new Circle();
        circle.getCenter().setX(1);
        Circle.Center center = new Circle().new Center(2, 3);
        circle.setCenter(center);
        System.out.println(center.getClassInformation());
        System.out.println(center.getObjectInformation());
    }
}

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

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

За допомогою внутрішніх класів можна змоделювати відсутній у Java механізм множинного спадкування:

class FirstBase {
    int a = 1;
}

class SecondBase {
    int b = 2;
}

class Outer extends FirstBase {
    int c = 3;

    class Inner extends SecondBase {
        void show() {
            System.out.println(a);
            System.out.println(b);
            System.out.println(c);
        }
    }
}

public class Test {

    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.show();
    }
}

Наведений приклад має суто теоретичний сенс, оскільки множинне успадкування класів незалежно від способів його реалізації є небезпечним з точки зору можливого конфлікту імен.

2.6.4 Локальні й безіменні класи

До внутрішніх класів також відносяться локальні. До таких класів не можна звернутися ззовні блоку, у якому вони визначені. Локальні класи найчастіше поміщають у тіло функції:

void f() {
    class Local {
        int j;
    }
    Local l = new Local();
    l.j = 100;
    System.out.println(l.j);
}

Можна також розміщати локальні класи всередині окремих блоків.

Локальні класи можуть мати свої базові класи або реалізовувати певні інтерфейси. У наведеному нижче прикладі локальний клас створюється для визначення способу сортування масиву рядків (за алфавітом у зворотному порядку):

static void sortReverse(String[] a) {
    class StringComparer implements Comparator<String> {
        @Override
        public int compare(String s1, String s2) {
            return s2.compareTo(s1);
        }
    }
    StringComparer comparer = new StringComparer();
    Arrays.sort(a, comparer);
}

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

new Object() {
    // Додавання нового методу:
    void hello() {
        System.out.println("Привіт!");
    }
}.hello();

System.out.println(new Object() {
    // Перевизначення методу:
    @Override 
    public String toString() {
        return "Це безіменний клас.";
    }
});

Безіменні класи не можуть бути абстрактними. Безіменний клас завжди є внутрішнім класом; він не може бути статичним. Безіменні класи автоматично є фінальними (final).

Безіменний клас може реалізовувати інтерфейс. Для визначення способу сортування масиву рядків можна також застосувати безіменний клас:

static void sortReverse(String[] a) {
    Arrays.sort(a, new Comparator<String>() {
        public int compare(String s1, String s2) {
            return s2.compareTo(s1);
        }
    });
}

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

abstract class Base {
    int k;

    Base(int k) {
        this.k = k;
    }

    abstract void show();
}

public class Test {

    static void showBase(Base b) {
        b.show();
    }

    public static void main(String[] args) {
        showBase(new Base(10) {
            void show() {
                System.out.println(k);
            }
        });
    }
}

Можна також використовувати блоки ініціалізації.

Для того, щоб безіменні класи мали доступ до локальних елементів зовнішніх блоків, ці елементи повинні бути описані як final.

2.7 Усталена реалізація методів інтерфейсів

Версія Java 8 надає нову можливість надання усталеної реалізації для методів, оголошених в інтерфейсі. Для цього перед відповідною функцією слід розмістити ключове слово default, після чого функцію можна реалізувати всередині інтерфейсу. Наприклад, можна запропонувати інтерфейс, який представляє деяку функцію, а також реалізацію цієї функції:

package ua.inf.iwanoff.java.third;

public interface Greetings {
    default void hello() {
        System.out.println("Hello everybody!");
    }
}

Клас, який реалізує інтерфейс, може бути порожнім. Можна залишити усталену реалізацію методу hello():

package ua.inf.iwanoff.java.third;

public class MyGreetings implements Greetings {

}

Під час тестування отримаємо усталене вітання.

package ua.inf.iwanoff.java.third;

public class GreetingsTest {

    public static void main(String[] args) {
        new MyGreetings().hello(); // Hello everybody!
    }

}

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

package ua.inf.iwanoff.java.third;

public class GreetingsTest {

    public static void main(String[] args) {
        new Greetings() { }.hello(); // Hello everybody!
    }

}

Наявність методів з усталеною реалізацією робить інтерфейси ще більш схожими на абстрактні (і навіть на неабстрактні) класи. Але зберігається принципова відмінність: інтерфейс не можна безпосередньо застосовувати для створення об'єктів. Усі класи безпосередньо або опосередковано походять від базового типу java.lang.Object, який містить дані й функції, необхідні для функціонування всіх, навіть найпростіших об'єктів. Інтерфейси не є класами й не походять від java.lang.Object. Інтерфейс – це лише декларація певної поведінки, яка може бути доповнена допоміжними засобами (методами з усталеною реалізацією). Поля, описані в інтерфейсі – це не власне дані об'єкту, а константи часу компіляції. Для виконання методів з усталеною реалізацією необхідний об'єкт класу, який реалізує інтерфейс. Саме через це в останньому прикладі створюється об'єкт безіменного класу

new Greetings() { }.hello();

а не інтерфейсу

new Greetings().hello(); // Синтаксична помилка!

Метод з усталеною реалізацією можна перевизначити:

package ua.inf.iwanoff.java.third;

public class MyGreetings implements Greetings {

    @Override
    public void hello() {
        System.out.println("Hello to me!");
    }

}

Тепер, створивши об'єкт цього класу, ми отримаємо нове привітання.

Якщо з перевизначеного методу необхідно викликати усталений метод інтерфейсу, можна скористатися ключовим словом super:

Greetings.super.hello();

Можна запропонувати такий приклад. Припустимо, необхідно надрукувати значення функції на деякому інтервалі з визначеним кроком. Створюємо інтерфейс з одним абстрактним методом (обчислення деякої функції) і одним з усталеною реалізацією:

package ua.inf.iwanoff.java.third;

public interface FunctionToPrint {
    double f(double x);
    default void print(double x) {
        System.out.printf("x = %7f f(x) = %7f%n", x, f(x));
    }
}

У класі PrintValues створюємо метод друку таблиці printTable(). Цей метод використовує створений раніше інтерфейс.

package ua.inf.iwanoff.java.third;

public class PrintValues {

    static void printTable(double from, double to, 
                      double step, FunctionToPrint func) {
        for (double x = from; x <= to; x += step) {
            func.print(x);
        }
        System.out.println();
    }

    // у функції main() створюємо об'єкт безіменного класу:
    public static void main(String[] args) {
        printTable(-2, 2, 0.5, new FunctionToPrint() {
            @Override
            public double f(double x) {
                return x * x * x;
            }
        });
    }

}

Припустимо, нас не влаштувала точність значень. У цьому випадку в безіменному класі можна також перевизначити метод print():

    public static void main(String[] args) {
        printTable(-2, 2, 0.5, new FunctionToPrint() {
            @Override
            public double f(double x) {
                return x * x * x;
            }

            @Override
            public void print(double x) {
                System.out.printf("x = %9f f(x) = %9f%n", x, f(x));
            }
            
        });
    }

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

public interface SomeInterface {
    void f();
}

Цей інтерфейс реалізовувався деяким класом:

public class OldImpl implements SomeInterface {
    @Override
    public void f() {
        // реалізація
    }
}

Тепер під час оновлення бібліотеки ми створили нову версію інтерфейсу, додавши в нього новий метод:

interface SomeInterface {
    void f();
    default void g() {
        // реалізація
    }
} 

Цей метод буде реалізований новими класами:

public class NewImpl implements SomeInterface {

    @Override
    public void f() {
        // реалізація
    }

    @Override
    public void g() {
        // реалізація
    }
}

Без усталеної реалізації не компілюватиметься код, побудований на попередній версії.

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

У Java 8 інтерфейси також можуть містити реалізацію статичних методів. Логічно всередині інтерфейсу визначати методи, що мають відношення до цього інтерфейсу (наприклад, вони можуть одержувати посилання на інтерфейс як параметр). Найчастіше, це допоміжні методи. Як і всі елементи інтерфейсу, такі статичні методи є публічними. Можна вказати public явно, але в цьому немає потреби.

У наведеному раніше прикладі функцію printTable() можна було б розмістити всередині інтерфейсу:

package ua.inf.iwanoff.java.third;

public interface FunctionToPrint {
    double f(double x);
    default void print(double x) {
        System.out.printf("x = %9f f(x) = %9f%n", x, f(x));
    }
    static void printTable(double from, double to, double step, FunctionToPrint func) {
        for (double x = from; x <= to; x += step) {
            func.print(x);
        }
        System.out.println();
    }
}

Виклик функції слід здійснювати через ім'я інтерфейсу.

2.8 Робота з функціональними інтерфейсами в Java 8

2.8.1 Лямбда-вирази й функціональні інтерфейси

Дуже часто інтерфейси в Java містять оголошення однієї абстрактної функції (без усталеної реалізації). Такі інтерфейси отримали назву функціональних інтерфейсів. Їх повсюдно використовують для реалізації механізмів зворотного виклику, обробки подій і т. д. Не дивлячись на простоту, для їхньої реалізації, (а) проте, потрібен окремий клас – звичайний, вкладений або безіменний. Навіть використовуючи безіменний клас ми отримуємо громіздкий синтаксис, який погано читається. Скоротити необхідність безіменних класів у сирцевому коді дозволяють лямбда-вирази, які з'явилися у версії Java 8.

У мовах програмування є поняття функціонального об'єкта – об'єкта, який можна використовувати як функцію. Лямбда-вираз – це спеціальний синтаксис опису функціонального об'єкта всередині методу. Іншими словами, лямбда-вираз – це спосіб опису функції всередині іншої функції.

Термін "лямбда- вираз" пов'язаний з математичною дисципліною – лямбда-численням. Лямбда-числення – це формальна система, розроблена американським математиком Алонсо Черчем для формалізації й аналізу поняття обчислюваності. Лямбда-числення стало формальною основою мов функційного програмування (Lisp, Scheme тощо).

Лямбда-вираз у Java має такий синтаксис:

  • список формальних параметрів, розділених комами й укладених у круглі дужки; якщо параметр один, дужки можна опустити; якщо параметрів немає, потрібна порожня пара дужок;
  • стрілка (->);
  • тіло, що складається з одного виразу або блоку; якщо використовується блок, усередині нього може бути твердження return;

Наприклад, функція з одним параметром:

k -> k * k

Те саме з дужками та блоком:

(k) -> { return k * k; }

Функція з двома параметрами:

(a, b) -> a + b

Функція без параметрів:

() -> System.out.println("First")

Наприклад, маємо функціональний інтерфейс:

public interface SomeInt {
    int f(int x);
}

Під час виклику деякої функції потрібен параметр типу функціонального інтерфейсу. Традиційно можна створити безіменний клас:

someFunc(new SomeInt() {
    @Override
    public int f(int x) {
        return x * x;
    }
});

Можна створити змінну типу об'єкта, що реалізує інтерфейс, і використовувати її замість безіменного класу:

SomeInt func = k -> k * k;
someFunc(func);

Можна також створити безіменний об'єкт під час виклику функції з параметром-функціональним інтерфейсом:

someFunc(x -> x * x);

Оскільки кожен лямбда-вираз пов'язаний з певним функціональним інтерфейсом, типи параметрів і результату визначаються автоматично через зіставлення з відповідним функціональним інтерфейсом.

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

package ua.inf.iwanoff.java.third;

public interface FunctionToPrint {
    double f(double x);
    default void print(double x) {
        System.out.printf("x = %9f f(x) = %9f%n", x, f(x));
    }
    static void printTable(double from, double to, double step, FunctionToPrint func) {
        for (double x = from; x <= to; x += step) {
            func.print(x);
        }
        System.out.println();
    }
}

Використання функціонального інтерфейсу з застосуванням лямбда-виразу:

package ua.inf.iwanoff.java.third;

public class PrintWithLambda {
    public static void main(String[] args) {
        FunctionToPrint.printTable(-2.0, 2.0, 0.5, x -> x * x * x);
    }
}

2.8.2 Використання посилань на методи

Дуже часто все тіло лямбда-виразу складається лише з виклику певного методу. У цьому випадку замість лямбда-виразу можна використовувати посилання на цей метод. Існує кілька варіантів опису посилань на методи.

Вид посилання на метод Синтаксис Приклад
Посилання на статичний метод ім'яКласу::ім'яСтатичногоМетоду String::valueOf
Посилання на нестатичний метод для заданого об'єкта ім'яОб'єкта::ім'яНестатичногоМетоду s::toString
Посилання на нестатичний метод для параметра ім'яКласу::ім'яНестатичногоМетоду Object::toString
Посилання на конструктор ім'яКласу::new String::new

Наприклад, є такі функціональні інтерфейси:

interface IntOperation {
    int f(int a, int b);
}

interface StringOperation {
    String g(String s);
}

Можна створити деякий клас:

class DifferentMethods
{
    public int add(int a, int b) {
        return a + b;
    }

    public static int mult(int a, int b) {
        return a * b;
    }
}

Викликаємо методи:

public class TestMethodReferences {

    static void print(IntOperation op, int a, int b) {
        System.out.println(op.f(a, b));
    }
  
    static void print(StringOperation op, String s) {
        System.out.println(op.g(s));
    }
  
    public static void main(String[] args) {
        DifferentMethods dm = new DifferentMethods();
        print(dm::add, 3, 4);
        print(DifferentMethods::mult, 3, 4);
        print(String::toUpperCase, "text");    
    }
}

2.8.3 Стандартні функціональні інтерфейси

Замість того, щоб створювати нові функціональні інтерфейси, в більшості випадків достатньо скористатися стандартними інтерфейсами, які описані в пакеті java.util.function.

Інтерфейс Опис
BooleanSupplier Представляє "постачальника" результату типу boolean
DoubleBinaryOperator Представляє операцію над двома аргументами типу double, яка повертає результат типу double
DoubleConsumer Представляє операцію, яка приймає один аргумент типу double і не повертає результату
DoublePredicate Представляє предикат (функцію з результатом типу boolean) з одним аргументом типу double
DoubleSupplier Представляє "постачальника" результату типу double
DoubleToIntFunction Представляє операцію, яка приймає один аргумент типу double і повертає результат типу int
DoubleToLongFunction Представляє операцію, яка приймає один аргумент типу double і повертає результат типу long
DoubleUnaryOperator Представляє операцію, яка приймає один аргумент типу double і повертає результат типу double
IntBinaryOperator Представляє операцію над двома аргументами типу int, яка повертає результат типу int
IntConsumer Представляє операцію, яка приймає один аргумент типу int і не повертає результату
IntPredicate Представляє предикат (функцію з результатом типу boolean) з одним аргументом типу int
IntSupplier Представляє "постачальника" результату типу int
IntToDoubleFunction Представляє операцію, яка приймає один аргумент типу int і повертає результат типу double
IntToLongFunction Представляє операцію, яка приймає один аргумент типу int і повертає результат типу long
IntUnaryOperator Представляє операцію, яка приймає один аргумент типу int і повертає результат типу int
LongBinaryOperator Представляє операцію над двома аргументами типу long, що повертає результат типу long
LongConsumer Представляє операцію, яка приймає один аргумент типу long і не повертає результату
LongPredicate Представляє предикат (функцію з результатом типу boolean) з одним аргументом типу long
LongSupplier Представляє "постачальника" результату типу long
LongToDoubleFunction Представляє операцію, яка приймає один аргумент типу long і повертає результат типу double
LongToIntFunction Представляє операцію, яка приймає один аргумент типу long і повертає результат типу int
LongUnaryOperator Представляє операцію, яка приймає один аргумент типуlong і повертає результат типу long

Наприклад, можна вручну створити функціональний інтерфейс SomeFunction і скористатися цим інтерфейсом:

interface SomeFunction {
    double func(double x);
}

class ValuePrinter {
    static void printValue(SomeFunction function, double x) {
        System.out.printf("x = %f, f(x) = %f", x, function.func(x));
    }

    static void test() {
        printValue(Math::sqrt, 4);
    }
}

Але більш продуктивний підхід – скористатися стандартним інтерфейсом DoubleUnaryOperator, не створюючи нового інтерфейсу:

class ValuePrinter {
    static void printValue(DoubleUnaryOperator function, double x) {
        System.out.printf("x = %f, f(x) = %f", x, function.applyAsDouble(x));
    }

    static void test() {
        printValue(Math::sqrt, 4);
    }
}

Крім перелічених, є корисні узагальнені функціональні інтерфейси, які будуть розглянути пізніше.

Функціональними інтерфейсами також є узагальнений інтерфейс Comparator, інтерфейс Runnable, який використовують у багатопотоковому програмуванні, а також багато інших.

2.8.4 Композиція лямбда-виразів

Можна здійснювати композицію лямбда-виразів (використовувати лямбда-вирази як параметри). З цією метою інтерфейси пакету java.util.function надають методи з усталеною реалізацією, що забезпечують виконання деякої функції, переданої як параметр до або після даного методу. Зокрема, у стандартних функціональних інтерфейсах визначені такі методи:

// Виконується функція before, а потім функція, що викликає:
Function compose(Function before)
// Функція after виконується після функції, що викликає:
Function andThen(Function after)

Використання цих методів та їх відмінність розглянемо на такому прикладі. Є клас зі статичною функцією calc(), що приймає функціональний інтерфейс і аргумент типу double. Можна здійснити композицію лямбда-виразів:

package ua.inf.iwanoff.java.third;

import java.util.function.DoubleUnaryOperator;

public class ComposeDemo {

    public static double calc(DoubleUnaryOperator operator, double x) {
        return operator.applyAsDouble(x);
    }

    public static void main(String[] args) {
        DoubleUnaryOperator addTwo = x -> x + 2;
        DoubleUnaryOperator duplicate = x -> x * 2;
        System.out.println(calc(addTwo.compose(duplicate), 10.0)); // 22.0
        System.out.println(calc(addTwo.andThen(duplicate), 10.0)); // 24.0
    }
}

Композиція може бути складнішою:

System.out.println(calc(addTwo.andThen(duplicate).andThen(addTwo), 10.0)); // 26.0

2.9 Клонування об'єктів, перевірка еквівалентності та хеш-коди

2.9.1 Клонування

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

У базовому класі java.lang.Object є функція clone(), усталене використання якої дозволяє скопіювати об'єкт поелементно. Ця функція також визначена для масивів, рядків і інших стандартних класів. Наприклад, так можна отримати копію масиву, який існує, і працювати з цією копією:

package ua.inf.iwanoff.java.third;

import java.util.Arrays;

public class ArrayClone {
  
    public static void main(String[] args) {
        int[] a1 = { 1, 2, 3, 4 };
        int[] a2 = a1.clone(); // Копія елементів
        System.out.println(Arrays.toString(a2)); // [1, 2, 3, 4]
        a1[0] = 10; // змінюємо перший масив
        System.out.println(Arrays.toString(a1)); // [10, 2, 3, 4]
        System.out.println(Arrays.toString(a2)); // [1, 2, 3, 4]
    }
}

Для того, щоб можна було клонувати об'єкти користувальницьких класів, ці класи повинні реалізовувати інтерфейс Cloneable. Цей інтерфейс не оголошує жодного методу. Він всього лише вказує, що об'єкти цього класу можна клонувати. В іншому випадку виклик функції clone() призведе до генерації винятку типу CloneNotSupportedException.

Примітка. Механізм обробки винятків багато в чому схожий на відповідний механізм мови C++. У Java в заголовку методів, які генерують винятки, слід перелічувати можливі винятки за допомогою ключового слова throws. Механізм обробки винятків буде розглянуто пізніше.

Припустимо, нам потрібно клонувати об'єкти класу Human, що включає два поля типу Stringname і surname. Додаємо до опису класу реалізацію інтерфейсу Cloneable, генеруємо конструктор з двома параметрами, для зручності виведення вмісту полів перекриваємо функцію toString(). У функції main() здійснюємо тестування клонування об'єкта:

package ua.inf.iwanoff.java.third;

public class Human implements Cloneable {
    private String name;
    private String surname;
  
    public Human(String name, String surname) {
        super();
        this.name = name;
        this.surname = surname;
    }

    @Override
    public String toString() {
        return name + " " + surname;
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        Human human1 = new Human("John", "Smith");
        Human human2 = (Human) human1.clone();
        System.out.println(human2); // John Smith
        human1.name = "Mary";
        System.out.println(human1); // Mary Smith
        System.out.println(human2); // John Smith
    }
}

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

Для зручності використання функції clone() її можна перекрити, змінивши її тип результату і зробивши відкритою. Завдяки наявності цієї функції спроститься клонування (не потрібно буде кожен раз приводити тип):

@Override
public Human clone() throws CloneNotSupportedException {
    return (Human) super.clone();
}

// ...

Human human2 = human1.clone();

Стандартне клонування, реалізоване в класі java.lang.Object, дозволяє створювати копії об'єктів, поля яких – типи значення і тип String (а також класи-обгортки). Якщо поля об'єкта – посилання на масиви або інші типи, необхідно застосовувати так зване "глибоке" клонування. Припустимо, певний клас SomeCloneableClass містить два поля типу double масив цілих. "Глибоке" клонування забезпечить створення окремих масивів для різних об'єктів.

package ua.inf.iwanoff.java.third;

import java.util.Arrays;

public class SomeCloneableClass implements Cloneable {
    private double x, y;
    private int[] a;
  
    public SomeCloneableClass(double x, double y, int[] a) {
        super();
        this.x = x;
        this.y = y;
        this.a = a;
    }

    @Override
    protected SomeCloneableClass clone() throws CloneNotSupportedException {
        SomeCloneableClass scc = (SomeCloneableClass) super.clone(); // копіюємо х і y
        scc.a = a.clone(); // тепер два об'єкти працюють з різними масивами
        return scc;
    }

    @Override
    public String toString() {
        return " x=" + x + " y=" + y + " a=" + Arrays.toString(a);
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        SomeCloneableClass scc1 = new SomeCloneableClass(0.1, 0.2, new int[] { 1, 2, 3 });
        SomeCloneableClass scc2 = scc1.clone();
        scc2.a[2] = 4;
        System.out.println("scc1:" + scc1);
        System.out.println("scc2:" + scc2);
    }
}

2.9.2 Перевірка еквівалентності

Для того, щоб переконатися, що клоновані об'єкти однакові, не завадило б мати можливість автоматичного порівняння всіх полів. Посилальна модель об'єктів Java не дозволяє порівнювати вміст об'єктів за допомогою операції порівняння (==), оскільки при цьому порівнюються посилання. Для порівняння даних доцільно використовувати функцію equals(), визначену в класі java.lang.Object. Для класів, полями яких є типи-значення, метод класу Object забезпечує поелементне порівняння. Якщо ж полями є посилання на об'єкти, необхідно явно перевизначити функцію equals(). Типова реалізація методу equals() передбачає перевірку посилань (чи вони збігаються), далі перевірку об'єкта, який ми порівнюємо, на значення null, потім – перевірку типу, наприклад, за допомогою instanceof. Якщо типи збігаються, здійснюється перевірка значень полів.

Наведемо повний приклад з класом Human.

package ua.inf.iwanoff.java.third;

public class Human implements Cloneable {
    private String name;
    private String surname;

    public Human(String name, String surname) {
        super();
        this.name = name;
        this.surname = surname;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || !(obj instanceof Human)) {
            return false;
        }
        Human h = (Human) obj;
        return name.equals(h.name) && surname.equals(h.surname);
    }

    @Override
    public Human clone() throws CloneNotSupportedException {
        return (Human) super.clone();
    }

    @Override
    public String toString() {
        return name + " " + surname;
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        Human human1 = new Human("John", "Smith");
        Human human2 = human1.clone();
        System.out.println(human2);
        human1.name = "Mary";
        System.out.println(human1);
        System.out.println(human2);
        human2.name = new String("Mary");
        System.out.println(human2);
        System.out.println(human1.equals(human2)); // true
    }
}

Якби метод equals() не було визначено, останнє порівняння дало б false.

Для порівняння двох масивів доцільно викликати статичну функцію equals() класу Arrays. Ця функція порівнює елементи масивів (викликає метод equals()):

Arrays.equals(array1, array2);

Записи (record) автоматично надають коректну реалізацію методу equals().

2.9.3 Хешування

Для великих об'єктів реалізація функції equals() може вимагати істотних обчислень, оскільки треба перевірити на еквівалентність усі наявні дані. Для скороченої перевірки на можливу рівність двох об'єктів у Java застосовують так зване хешування.

Хешування (hashing) – це процес отримання з даних про об'єкт унікального коду з використанням деякого формального алгоритму. У широкому сенсі результатом є послідовність біт фіксованої довжини, в окремому випадку – просто ціле число. Це перетворення здійснює так звана хеш-функція, або функція хешування. Функція хешування повинна відповідати такій вимозі: хеш-функція повинна повертати однаковий хеш-код кожного разу, коли вона застосована до однакових або рівних об'єктів. Для різних об'єктів гарантувати відмінність хеш-кодів на жаль не можливо.

Хешування використовують в деяких контейнерних класів бібліотеки колекцій Java, для того, щоб унеможливити потрапляння в колекції однакових елементів.

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

Конкретні стандартні класи реалізують свої хеш-функції. Наприклад, для рядків значення хеш-функції обчислюється за формулою:

s[0]*31n-1 + s[1]*31n-2 + ... + s[n-1]

Тут s[0], s[1] і т.д. – коди відповідних символів.

Стабільно працюють також функції hashCode() для класів Integer, Double тощо. Для користувацьких типів функцію hashCode() слід перевизначати. Зазвичай хеш-код об'єкта генерують з хеш-кодів полів. Найпростіший варіант – скористатися статичною функцією hash() класу java.util.Objects (починаючи з Java 7). Припустимо, створено клас Person. Хеш-код можна згенерувати за допомогою функції hash():

class Person {
    String name;
    int year;

    public Person(String name, int year) {
        this.name = name;
        this.year = year;
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, year);
    }
}

Можна також запропонувати власні алгоритми отримання хеш-кодів.

Записи (record) автоматично надають коректну хеш-функцію.

3 Приклади програм

3.1 Ієрархія об'єктів реального світу

Припустимо, необхідно розробити ієрархію класів "Регіон" – "Населений район" – "Країна". Окремі класи цієї ієрархії можуть стати базовими для інших класів (наприклад "Незаселений острів", "Національний парк", "Адміністративний район", "Автономна республіка" і т.д.). Ієрархію класів можна доповнити класами "Місто" і "Острів". Доцільно в кожен клас додати конструктор, який ініціалізує усі поля. Можна також створити масив посилань на різні об'єкти ієрархії та для кожного об'єкта вивести на екран рядок даних про нього.

Для того, щоб одержати рядкове представлення об'єкта, необхідно перекрити функцію toString()

Можна запропонувати таку ієрархію класів.

package ua.inf.iwanoff.java.third;

import java.util.*;

// Ієрархія класів
class Region {
    private String name;
    private double area;

    public Region(String name, double area) {
        this.name = name;
        this.area = area;
    }

    public String getName() {
        return name;
    }

    public double getArea() {
        return area;
    }

    @Override
    public String toString() {
        return "Регіон " + name + ".\tТериторія   " + area + " кв.км.";
    }
}

class PopulatedRegion extends Region {
    private int population;

    public PopulatedRegion(String name, double area, int population) {
        super(name, area);
        this.population = population;
    }

    public int getPopulation() {
        return population;
    }

    public int density() {
        return (int) (population / getArea());
    }

    @Override
    public String toString() {
        return "Населений регіон " + getName() + ".\tТериторія   " + getArea() +
               " кв.км.   \tНаселення " + population + 
               " чол.\tЩільність населення " + density() + " чол/кв.км.";
    }
}

class Country extends PopulatedRegion {
    private String capital;

    public Country(String name, double area, int population, String capital) {
        super(name, area, population);
        this.capital = capital;
    }

    public String getCapital() {
        return capital;
    }

    @Override
    public String toString() {
        return "Країна " + getName() + ".\tТериторія   " + getArea() +
               " кв.км.   \tНаселення " + getPopulation() + 
               " чол.\tЩільність населення " + density() + 
               " чол/кв.км.\tСтолиця " + capital;
    }
}

class City extends PopulatedRegion {
    private int boroughs; // Кількість районів

    public City(String name, double area, int population, int boroughs) {
        super(name, area, population);
        this.boroughs = boroughs;
    }

    public int getBoroughs() {
        return boroughs;
    }

    @Override
    public String toString() {
        return "Місто " + getName() + ".\tТериторія   " + getArea() +
               " кв.км.   \tНаселення " + getPopulation() + 
               " чол.\tЩільність населення " + density() + 
               " чол/кв.км.\tРайонів – " + boroughs;
    }
  
}

class Island extends PopulatedRegion {
    private String sea;

    public Island(String name, double area, int population, String sea) {
        super(name, area, population);
        this.sea = sea;
    }

    public String getSea() {
        return sea;
    }

    @Override
    public String toString() {
        return "Острів " + getName() + ".\tТериторія   " + getArea() +
               " кв.км.   \tНаселення " + getPopulation() + 
               " чол.\tЩільність населення " + density() + 
               " чол/кв.км.\tМоре – " + sea;
    }  
}

public class Regions {
  
    public static void main(String[] args) {
        Region[] a = { new City("Київ", 839, 2679000, 10),
                       new Country("Україна", 603700, 46294000, "Київ"),
                       new City("Харків", 310, 1461000, 9),
                       new Island("Зміїний", 0.2, 30, "Чорне") };
        for (Region region : a) {
            System.out.println(region);
        }
    }
}

3.2 Клас для представлення масиву точок

3.2.1 Визначення завдання й створення абстрактного класу

Припустимо, необхідно розробити клас для представлення масиву точок. Кожна точка представлена двома числами типу doublex і y. Необхідно забезпечити завдання точки, отримання інформації про координати конкретної точки та загальну кількість точок, а також додавання точки в кінець масиву і видалення останньої точки. Крім того, необхідно організувати сортування масиву за зростанням заданої координати й виведення координат точок у рядок.

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

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

package ua.inf.iwanoff.java.third;

public abstract class AbstractArrayOfPoints {
    // Запис нових координат точки:
    public abstract void setPoint(int i, double x, double y);

    // Отримання X точки i:
    public abstract double getX(int i);

    // Отримання Y точки i:
    public abstract double getY(int i);

    // Отримання кількості точок:
    public abstract int count();

    // Додавання точки в кінець масиву:
    public abstract void addPoint(double x, double y);

    // Видалення останньої точки:
    public abstract void removeLast();

    // Сортування за значеннями X:
    public void sortByX() {
        boolean mustSort; // Повторюємо доти,
                          // доки mustSort дорівнює true
        do {
            mustSort = false;
            for (int i = 0; i < count() - 1; i++) {
                if (getX(i) > getX(i + 1)) {
                    // обмінюємо елементи місцями
                    double x = getX(i);
                    double y = getY(i);
                    setPoint(i, getX(i + 1), getY(i + 1));
                    setPoint(i + 1, x, y);
                    mustSort = true;
                }
            }
        }
        while (mustSort);
    }

    // Аналогічно можна реалізувати функцію sortByY()

    // Виведення точок у рядок:
    @Override
    public String toString() {
        String s = "";
        for (int i = 0; i < count(); i++) {
            s += "x = " + getX(i) + " \ty = " + getY(i) + "\n";
        }
        return s + "\n";
    }

    // Тестуємо сортування на чотирьох точках:
    public void test() {
        addPoint(22, 45);
        addPoint(4, 11);
        addPoint(30, 5.5);
        addPoint(-2, 48);
        sortByX();
        System.out.println(this);
    }

}

Тепер можна реалізувати різні варіанти представлення структури даних.

3.2.2 Реалізація через масив об'єктів типу Point

Першою з можливих реалізацій буде створення класу Point та використання масиву посилань на Point. Клас для представлення точки можна додати в той же пакет. Клас Point міститиме два поля і конструктор:

package ua.inf.iwanoff.java.third;

public class Point {
    private double x, y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() {
        return x;
    }

    public double getY() {
        return y;
    }

    public void setPoint(double x, double y) {
        this.x = x;
        this.y = y;
    }
  
}

У тому ж проєкті створюємо клас ArrayOfPointObjects. У класі ArrayOfPointObjects створюємо поле – посилання на масив Point та ініціалізуємо його порожнім масивом. Реалізація більшості функцій видається очевидною. Найбільшу складність являють функції додавання та видалення точок. В обох випадках необхідно створити новий масив потрібної довжини й переписати в нього вміст старого. У функції main() здійснюємо тестування. Весь код файлу AbstractArrayOfPoints.java матиме такий вигляд:

package ua.inf.iwanoff.java.third;

public class ArrayOfPointObjects extends AbstractArrayOfPoints {
    private Point[] p = { };
  
    @Override
    public void setPoint(int i, double x, double y) {
        if (i < count()) {
            p[i].setPoint(x, y);
        }
    }

    @Override
    public double getX(int i) {
        return p[i].getX();
    }

    @Override
    public double getY(int i) {
        return p[i].getY();
    }

    @Override
    public int count() {
        return p.length;
    }

    @Override
    public void addPoint(double x, double y) {
        // Створюємо масив, більший на один елемент:
        Point[] p1 = new Point[p.length + 1];
        // Копіюємо всі елементи:
        System.arraycopy(p, 0, p1, 0, p.length);
        // Записуємо нову точку в останній елемент:
        p1[p.length] = new Point(x, y);
        p = p1; // Тепер p вказує на новий масив
    }

    @Override
    public void removeLast() {
        if (p.length == 0) {
            return; // Масив вже порожній
        }
        // Створюємо масив, менший на один елемент:
        Point[] p1 = new Point[p.length - 1];
        // Копіюємо всі елементи, крім останнього:
        System.arraycopy(p, 0, p1, 0, p1.length);
        p = p1; // Тепер p вказує на новий масив
    }

    public static void main(String[] args) {
        // Можна створити безіменний об'єкт:
        new ArrayOfPointObjects().test();
    }

}

Як результат отримаємо в консольному вікні точки, розсортовані за координатою X.

3.2.3 Реалізація через два масиви

Альтернативна реалізація передбачає створення двох масивів для окремого зберігання значень X і Y. Створюємо клас ArrayWithTwoArrays із використанням аналогічних опцій. У класі ArrayWithTwoArrays створюємо два поля – посилання на масиви дійсних чисел і ініціалізуємо їх порожніми масивами. Реалізація функцій аналогічна попередньому варіанту. У функції main() здійснюємо тестування:

package ua.inf.iwanoff.java.third;

public class ArrayWithTwoArrays extends AbstractArrayOfPoints {
    private double[] ax = { };
    private double[] ay = { };
  
    @Override
    public void setPoint(int i, double x, double y) {
        if (i < count()) {
            ax[i] = x;
            ay[i] = y;
        }
    }

    @Override
    public double getX(int i) {
        return ax[i];
    }

    @Override
    public double getY(int i) {
        return ay[i];
    }

    @Override
    public int count() {
        return ax.length; // Можна ay.length, вони однакові
    }

    @Override
    public void addPoint(double x, double y) {
        double[] ax1 = new double[ax.length + 1];
        System.arraycopy(ax, 0, ax1, 0, ax.length);
        ax1[ax.length] = x;
        ax = ax1;
        double[] ay1 = new double[ay.length + 1];
        System.arraycopy(ay, 0, ay1, 0, ay.length);
        ay1[ay.length] = y;
        ay = ay1;
    }

    @Override
    public void removeLast() {
        if (count() == 0) {
            return;
        }
        double[] ax1 = new double[ax.length - 1];
        System.arraycopy(ax, 0, ax1, 0, ax1.length);
        ax = ax1;
        double[] ay1 = new double[ay.length - 1];
        System.arraycopy(ay, 0, ay1, 0, ay1.length);
        ay = ay1;
    }

    public static void main(String[] args) {
        new ArrayWithTwoArrays().test();
    }

}

Результати мають бути ідентичними.

3.3 Використання інтерфейсів з усталеною реалізацією методів

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

f '(x) = (f(x + dx) – f(x)) / dx

Чим менше dx, тим точніше буде знайдена похідна. Другу похідну можна знайти як похідну першої похідної.

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

xn+1 = xnf(xn) / f '(xn)

Описуємо інтерфейс. Обчислення першої та другої похідної здійснюється методами з усталеною реалізацією:

package ua.inf.iwanoff.java.third;

public interface FunctionWithDerivatives {
    double DX = 0.001;
  
    double f(double x);
  
    default double f1(double x) {
        return (f(x + DX) - f(x)) / DX;
    }
  
    default double f2(double x) {
        return (f1(x + DX) - f1(x)) / DX;
    }
}

Реалізуємо клас зі статичною функцією розв'язання рівняння:

package ua.inf.iwanoff.java.third;

public class Newton {
  
    public static double solve(double from, double to, double eps, FunctionWithDerivatives func) {
        double x = from;
        if (func.f(x) * func.f2(x) < 0) { // знаки різні
            x = to;
        }
        double d;
        do {
            d = func.f(x) / func.f1(x);
            x -= d;
        }
        while (Math.abs(d) > eps);
        return x;
    }
}

Створюємо клас, який реалізує інтерфейс, і здійснюємо розв'язання рівняння:

package ua.inf.iwanoff.java.third;

public class FirstImplementation implements FunctionWithDerivatives {

    @Override
    public double f(double x) {
        return Math.sin(x - 0.5);
    }
  
    public static void main(String[] args) {
        System.out.println(Newton.solve(0, 1, 0.000001, new FirstImplementation()));
    }
}

Для функцій можна перевизначити механізм обчислення першої та другої похідної. Наприклад, для кубічного полінома

f(x) = x36x2 + 12x – 9

можна так визначити першу і другу похідну:

f '(x) = 3x2 – 12x + 12
f ''(x) = 6x – 12

Тоді клас, який реалізує інтерфейс, може бути таким:

package ua.inf.iwanoff.java.third;

public class SecondImplementation implements FunctionWithDerivatives {

    @Override
    public double f(double x) {
        return x * x * x - 6 * x * x + 12 * x - 9;
    }

    @Override
    public double f1(double x) {
        return 3 * x * x - 12 * x + 12;
    }

    @Override
    public double f2(double x) {
        return 6 * x - 12;
    }

    public static void main(String[] args) {
        System.out.println(Newton.solve(0, 1, 0.000001, new SecondImplementation()));
    }
}

Явне визначення похідних може підвищити ефективність алгоритму.

3.4 Розв'язання рівняння методом дихотомії

3.4.1 Постановка задачі

Припустимо, необхідно розв'язати методом дихотомії (ділення відрізка навпіл) довільне рівняння.

f(x) = 0

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

Чисельні методи рішення рівнянь припускають багаторазове обчислення в різних точках значень функціональної залежності f(x), яка визначає ліву частину рівняння. Функція f(x) може мінятися в різних задачах. Необхідно реалізувати механізм передачі інформації про цю залежність класу, який відповідає за розв'язання рівняння.

3.4.2 Використання абстрактного класу

Перший варіант ґрунтується на використанні абстрактного класу. Створюємо новий клас – AbstractEquation, який містить абстрактну функцію f() і функцію розв'язання рівняння – solve():

package ua.inf.iwanoff.java.third;

public abstract class AbstractEquation {
    public abstract double f(double x);
  
    public double solve(double a, double b, double eps) {
        double x = (a + b) / 2;
        while (Math.abs(b - a) > eps) {
            if (f(a) * f(x) > 0) {
                a = x;
            }
            else {
                b = x;
            }
            x = (a + b) / 2;
        }
        return x;
    }
}

Тепер можна створити клас з конкретною функцією f():

package ua.inf.iwanoff.java.third;

public class SpecificEquation extends AbstractEquation {
    @Override
    public double f(double x) {
        return x * x - 2;
    }

    public static void main(String[] args) {
        SpecificEquation se = new SpecificEquation();
        System.out.println(se.solve(0, 2, 0.000001));
    }
}

3.4.3 Використання інтерфейсу і класу, який його реалізує

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

Для створення інтерфейсу в середовищі IntelliJ IDEA використовується функція головного менюFile | New | Java Class, далі вводимо ім'я та зі списку вибираємо Interface.

package ua.inf.iwanoff.java.third;

public interface LeftSide {
    double f(double x);
}

Клас Solver реалізує статичний метод для розв'язання рівняння:

package ua.inf.iwanoff.java.third;

public class Solver {
    static double solve(double a, double b, double eps, LeftSide ls) {
        double x = (a + b) / 2;
        while (Math.abs(b - a) > eps) {
          if (ls.f(a) * ls.f(x) > 0) {
                a = x;
            }
            else {
                b = x;
            }
            x = (a + b) / 2;
        }
        return x;
    }
}

Клас, який реалізує інтерфейс, містить конкретну реалізацію функції f():

package ua.inf.iwanoff.java.third;

class MyEquation implements LeftSide {
    @Override
    public double f(double x) {
        return x * x - 2;
    }
}

public class InterfaceTest {

    public static void main(String[] args) {
        System.out.println(Solver.solve(0, 2, 0.000001, new MyEquation()));
    }

}

Програму можна модифікувати з урахуванням можливостей Java 8. Метод знаходження кореня можна реалізувати всередині інтерфейсу:

package ua.inf.iwanoff.java.third;

public interface FunctionToSolve {
    double f(double x);

    static double solve(double a, double b, double eps, FunctionToSolve func) {
        double x = (a + b) / 2;
        while (Math.abs(b - a) > eps) {
            if (func.f(a) * func.f(x) > 0) {
                a = x;
            }
            else {
                b = x;
            }
            x = (a + b) / 2;
        }
        return x;
    }
}

Тепер замість Solver.solve() слід викликати FunctionToSolve.solve().

3.4.4 Використання безіменного класу

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

package ua.inf.iwanoff.java.third;

public class SolveUsingAnonymousClass {

    public static void main(String[] args) {
        System.out.println(FunctionToSolve.solve(0, 2, 0.000001, new FunctionToSolve() {
            @Override
            public double f(double x) {
                return x * x - 2;
            }
        }));
    }

}

3.4.5 Використання лямбда-виразів

У задачі розв'язання рівняння методом дихотомії можна визначити ліву частину рівняння лямбда-виразом (замість безіменного класу):

package ua.inf.iwanoff.java.third;

public class SolveUsingLambda {

    public static void main(String[] args) {
        System.out.println(FunctionToSolve.solve(0, 2, 0.000001, x -> x * x - 2));
    }

}

3.4.6 Використання посилань на методи

Попередня задача може бути розв'язана за допомогою посилань на методи. Можна реалізувати функцію як окремий статичний метод:

package ua.inf.iwanoff.java.third;

public class SolveUsingReference {

    public static double f(double x) {
        return x * x - 2;
    }
  
    public static void main(String[] args) {
        System.out.println(FunctionToSolve.solve(0, 2, 0.000001, SolveUsingReference::f));
    }

}

3.5 Ієрархія класів "Країна" та "Перепис населення"

Припустимо, необхідно розширити програму обробки даних про переписи населення так, щоб потенційно можна було використовувати різні структури даних для представлення послідовності переписів населення. Клас для представлення країни не повинен містити даних про послідовність переписів. Цей клас повинен бути абстрактним. Дані про країну – назва, територія, а також послідовність посилань на об'єкт типу "Перепис населення". Необхідно поля для визначити такі функції:

  • функції для доступу до даних;
  • абстрактні функції для доступу до послідовності переписів;
  • перевизначення методу equals() для перевірки еквівалентності об'єктів;
  • перевизначення методу hashCode() для отримання хеш-кодів об'єктів.

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

Своєю чергою, клас, який представляє перепис населення, повинен включати дані про рік перепису, кількість населення та коментарі. Необхідно також перевизначити методи перевірки еквівалентності та отримання хеш-коду. Для забезпечення сортування за збільшенням населення слід реалізувати інтерфейс Comparable і у функції compareTo() забезпечити "природне" порівняння за кількістю населення.

У програмі необхідно реалізувати такі функції:

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

Необхідно додатково реалізувати функції отримання представлення перепису у вигляді рядка, перевірки наявності слів і послідовності літер у коментарях і тестування. Як і в попередній реалізації, ці функції будуть представлені в окремих класах.

У пакеті ua.inf.iwanoff.java.third створюємо новий клас Census. Його код можна скопіювати з пакету ua.inf.iwanoff.java.second, але, на жаль, не можна скористатися раніше створеним класом безпосередньо, оскільки треба додати методи equals(), hashCode() та compareTo(). Код класу буде таким:

package ua.inf.iwanoff.java.third;

import java.util.Objects;

/**
 * Клас відповідає за представлення перепису населення.
 * Перепис населення представлено роком, кількістю населення та коментарем
 */
public class Census implements Comparable<Census> {
    private int year;
    private int population;
    private String comments;

    /**
     * Конструктор ініціалізує об'єкт усталеними значеннями
     */
    public Census() {
    }

    /**
     * Конструктор ініціалізує об'єкт вказаними значеннями
     *
     * @param year рік перепису
     * @param population кількість населення
     * @param comments текст коментаря
     */
    public Census(int year, int population, String comments) {
        this.year = year;
        this.population = population;
        this.comments = comments;
    }

    /**
     * Повертає рік перепису
     * @return рік перепису у вигляді цілого значення
     */
    public int getYear() {
        return year;
    }

    /**
     * Встановлює значення року перепису
     * @param year рік перепису у вигляді цілого значення
     */
    public void setYear(int year) {
        this.year = year;
    }

    /**
     * Повертає кількість населення
     * @return кількість населення у вигляді цілого значення
     */
    public int getPopulation() {
        return population;
    }

    /**
     * Встановлює кількість населення
     * @param population кількість населення у вигляді цілого значення
     */
    public void setPopulation(int population) {
        this.population = population;
    }

    /**
     * Повертає рядок коментаря до перепису
     * @return коментар перепису у вигляді рядка
     */
    public String getComments() {
        return comments;
    }

    /**
     * Встановлює вміст рядка коментаря до перепису
     * @param comments коментар перепису у вигляді рядка
     */
    public void setComments(String comments) {
        this.comments = comments;
    }
    /**
     * Перевіряє, чи еквівалентний цей перепис іншому
     * @param obj перепис, еквівалентність з яким ми перевіряємо
     * @return {@code true}, якщо два переписи однакові
     *         {@code false} в протилежному випадку
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Census c)) {
            return false;
        }
        return c.year == year &&
               c.population == population &&
               c.comments.equals(comments);
    }

    /**
     * Повертає хеш-код перепису
     * @return значення хеш-коду
     */
    @Override
    public int hashCode() {
        return Objects.hash(year, population, comments);
    }

    /**
     * Порівнює поточний об'єкт з отриманим параметром. Повертає від'ємне значення,
     * якщо поточний перепис менше отриманого як параметр,
     * нуль, якщо переписи однакові та додатне значення у протилежному випадку.
     * @param c перепис, з яким ми порівнюємо поточний.
     * @return результат порівняння
     */
    @Override
    public int compareTo(Census c) {
        return Integer.compare(population, c.population);
    }
}

Як видно з наведеного коду, клас Census реалізує інтерфейс Comparable<Census>. Реалізація цього інтерфейсу вимагає додавання методу compareTo(), в якому визначене "природне" порівняння – за кількістю населення.

З попередньої лабораторної роботи копіюємо код класу CensusUtilities:

package ua.inf.iwanoff.java.third;

import java.util.Arrays;

/**
 * Надає статичні методи для пошуку даних в коментарі
 */
public class CensusUtilities {
    /**
     * Перевіряє, чи міститься слово в тексті коментаря до перепису
     * @param census посилання на перепис
     * @param word слово, яке ми шукаємо в коментарі
     * @return {@code true}, якщо слово міститься в тексті коментаря
     *         {@code false} в протилежному випадку
     */
    public static boolean containsWord(Census census, String word) {
        String[] words = census.getComments().split("\\s");
        Arrays.sort(words);
        return Arrays.binarySearch(words, word) >= 0;
    }

    /**
     * Перевіряє, чи міститься підрядок в тексті коментаря
     * @param census посилання на перепис
     * @param substring підрядок, який ми шукаємо в коментарі
     * @return {@code true}, якщо підрядок міститься в тексті коментаря
     *         {@code false} в протилежному випадку
     */
    public static boolean containsSubstring(Census census, String substring) {
        return census.getComments().toUpperCase().contains(substring.toUpperCase());
    }

    /**
     * Допоміжна статична функція додавання посилання на перепис
     * до наданого масиву переписів
     * @param arr масив, до якого додається перепис
     * @param item посилання, яке додається
     * @return оновлений масив переписів
     */
    public static Census[] addToArray(Census[] arr, Census item) {
        Census[] newArr;
        if (arr != null) {
            newArr = new Census[arr.length + 1];
            System.arraycopy(arr, 0, newArr, 0, arr.length);
        }
        else {
            newArr = new Census[1];
        }
        newArr[newArr.length - 1] = item;
        return newArr;
    }
}

Безпосередньо посилатися на код попередньої роботи не слід, оскільки насправді функції отримують посилання на новий клас Census.

Клас AbstractCountry також містить методи equals() і hashCode(). Код класу AbstractCountry буде таким:

package ua.inf.iwanoff.java.third;

import java.util.Arrays;
import java.util.Objects;

/**
 * Абстрактний клас для представлення країни, в якій здійснюється перепис населення.
 * Країна характеризується назвою, площею та послідовністю переписів.
 * Доступ до послідовності переписів представлено абстрактними методами
 */
public abstract class AbstractCountry {
    private String name;
    private double area;

    /**
     * Повертає назву країни
     * @return рядок - назва країни
     */
    public String getName() {
        return name;
    }

    /**
     * Встановлює назву країни
     * @param name рядок - назва країни
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Повертає територію країни
     * @return територія країни у вигляді числа з рухомою крапкою
     */
    public double getArea() {
        return area;
    }

    /**
     * Встановлює територію країни
     * @param area територія країни у вигляді числа з рухомою крапкою
     */
    public void setArea(double area) {
        this.area = area;
    }

    /**
     * Повертає посилання на перепис населення,
     * визачений його індексом в послідовності
     *
     * <p> Похідний клас повинен надавати реалізацію цього методу
     *
     * @param i номер (індекс) перепису
     * @return посилання на перепис населення
     */
    public abstract Census getCensus(int i);

    /**
     * Встановлює посилання на новий перепис всередині позиції послідовностей
     * за вказаним індексом.
     *
     * <p> Похідний клас повинен надавати реалізацію цього методу
     *
     * @param i номер (індекс) позиції в послідовності
     * @param census посилання на новий перепис
     */
    public abstract void setCensus(int i, Census census);

    /**
     * Додає посилання на новий перепис в кінець послідовності
     *
     * <p> Похідний клас повинен надавати реалізацію цього методу
     *
     * @param census посилання на новий перепис
     * @return {@code true}, якщо посилання вдалося додати
     *         {@code false} в протилежному випадку
     */
    public abstract boolean addCensus(Census census);

    /**
     * Створює новий перепис та додає посилання на нього в кінець послідовності.
     * @param year рік перепису
     * @param population кількість населення
     * @param comments текст коментаря
     * @return {@code true}, якщо посилання вдалося додати
     *         {@code false} в протилежному випадку
     */
    public boolean addCensus(int year, int population, String comments) {
        Census census = new Census(year, population, comments);
        return addCensus(census);
    }

    /**
     * Повертає кількість переписів у послідовності.
     *
     * <p> Похідний клас повинен надавати реалізацію цього методу
     *
     * @return кількість переписів
     */
    public abstract int censusesCount();

    /**
     * Очищає послідовність переписів
     *
     * <p> Похідний клас повинен надавати реалізацію цього методу
     */
    public abstract void clearCensuses();

    /**
     * Переписує дані з масиву переписів у послідовність
     *
     * <p> Похідний клас повинен надавати реалізацію цього методу
     *
     * @param censuses довільний масив переписів
     */
    public abstract void setCensuses(Census[] censuses);


    /**
     * Повертає масив переписів з послідовності
     *
     * <p> Похідний клас повинен надавати реалізацію цього методу
     *
     * @return сформаваний масив посилань на переписи
     */
    public abstract Census[] getCensuses();

    /**
     * Перевіряє, чи еквівалентна ця країна іншій
     * @param obj країна, еквівалентність з якою ми перевіряємо
     * @return {@code true}, якщо дві країни однакові
     *      *  {@code false} в протилежному випадку
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof AbstractCountry c)) {
            return false;
        }
        if (!getName().equals(c.getName()) || getArea() != c.getArea()) {
            return false;
        }
        return Arrays.equals(getCensuses(), c.getCensuses());
    }

    /**
     * Повертає хеш-код країни
     * @return значення хеш-коду
     */
    @Override
    public int hashCode() {
        return Objects.hash(name, area, Arrays.hashCode(getCensuses()));
    }
}

У похідному класі CountryWithArray використовуємо масив для представлення послідовності переписів:

package ua.inf.iwanoff.java.third;

/**
 * Клас для представлення країни, в якій здійснюється перепис населення.
 * Дані про переписи представлені масивом
 */
public class CountryWithArray extends AbstractCountry {
    private Census[] censuses;

    /**
     * Повертає посилання на перепис населення,
     * визачений його індексом в послідовності
     * @param i номер (індекс) перепису
     * @return посилання на перепис населення
     */
    @Override
    public Census getCensus(int i) {
        return censuses[i];
    }

    /**
     * Встановлює посилання на новий перепис всередині позиції послідовностей
     * за вказаним індексом.
     * @param i номер (індекс) позиції в послідовності
     * @param census посилання на новий перепис
     */
    @Override
    public void setCensus(int i, Census census) {
        censuses[i] = census;
    }

    /**
     * Додає посилання на новий перепис в кінець послідовності
     * @param census посилання на новий перепис
     * @return {@code true}, якщо посилання вдалося додати
     *         {@code false} в протилежному випадку
     */
    @Override
    public boolean addCensus(Census census) {
        if (getCensuses() != null) {
            for (Census c : getCensuses()) {
                if (c.equals(census)) {
                    return false;
                }
            }
        }
        setCensuses(CensusUtilities.addToArray(getCensuses(), census));
        return true;
    }

    /**
     * Повертає кількість переписів у послідовності
     * @return кількість переписів
     */
    @Override
    public int censusesCount() {
        return censuses.length;
    }

    /**
     * Очищує послідовність переписів
     */
    @Override
    public void clearCensuses() {
        censuses = null;
    }

    /**
     * Повертає масив переписів з послідовності
     * @return сформаваний масив посилань на переписи
     */
    @Override
    public Census[] getCensuses() {
        return censuses;
    }

    /**
     * Переписує дані з масиву переписів у послідовність
     * @param censuses довільний масив переписів
     */
    @Override
    public void setCensuses(Census[] censuses) {
        this.censuses = censuses;
    }
}

В іншому похідному класі реалізуємо найпростіший варіант однобічно зв'язаного списку для зберігання послідовності переписів:

package ua.inf.iwanoff.java.third;

/**
 * Клас для представлення країни, в якій здійснюється перепис населення.
 * Дані про переписи представлені однобічно зв'язаним списком
 */
public class CountryWithLinkedList extends AbstractCountry {
    /**
     * Допоміжний клас, який представляє вузол зв'язаного списку
     */
    private class Node {
        Census census;
        Node next;
    }

    private int size = 0;     // кількість переписів
    private Node head = null; // посилання на початок списку

    /**
     * Повертає посилання на перепис населення,
     * визачений його індексом в послідовності
     * @param i номер (індекс) перепису
     * @return посилання на перепис населення
     */
    @Override
    public Census getCensus(int i) {
        if (i < 0 || i >= size || head == null) {
            return null;
        }
        Node node = head;
        for (int j = 0; j < i; j++) {
            node = node.next;
        }
        return node.census;
    }

    /**
     * Встановлює посилання на новий перепис всередині позиції послідовностей
     * за вказаним індексом.
     * @param i номер (індекс) позиції в послідовності
     * @param census посилання на новий перепис
     */
    @Override
    public void setCensus(int i, Census census) {
        if (i < 0 || i >= size || head == null) {
            return;
        }
        Node node = head;
        for (int j = 0; j < i; j++) {
            node = node.next;
        }
        node.census = census;
    }

    /**
     * Додає посилання на новий перепис в кінець послідовності
     * @param census посилання на новий перепис
     * @return {@code true}, якщо посилання вдалося додати
     *         {@code false} в протилежному випадку
     */
    @Override
    public boolean addCensus(Census census) {
        Node newNode = new Node();
        newNode.census = census;
        newNode.next = null;
        if (head == null) {
            head = newNode;
            size = 1;
            return true;
        }
        Node node = head;
        Node previous = null;
        for (int i = 0; i < size; i++) {
            if (node.census.equals(census)) {
                return false;
            }
            previous = node;
            node = node.next;
        }
        node = newNode;
        previous.next = node;
        size++;
        return true;
    }

    /**
     * Повертає кількість переписів у послідовності
     * @return кількість переписів
     */
    @Override
    public int censusesCount() {
        return size;
    }

    /**
     * Очищує послідовність переписів
     */
    @Override
    public void clearCensuses() {
        head = null;
        size = 0;
    }

    /**
     * Повертає масив переписів з послідовності
     * @return сформаваний масив посилань на переписи
     */
    @Override
    public void setCensuses(Census[] censuses) {
        clearCensuses();
        for (Census census : censuses) {
            addCensus(census);
        }
    }

    /**
     * Переписує дані з масиву переписів у послідовність
     * @param censuses довільний масив переписів
     */
    @Override
    public Census[] getCensuses() {
        Census[] censuses = new Census[size];
        Node node = head;
        for (int i = 0; i < size; i++) {
            censuses[i] = node.census;
            node = node.next;
        }
        return censuses;
    }
}

Функції сортування додаємо до коду раніше створено класу, який теж слід скопіювати у новий пакет:

package ua.inf.iwanoff.java.third;

import java.util.Arrays;
import java.util.Comparator;

/**
 * Надає статичні методи для пошуку і сортування переписів
 */
public class CountryUtilities {

    /**
     * Повертає густоту населення для вказаного року
     * @param country посилання на країну
     * @param year рік (наприклад, 1959, 1979, 1989 тощо)
     * @return густота населення для вказаного року
     */
    public static double density(AbstractCountry country, int year) {
        for (int i = 0; i < country.censusesCount(); i++) {
            if (year == country.getCensus(i).getYear()) {
                return country.getCensus(i).getPopulation() / country.getArea();
            }
        }
        return 0;
    }
    /**
     * Знаходить і повертає рік з максимальним населенням
     * @param country посилання на країну
     * @return рік з максимальним населенням
     */
    public static int maxYear(AbstractCountry country) {
        Census census = country.getCensus(0);
        for (int i = 1; i < country.censusesCount(); i++) {
            if (census.getPopulation() < country.getCensus(i).getPopulation()) {
                census = country.getCensus(i);
            }
        }
        return census.getYear();
    }

    /**
     * Створює та повертає масив переписів зі вказаним словом в коментарях
     * @param country посилання на країну
     * @param word слово, яке відшукується
     * @return масив переписів зі вказаним словом в коментарях
     */
    public static Census[] findWord(AbstractCountry country, String word) {
        Census[] result = null;
        for (Census census : country.getCensuses()) {
            if (CensusUtilities.containsWord(census, word)) {
                result = CensusUtilities.addToArray(result, census);
            }
        }
        return result;
    }
    /**
     * Здійснює сортування послідовності переписів за кількістю населення
     *
     * @param country посилання на країну
     */
    public static void sortByPopulation(AbstractCountry country) {
        Census[] censuses = country.getCensuses();
        Arrays.sort(censuses);
        country.setCensuses(censuses);
    }

    /**
     * Здійснює сортування послідовності переписів за алфавітом коментарів
     *
     * @param country посилання на країну
     */
    public static void sortByComments(AbstractCountry country) {
        Census[] censuses = country.getCensuses();
        Arrays.sort(censuses, Comparator.comparing(Census::getComments));
        country.setCensuses(censuses);
    }
}

В окремому класі містяться засоби отримання представлення даних у вигляді рядків:

package ua.inf.iwanoff.java.third;

/**
 * Клас, який дозволяє отримувати представлення
 * у вигляді рядків різних об'єктів застосунку
 */
public class StringRepresentations {
    /**
     * Надає подання перепису у вигляді рядка
     *
     * @param census посилання на перепис
     * @return подання перепису у вигляді рядка
     */
    public static String toString(Census census)
    {
        return "Перепис " + census.getYear() + " року. Населення: " + census.getPopulation() +
               ". Коментар: " + census.getComments();
    }

    /**
     * Надає подання країни у вигляді рядка
     *
     * @param country посилання на країну
     * @return подання країни у вигляді рядка
     */
    public static String toString(AbstractCountry country) {
        StringBuilder result = new StringBuilder(country.getName() + ". Територія: " + country.getArea() + " кв. км.");
        for (int i = 0; i < country.censusesCount(); i++) {
            result.append("\n").append(toString(country.getCensus(i)));
        }
        return result + "";
    }
}

Демонстрація можливостей класів реалізується у класі CountryDemo:

package ua.inf.iwanoff.java.third;

import static ua.inf.iwanoff.java.third.CountryUtilities.*;

/**
 * Програма тестування можливості роботи з країною
 */
public class CountryDemo {

    /**
     * Допоміжна функція заповнення даними об'єкта "Країна"
     * @param country посилання на країну
     * @return посилання на новий об'єкт "Країна"
     */
    public static AbstractCountry setCountryData(AbstractCountry country) {
        country.setName("Україна");
        country.setArea(603628);
        // Додавання переписів:
        System.out.println(country.addCensus(1959, 41869000, "Перший перепис після другої світової війни"));
        System.out.println(country.addCensus(1970, 47126500, "Нас побільшало"));
        System.out.println(country.addCensus(1979, 49754600, "Просто перепис"));
        System.out.println(country.addCensus(1989, 51706700, "Останній перепис радянських часів"));
        System.out.println(country.addCensus(2001, 48475100, "Перший перепис у незалежній Україні"));
        // Спроба додати перепис двічі:
        System.out.println(country.addCensus(1959, 41869000, "Перший перепис після другої світової війни"));
        return country;
    }

    /**
     * Виводить на екран дані про переписи, які містять певне слово в коментарях
     * @param country посилання на країну
     * @param word слово, яке відшукується
     */
    public static void printWord(AbstractCountry country, String word) {
        Census[] result = findWord(country, word);
        if (result == null) {
            System.out.println("Слово \"" + word + "\" не міститься в коментарях.");
        }
        else {
            System.out.println("Слово \"" + word + "\" міститься в коментарях:");
            for (Census census : result) {
                System.out.println(StringRepresentations.toString(census));
            }
        }
    }

    /**
     * Здійснює тестування методів пошуку
     * @param country посилання на країну
     */
    public static void testSearch(AbstractCountry country) {
        System.out.println("Щільність населення у 1979 році: " + density(country, 1979));
        System.out.println("Рік з найбільшим населенням: " + maxYear(country) + "\n");
        printWord(country, "перепис");
        printWord(country, "запис");
    }

    /**
     * Здійснює тестування методів сортування
     * @param country посилання на країну
     */
    public static void testSorting(AbstractCountry country) {
        sortByPopulation(country);
        System.out.println("\nСортування за кількістю населення:");
        System.out.println(StringRepresentations.toString(country));

        sortByComments(country);
        System.out.println("\nСортування за алфавітом коментарів:");
        System.out.println(StringRepresentations.toString(country));
    }

    /**
     * Демонстрація роботи програми
     * @param args аргументи командного рядка (не використовуються)
     */
    public static void main(String[] args) {
        AbstractCountry country = setCountryData(new CountryWithArray());
        testSearch(country);
        testSorting(country);
        System.out.println("----------------------------------------");
        country = setCountryData(new CountryWithLinkedList());
        testSearch(country);
        testSorting(country);
    }
}

Під час виконання програми для кожного з варіантів реалізації в консольне вікно спочатку виводяться результати функцій addCensus() – п'ять разів true і один раз false.

4 Вправи для контролю

  1. Створити ієрархію класів Книга та Підручник. Реалізувати конструктори та функції доступу. Перекрити функцію toString(). У функції main() створити масив, який містить елементи різних типів. Вивести елементи на екран.
  2. Створити ієрархію класів Кінофільм і Серіал. Реалізувати конструктори та функції доступу. Перекрити функцію toString(). У функції main() створити масив, який містить елементи різних типів. Вивести елементи на екран.
  3. Створити ієрархію класів Місто та Столиця. Реалізувати конструктори та функції доступу. Перекрити функцію toString(). У функції main() створити масив, який містить елементи різних типів. Вивести елементи на екран.
  4. Створити клас для представлення іменованої матриці з полем типу String – іменем матриці й полем, що представляє двовимірний масив. Реалізувати методи клонування, перевірки еквівалентності, генерації хеш-коду та отримання подання у вигляді рядка. Здійснити тестування.

5 Контрольні запитання

  1. У чому полягає зміст успадкування?
  2. У чому є сенс наявності спільного базового класу?
  3. Які елементи базового класу не успадковуються?
  4. Як здійснити ініціалізацію базового класу?
  5. Як викликати однойменний метод базового класу з похідного?
  6. Де і для чого можна застосовувати ключове слово super?
  7. Як перекрити метод з модифікатором final?
  8. Чи допускається множинне успадкування класів?
  9. Чи можна неявно приводити посилання на базовий клас до посилання на похідний клас?
  10. У чому є сенс застосування анотацій?
  11. Які можливості надає використання поліморфізму?
  12. Чим віртуальна функція відрізняється від невіртуальної?
  13. Як у Java указати, що функція віртуальна?
  14. Чи можна в класах, описаних як final, створювати віртуальні функції?
  15. Чому функції з модифікатором private не є віртуальними?
  16. Чи можна створити абстрактний клас без абстрактних методів?
  17. Чи можуть абстрактні класи містити неабстрактні методи?
  18. У чому перевага інтерфейсів у порівнянні з абстрактними класами?
  19. Чи можуть інтерфейси містити поля?
  20. Чи допускається множинне успадкування інтерфейсів?
  21. Яким вимогам повинен відповідати клас, який реалізує інтерфейс?
  22. Чи може клас реалізовувати кілька інтерфейсів?
  23. Яким вимогам повинен задовольняти об'єкт, щоб масив таких об'єктів можна було сортувати без визначення ознаки сортування?
  24. Як визначити спеціальне правило для сортування елементів масиву?
  25. Як звернутися до локального класу ззовні блоку?
  26. Чи може в одному файлі з сирцевим кодом бути визначено більш одного відкритого класу?
  27. Чи можна створювати об'єкт нестатичного внутрішнього класу, не створюючи об'єкта зовнішнього класу?
  28. Чи можуть нестатичні внутрішні класи містити статичні елементи?
  29. Чим відрізняються статичні вкладені класи від внутрішніх?
  30. Чи можуть статичні вкладені класи містити нестатичні елементи?
  31. Чи можна створювати класи усередині інтерфейсів?
  32. Чи є безіменний клас статичним?
  33. Чому не можна створити явний конструктор безіменного класу?
  34. Що таке лямбда-вираз?
  35. Що таке функціональний інтерфейс?
  36. Які переваги надають лямбда-вирази?
  37. Для чого використовуються посилання на методи?
  38. У чому полягає процес клонування об'єктів?
  39. Для чого використовується перевизначення функції equals()?
  40. Для чого використовується перевизначення функції hashCode()?

 

up