Лабораторна робота 3
Використання успадкування та поліморфізму в Java
1 Завдання на лабораторну роботу
1.1 Індивідуальне завдання
Створити ієрархію класів, які представляють сутності відповідно до індивідуального завдання. Варіант завдання, який слід реалізувати у програмі, визначається залежно від номера студента у списку групи.
Слід окремо здійснити тестування кожного з класів, після чого окремими методами першого класу реалізувати
основне завдання. У функції main()
створити необхідний об'єкт та викликати
для нього методи, які реалізують основне завдання. Вивести результати у консольне вікно.
№№ | Перша сутність | Друга сутність | Основне завдання: знайти та вивести такі дані |
||
---|---|---|---|---|---|
Сутність | Обов'язкові поля | Сутність | Обов'язкові поля | ||
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 | Телефонний номер | Номер, оператор | Дзвінки | Дата, кількість хвилин розмов, кошти, що використано на розмови | Середня платня в день за період; кількість днів, коли вартість хвилини розмови
перевищувала задане значення; Дні, коли кількість хвилин розмов була парна |
Клас, який представляє другу сутність індивідуального завдання, повинен містити методи
доступу, перевизначення функцій toString()
, equals()
та hashCode()
,
а також реалізацію функцій, визначених індивідуальним завданням (за необхідністю). Цей клас також повинен
реалізовувати інтерфейс Comparable
для
природного порівняння об'єктів під час сортування за однією з ознак.
Базовий абстрактний клас, який представляє першу з сутностей індивідуального завдання (умовно AbstractFirstEntity
),
не повинен містити даних про послідовність елементів типу другого класу. Слід визначити такі функції:
- функції для доступу до даних;
- абстрактні функції для доступу до послідовності елементів типу другого класу;
- перевизначення функції
toString()
для виведення даних про об'єкти; - перевизначення методу
equals()
для перевірки еквівалентності об'єктів; - перевизначення методу
hashCode()
для отримання хеш-кодів об'єктів; - реалізацію методів пошуку за визначеними ознаками;
- реалізацію функції сортування елементів послідовності за першою ознакою індивідуального завдання методом бульбашки;
- реалізацію функції сортування елементів послідовності за другою ознакою індивідуального завдання методом сортування включенням;
- реалізацію методу тестування функціональності класів.
Ознаки сортування визначаються залежно від номера студента у списку групи. Функції пошуку повинні
повертати
масиви об'єктів (або null
, якщо пошук не дав результатів). Індивідуальні ознаки
сортування вказані в таблиці:
№№
| Перша ознака | Друга ознака |
---|---|---|
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
|
За зменшенням кількості хвилин розмов | За збільшенням кількості коштів, що використано на розмови |
Від створеного абстрактного класу необхідно створити похідний клас (умовно FirstEntityWithArray
),
який повинен містити поля конкретних типів, зокрема, у класі FirstEntityWithArray
послідовність
елементів другої сутності повинна бути представлена у вигляді масиву.
Створити ще один клас (умовно FirstEntityWithSorting
), похідний від попередньо створеного класу
FirstEntityWithArray
.
Цей клас повинен перевизначати методи сортування – замість сортування бульбашкою і включенням застосувати
стандартну функцію Arrays.sort()
. Одне з сортувань повинне бути забезпечене реалізацією інтерфейсу
Comparable
для
сутності, об'єкти якої зберігаються в масиві. Друге сортування забезпечується створенням окремого класу, який
реалізує інтерфейс Comparator
. Рекомендовано скористатися лямбда-виразом.
Тестування програми повинно включати основне завдання, а також сортування за певними критеріями.
Примітка: слід створити класи зі змістовними іменами, які відображають фізичну сутність
індивідуального завдання, а не FirstEntity
, SecondEntity
тощо.
Слід додати коментарі Javadoc до сирцевого коду.
1.2 Ієрархія класів
Реалізувати класи "Людина", "Громадянин", "Студент", "Співробітник". Створити масив посилань на різні об'єкти ієрархії. Для кожного об'єкта вивести на екран рядок даних про нього.
Примітка: слід створити класи зі змістовними іменами.
1.3 Мінімум функції
Реалізувати програму, що дозволяє знайти мінімум деякої функції на визначеному інтервалі. Алгоритм знаходження мінімуму полягає в послідовному переборі з певним кроком точок інтервалу і порівнянні значень функції в поточній точці з раніше знайденим мінімумом.
Реалізувати п'ять варіантів розв'язання:
- використання абстрактного та похідних класів
- опис інтерфейсу, створення класу, який використовує інтерфейс як тип параметра функції знаходження мінімуму, створення окремих класів, які реалізують інтерфейс
- використання попередньо описаного інтерфейсу і безіменних класів
- використання лямбда-виразів
- використання посилань на методи.
Перевірити роботу програми на двох різних функціях.
1.4 Реалізація масиву точок через масиви дійсних чисел
Реалізувати функціональність абстрактного класу AbstractArrayOfPoints
, наведеного в прикладі
3.2,
у два способи:
- через використання двовимірного масиву дійсних чисел: кожен рядок масиву має відповідати точці;
- через використання одновимірного масиву дійсних чисел: кожна пара чисел у масиві має відповідати точці.
Здійснити тестування створених класів.
Примітка: не слід вносити зміни у код класу AbstractArrayOfPoints
(за винятком
імені пакета і реалізації методу sortByY()
).
1.5 Реалізація інтерфейсу Comparable
Створити клас Circle
, який реалізує інтерфейс Comparable
. Більшим вважається
коло з більшим радіусом. Здійснити сортування масиву об'єктів типу Circle
за допомогою функції
Arrays.sort()
.
1.6 Реалізація інтерфейсу Comparator
Створити клас Triangle
. Трикутник визначати довжинами сторін.
Площа трикутника в цьому випадку може бути обчислена за формулою Герона:
де 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 DerivedClass extends BaseClass { // тіло класу }
Функції похідного класу мають доступ до елементів, описаних у як public
і protected
(захищені). Члени класу, оголошені як захищені, можуть використовуватися класами-нащадками, а також у
межах
пакета. Закриті (приватні, private
) члени класу недоступні навіть для його нащадків.
Усі класи Java безпосередньо чи опосереднено походять від класу java.lang.Object
. Цей клас
надає набір корисних методів, таких як toString()
для отримання даних будь-якого об'єкта у
вигляді рядка тощо. Базовий клас Object
не вказують явно.
Клас успадковує всі елементи базового класу, крім конструкторів. До початку виконання конструктора похідного класу викликається конструктор базового класу (усталений конструктор, якщо явно не викликано іншого).
Ключове слово super
використовують для доступу до елементів базового класу з
похідного класу, зокрема:
- для виклику перекритого методу базового класу;
- для передачі параметрів конструктору базового класу.
Наприклад:
class BaseClass { int i, j; BaseClass(int i, int j) { this.i = i; this.j = j; } } class DerivedClass extends BaseClass { int k; DerivedClass(int i, int j, int k) { super(i, j); this.k = k; } }
Доступ до базового класу з використанням super
дозволений тільки в конструкторах і
нестатичних методах.
Класи можуть бути визначені з модифікатором final
(фінальний). Фінальні класи не
можуть
використовуватися як базові. Методи з модифікатором final
не можуть бути
перевизначені.
Наприклад:
final class A { void f() { } } class B { final void g() { } } class C extends A { // Помилка! Не можна успадкувати від A } class D extends B { void g() { } // Помилка! g() не можна перекрити }
Посилання на похідний клас неявно приводяться до посилання на базовий клас. Об'єкти похідних класів завжди можна використовувати там, де потрібен об'єкт базового класу.
class Base { static void f(Base b) { } } class Derived extends Base { public static void main(String[] args) { Base b; b = new Derived(); // Неявне приведення Derived d = new Derived(); f(d); // Неявне приведення } }
Зворотне приведення необхідно робити явно:
Base b = new Base(); Derived d = (Derived) b;
У Java є ключове слово instanceof
, яке дозволяє перевірити, чи є об'єкт екземпляром
певного
типу (або похідних типів). Вираз
об'єкт instanceof клас
повертає значення типу boolean
, яке може бути використане для перевірки, чи можна
викликати
метод цього класу:
if (x instanceof SomeClass) ((SomeClass)x).someMethod();
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() { 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 MyClass { int i = 10; @Override public String toString() { return "i = " + i; } public static void main(String[] args) { MyClass mc = new MyClass(); 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 { void draw() { . . . } } class Rectangle extends Shape { void draw() { . . . } }
Абстрактні методи аналогічні суто віртуальним функціям у C++.
Від абстрактного класу не вимагають обов'язкової наявності абстрактних методів. Але кожен клас, у якому є
хоч
один абстрактний метод, чи хоча б один абстрактний метод базового класу не був визначений, повинен бути
оголошений як абстрактний (з використанням ключового слова abstract
).
2.5 Загальні відомості про інтерфейси. Упорядкування об'єктів
У Java використовується поняття інтерфейсів. Інтерфейс може розглядатися як чисто абстрактний клас, але на відміну від абстрактних класів інтерфейс ніколи не містить даних, тільки методи. Ці методи усталено вважаються публічними:
interface SomeFunctions { void f(); int g(int x); }
Кожен клас може бути створений тільки від одного базового класу, але при цьому реалізовувати один чи кілька
інтерфейсів. Клас, що реалізує інтерфейс, повинен забезпечити реалізацію всіх методів, оголошених в інтерфейсі.
В іншому випадку такий клас буде абстрактним і повинен бути оголошений зі специфікатором abstract
.
Інтерфейси формально можуть містити поля, але вони є фінальними та статичними (константами часу компіляції). Вони повинні бути ініціалізовані під час створення. Інтерфейси не можуть містити конструкторів, оскільки немає ніяких даних окрім статичних констант.
Для того, щоб указати, що клас реалізує інтерфейс, ім'я інтерфейсу вказують у списку реалізованих інтерфейсів.
Такий список розташовують у заголовку класу після ключового слова implements
. Методи,
визначені в інтерфейсі, є абстрактними та відкритими. У класі, що реалізує інтерфейс, такі методи повинні
бути оголошені як public
:
interface SomeFunctions { void f(); int g(int x); } class SomeClass implements SomeFunctions { @Override public void f() { } @Override public int g(int x) { return x; } }
Примітка: використання анотації @Override
є бажаним, але не обов'язковим.
Інтерфейс може мати кілька базових інтерфейсів. Множинне успадкування інтерфейсів є безпечним з точки зору дублювання даних і конфліктів імен:
interface SomeFunctions { void f(); int g(int x); } interface AnotherFunction { void h(int z); } interface AllFunctions extends SomeFunctions, AnotherFunction { int g(int x); // оголошення може повторюватися }
Тепер клас, який реалізує інтерфейс AllFunctions
, повинен визначати три функції:
class Implementation implements AllFunctions { @Override public void f() { } @Override // Одна реалізація використовується для базового і похідного інтерфейсів: public int g(int x) { return x; } @Override public void h(int z) { } }
Клас може реалізувати кілька інтерфейсів. Це – більш розповсюджений шлях, ніж створення похідного інтерфейсу:
class AnotherImplementation implements SomeFunctions, AnotherFunction { @Override public void f() { } @Override public int g(int x) { return x; } @Override public void h(int z) { } }
Дуже часто в програмі створюють посилання на інтерфейс, яке ініціалізують об'єктом класу. Такий підхід є належною практикою, оскільки він дозволяє легко замінити одну реалізацію інтерфейсу іншою.
Інтерфейси не походять від класу java.lang.Object
. Не можна створювати новий об'єкт типу
інтерфейсу. Навіть для порожнього інтерфейсу треба створити клас, який його реалізує:
interface Empty { } class EmptyImplementation implements Empty { } ... public static void main(String[] args) { Empty empty1 = new Empty(); // Помилка Empty empty2 = new EmptyImplementation(); // Коректне створення об'єкта }
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
(Comparable
,
Runnable
тощо). Але це правило дуже часто порушується навіть для стандартних інтерфейсів.
У 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) // Опис функції у Java 2 public static void sort(Object[] a, int fromIndex, int toIndex, Comparator c) // Опис функції у Java 2
Інтерфейс містить опис методу 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
.
Локальні класи створюють всередині блоків. Існує також спеціальний різновид локальних класів – безіменні класи.
Окрема категорія – статичні вкладені класи, використання яких аналогічне вкладеним класам C++ і C#.
2.6.2 Внутрішні класи
Нестатичні вкладені класи називають також внутрішніми. Головною відмінністю внутрішніх класів у Java є те, що об'єкти цих класів отримують посилання на об'єкт обхопного класу. З цього факту випливає два важливих висновки:
- об'єкти внутрішніх класів мають прямий доступ до даних об'єкта обхопного класу;
- для створення об'єкта внутрішнього класу обов'язково мати в наявності об'єкт обхопного класу.
У зв'язку з цим в Java запропонований спеціальний механізм створення об'єктів внутрішніх класів. Цей механізм проілюстрований на наведеному нижче прикладі.
class Outer { int k = 100; class Inner { void show() { System.out.println(k); } } } public class Test { public static void main(String[] args) { Outer outer = new Outer(); Outer.Inner inner = outer.new Inner(); inner.show(); } }
Нестатичні внутрішні класи не можуть містити статичних елементів.
Слід пам'ятати, що об'єкт внутрішнього класу автоматично не створюється. Створення об'єкта може бути
передбачене в конструкторі чи у будь-якому методі обхопного класу, а також поза ним (якщо цей клас не
оголошений
як 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.3 Локальні й безіменні класи
До внутрішніх класів також відносяться локальні. До таких класів не можна звернутися ззовні блоку, у якому вони визначені. Локальні класи найчастіше поміщають у тіло функції:
void f() { class Local { int j; } Local l = new Local(); l.j = 100; System.out.println(l.j); }
Можна також розміщати локальні класи всередині окремих блоків.
Безіменний клас може реалізовувати певний інтерфейс, перекривати абстрактні функції базового класу чи розширювати його. Для створення об'єкта безіменного класу здійснюється виклик конструктора базового класу, або вказується ім'я інтерфейсу з круглими дужками, після чого розташовують тіло безіменного класу:
new Object() { // Додавання нового методу: void hello() { System.out.println("Привіт!"); } }.hello(); System.out.println(new Object() { // Перевизначення методу: @Override public String toString() { return "Це безіменний клас."; } });
Безіменні класи не можуть бути абстрактними. Безіменний клас завжди є внутрішнім класом; він не може бути
статичним. Безіменні класи автоматично є фінальними (final
). У наведеному нижче
прикладі безіменний клас створюється для визначення способу сортування масиву рядків:
void sortByABC(String[] a) { Arrays.sort(a, new Comparator<String>() { public int compare(String s1, String s2) { return (s1).compareTo(s2); } }); }
У безіменних класів не може бути явних конструкторів. Разом з тим, завжди створюється усталений безіменний конструктор. Якщо в базового класу немає конструктора без параметрів, необхідні параметри конструктора вказуються в дужках під час створення об'єкта:
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.6.4 Статичні вкладені класи
Статичні вкладені класи мають доступ тільки до статичних елементів обхопних класів. Об'єкти таких класів можуть бути створені без створення об'єктів обхопних класів:
class Outer { int k = 100; static int m = 200; static class Inner { void show() { // k недоступно System.out.println(m); } } } public class Test { public static void main(String[] args) { Outer.Inner inner = new Outer.Inner(); inner.show(); } }
Статичні вкладені класи можуть містити свої статичні елементи, у тому числі свої вкладені статичні й нестатичні класи.
Класи можна створювати всередині інтерфейсів. Такі класи автоматично є статичними. Усередині класів також можна створювати інтерфейси, що є також статичними.
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 { public 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 { public 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 { public 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
.
Інтерфейс | Опис |
---|---|
BiConsumer<T,U> |
Представляє операцію, яка приймає два вхідних аргументи та не повертає результату |
BiFunction<T,U,R> |
Представляє функцію, яка приймає два аргументи й повертає результат |
BinaryOperator<T> |
Представляє операцію над двома операндами одного типу, виробляючи результат того ж типу, що й операнди |
BiPredicate<T,U> |
Представляє предикат (функцію з результатом типу boolean ) з
двома аргументами |
BooleanSupplier |
Представляє "постачальника" результату типу boolean |
Consumer<T> |
Представляє операцію, яка приймає один аргумент і не повертає результату |
DoubleBinaryOperator |
Представляє операцію над двома аргументами типу double , яка
повертає результат типу double |
DoubleConsumer |
Представляє операцію, яка приймає один аргумент типу
double і не повертає результату |
DoubleFunction<R> |
Представляє операцію, яка приймає один аргумент типу
double і повертає результат |
DoublePredicate |
Представляє предикат (функцію з результатом типу boolean ) з
одним аргументом типу double |
DoubleSupplier |
Представляє "постачальника" результату типу double |
DoubleToIntFunction |
Представляє операцію, яка приймає один аргумент типу double і
повертає результат типу int |
DoubleToLongFunction |
Представляє операцію, яка приймає один аргумент типу double і
повертає результат типу long |
DoubleUnaryOperator |
Представляє операцію, яка приймає один аргумент типу double і
повертає результат типу double |
Function<T,R> |
Представляє операцію, яка приймає один аргумент і повертає результат |
IntBinaryOperator |
Представляє операцію над двома аргументами типу int , яка
повертає результат типу int |
IntConsumer |
Представляє операцію, яка приймає один аргумент типу
int і не повертає результату |
IntFunction<R> |
Представляє операцію, яка приймає один аргумент типу int і
повертає результат |
IntPredicate |
Представляє предикат (функцію з результатом типу boolean ) з
одним аргументом типу
int
|
IntSupplier |
Представляє "постачальника" результату типу int |
IntToDoubleFunction |
Представляє операцію, яка приймає один аргумент типу int і
повертає результат типу double |
IntToLongFunction |
Представляє операцію, яка приймає один аргумент типу int і
повертає результат типу long |
IntUnaryOperator |
Представляє операцію, яка приймає один аргумент типу
int і повертає результат типу
int |
LongBinaryOperator |
Представляє операцію над двома аргументами типу long , що
повертає
результат типу long |
LongConsumer |
Представляє операцію, яка приймає один аргумент типу
long і не повертає результату |
LongFunction<R> |
Представляє операцію, яка приймає один аргумент типу
long і повертає результат |
LongPredicate |
Представляє предикат (функцію з результатом типу boolean ) з
одним аргументом типу long |
LongSupplier |
Представляє "постачальника" результату типу long |
LongToDoubleFunction |
Представляє операцію, яка приймає один аргумент типу long і повертає
результат типу double
|
LongToIntFunction |
Представляє операцію, яка приймає один аргумент типу
long і повертає результат типу int |
LongUnaryOperator |
Представляє операцію, яка приймає один аргумент типуlong і повертає
результат типу long |
ObjDoubleConsumer<T> |
Представляє функцію, яка приймає аргументи типів T і
double і не повертає результату |
ObjIntConsumer<T> |
Представляє функцію, яка приймає аргументи типів T і
int і не повертає результату
|
ObjLongConsumer<T> |
Представляє функцію, яка приймає аргументи типів T і long
і не повертає результату |
Predicate<T> |
Представляє предикат (функцію з результатом типу boolean ) з
одним аргументом |
Supplier<T> |
Представляє "постачальника" результату |
ToDoubleBiFunction<T,U> |
Представляє функцію, яка приймає два аргументи й продукує результат типу
double |
ToDoubleFunction<T> |
Представляє функцію, яка продукує результат типу double |
ToIntBiFunction<T,U> |
Представляє функцію, яка приймає два аргументи й продукує результат типу
int |
ToIntFunction<T> |
Представляє функцію, яка продукує результат типу int |
ToLongBiFunction<T,U> |
Представляє функцію, яка приймає два аргументи й продукує результат типу
long |
ToLongFunction<T> |
Представляє функцію, яка продукує результат типу long |
UnaryOperator<T> |
Представляє операцію над одним операндом, яка повертає результат того ж типу, що й операнд |
Крім перелічених, функціональними інтерфейсами також є узагальнений інтерфейс Comparator
,
інтерфейс Runnable
, який використовують у багатопотоковому програмуванні, а також багато
інших.
2.8.4 Композиція лямбда-виразів
Можна здійснювати композицію лямбда-виразів (використовувати лямбда-вирази як параметри). З цією метою
інтерфейси пакету java.util.function
надають методи з усталеною реалізацією, що
забезпечують виконання деякої функції, переданої як параметр до або після даного методу. Зокрема, в інтерфейсі
Function
визначені такі методи:
// Виконується функція before, а потім функція, що викликає: Function compose(Function before) // Функція after виконується після функції, що викликає: Function andThen(Function after)
Використання цих методів та їх відмінність розглянемо на такому прикладі. Є клас зі статичною функцією
calc()
,
що приймає функціональний інтерфейс і аргумент типу Double
. Можна здійснити композицію
лямбда-виразів:
package ua.inf.iwanoff.java.third; import java.util.function.Function; public class ComposeDemo { public static Double calc(Function<Double , Double> operator, Double x) { return operator.apply(x); } public static void main(String[] args) { Function<Double , Double> addTwo = x -> x + 2; Function<Double , Double> 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
, що включає два поля типу
String
– name
і 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);
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()
слід перевизначати.
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 Визначення завдання й створення абстрактного класу
Припустимо, необхідно розробити клас для представлення масиву точок. Кожна точка представлена двома числами
типу double
– x
і 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 Використання інтерфейсів з усталеною реалізацією методів
Припустимо, необхідно знайти корінь рівняння методом дотичних (Ньютона). Цей метод передбачає використання першої та другої похідної функції для знаходження кореня. Наближене значення першої похідної будь-якої функції можна знайти за формулою
Чим менше dx, тим точніше буде знайдена похідна. Другу похідну можна знайти як похідну першої похідної.
Алгоритм полягає в такому: на заданому відрізку пошуку знаходимо початкове наближення. Це буде початок відрізка (якщо знак функції та другої похідної в цій точці збігаються) або кінець відрізка (в іншому випадку). Далі обчислюємо наступні наближення за такою формулою:
Описуємо інтерфейс. Обчислення першої та другої похідної здійснюється методами з усталеною реалізацією:
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())); } }
Для функцій можна перевизначити механізм обчислення першої та другої похідної. Наприклад, для кубічного полінома
можна так визначити першу і другу похідну:
+
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), яка визначає ліву частину рівняння. Функція 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 Ієрархія класів "Країна" та "Перепис населення"
Припустимо, необхідно спроєктувати програму, в якій описуються класи для представлення країни та перепису населення. Дані про країну – назва, територія, а також послідовність посилань на об'єкт типу "Перепис населення".
Своєю чергою, клас, який представляє перепис населення, повинен включати дані про рік перепису, кількість населення та коментарі.
Необхідно реалізувати такі функції:
- обчислення щільності населення згідно з певним переписом;
- визначення перепису з найбільшою кількістю населення;
- перевірка входження певного слова в коментарі;
- сортування переписів за кількістю населення;
- сортування переписів за алфавітом коментарів.
У класі Census
можна реалізувати функції отримання представлення у вигляді рядка,
перевірки еквівалентності, отримання хеш-коду, перевірки наявності слів
і послідовності літер у коментарях і тестування. Для забезпечення сортування за збільшенням населення слід
реалізувати інтерфейс Comparable
і у функції compareTo()
забезпечити "природне"
порівняння за кількістю населення.
Оскільки передбачені різні варіанти зберігання даних про послідовність переписів, доцільно створити ієрархію
класів. У класі AbstractCountry
,
який не містить даних про послідовність переписів, слід описати абстрактні методи доступу до послідовності
й сортування. У цьому класі слід реалізувати функції отримання представлення у вигляді рядка, перевірки
еквівалентності, отримання хеш-коду, обчислення щільності населення згідно з певним переписом, визначення
перепису з найбільшою кількістю населення, а також перевірки входження певного слова в коментарі.
Похідний клас, який описує країну, повинен представляти послідовність елементів у вигляді масиву. Необхідно реалізувати функції сортування переписів за кількістю населення та за алфавітом коментарів, використовуючи стандартні засоби сортування масивів.
У пакеті ua.inf.iwanoff.java.third
створюємо новий клас
Census
. Його код буде таким:
package ua.inf.iwanoff.java.third; import java.util.StringTokenizer; /** * Клас відповідає за представлення перепису населення. * Перепис населення представлено роком, кількістю населення та коментарем */ 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; } /** * Надає подання перепису у вигляді рядка * * @return подання перепису у вигляді рядка */ @Override public String toString() { return "Перепис " + getYear() + " року. Населення: " + getPopulation() + ". Коментар: " + getComments(); } /** * Перевіряє, чи еквівалентний цей перепис іншому * @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.getYear() == getYear() && c.getPopulation() == getPopulation() && c.getComments().equals(getComments()); } /** * Повертає хеш-код перепису * @return значення хеш-коду */ @Override public int hashCode() { return (int) (year * population * getComments().hashCode()); } /** * Порівнює поточний об'єкт з отриманим параметром. Повертає від'ємне значення, * якщо поточний перепис менше отриманого як параметр, * нуль, якщо переписи однакові та додатне значення у протилежному випадку. * @param c перепис, з яким ми порівнюємо поточний. * @return результат порівняння */ @Override public int compareTo(Census c) { return Integer.compare(getPopulation(), c.getPopulation()); } /** * Перевіряє, чи міститься слово в тексті коментаря * @param word слово, яке ми шукаємо в коментарі * @return {@code true}, якщо слово міститься в тексті коментаря * {@code false} в протилежному випадку */ public boolean containsWord(String word) { StringTokenizer st = new StringTokenizer(getComments()); String s; while (st.hasMoreTokens()) { s = st.nextToken(); if (s.equalsIgnoreCase(word)) { return true; } } Object dd; return false; } /** * Перевіряє, чи міститься підрядок в тексті коментаря * @param substring підрядок, який ми шукаємо в коментарі * @return {@code true}, якщо підрядок міститься в тексті коментаря * {@code false} в протилежному випадку */ public boolean containsSubstring(String substring) { return getComments().toUpperCase().indexOf(substring.toUpperCase()) >= 0; } /** * Виводить на екран результати перевірки, чи міститься слово або підрядок * в тексті коментаря * @param word слово або підрядок, для якого здійснюється перевірка */ private void testWord(String word) { if (containsWord(word)) { System.out.println("Слово \"" + word + "\" міститься у коментарі"); } else { System.out.println("Слово \"" + word + "\" не міститься у коментарі"); } if (containsSubstring(word)) { System.out.println("Текст \"" + word + "\" міститься у коментарі"); } else { System.out.println("Текст \"" + word + "\" не міститься у коментарі"); } } /** * Здійснює тестування перевірки наявності слова або підрядка * в коментарі до перепису */ protected void testCensus() { setYear(2001); setPopulation(48475100); setComments("Перший перепис у незалежній Україні"); System.out.println(this); testWord("Україні"); testWord("Країні"); testWord("Україна"); } /** * Програма тестування можливості перевірки * наявності слова або підрядка * @param args аргументи командного рядка (не використовуються) */ public static void main(String[] args) { new Census().testCensus(); } }
Як видно з наведеного коду, клас Census
реалізує інтерфейс Comparable<Census>
.
Реалізація цього інтерфейсу вимагає додавання методу compareTo()
, в якому визначене "природне"
порівняння – за кількістю населення.
Клас AbstractCountry
також містить методи equals()
, hashCode()
і toString()
.
У класі розташовуємо допоміжну статичну функцію addToArray()
, яка додає до масиву новий елемент.
Реалізуємо всі функції, не залежні від внутрішнього представлення послідовності переписів. Код класу
AbstractCountry
буде таким:
package ua.inf.iwanoff.java.third; import java.util.Arrays; /** * Абстрактний клас для представлення країни, в якій здійснюється перепис населення. * Країна характеризується назвою, площею та послідовністю переписів. * Доступ до послідовності переписів представлено абстрактними методами */ 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); /** * Створює новий перепис та додає посилання на нього в кінець послідовності * * <p> Похідний клас повинен надавати реалізацію цього методу * * @param year рік перепису * @param population кількість населення * @param comments текст коментаря * @return {@code true}, якщо посилання вдалося додати * {@code false} в протилежному випадку */ public abstract boolean addCensus(int year, int population, String comments); /** * Повертає кількість переписів у послідовності. * * <p> Похідний клас повинен надавати реалізацію цього методу * * @return кількість переписів */ public abstract int censusesCount(); /** * Очищає послідовність переписів * * <p> Похідний клас повинен надавати реалізацію цього методу */ public abstract void clearCensuses(); /** * Здійснює сортування послідовності переписів за кількістю населення * * <p> Похідний клас повинен надавати реалізацію цього методу */ public abstract void sortByPopulation(); /** * Здійснює сортування послідовності переписів за алфавітом коментарів * * <p> Похідний клас повинен надавати реалізацію цього методу */ public abstract void sortByComments(); /** * Переписує дані з масиву переписів у послідовність * * <p> Похідний клас повинен надавати реалізацію цього методу * * @param censuses довільний масив переписів */ public abstract void setCensuses(Census[] censuses); /** * Повертає масив переписів з послідовності * * <p> Похідний клас повинен надавати реалізацію цього методу * * @return сформаваний масив посилань на переписи */ public abstract Census[] getCensuses(); /** * Допоміжна статична функція додавання посилання на перепис * до наданого масиву переписів * @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; } /** * Перевіряє, чи еквівалентна ця країна іншій * @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 (int) (getName().hashCode() * area * Arrays.hashCode(getCensuses())); } /** * Надає подання країни у вигляді рядка * * @return подання країни у вигляді рядка */ @Override public String toString() { StringBuilder result = new StringBuilder(getName() + ". Територія: " + getArea() + " кв. км."); for (int i = 0; i < censusesCount(); i++) { result.append("\n").append(getCensus(i)); } return result + ""; } /** * Повертає густоту населення для вказаного року * @param year рік (наприклад, 1959, 1979, 1989 тощо) * @return густота населення для вказаного року */ public double density(int year) { for (int i = 0; i < censusesCount(); i++) { if (year == getCensus(i).getYear()) { return getCensus(i).getPopulation() / getArea(); } } return 0; } /** * Знаходить і повертає рік з максимальним населенням * @return рік з максимальним населенням */ public int maxYear() { Census census = getCensus(0); for (int i = 1; i < censusesCount(); i++) { if (census.getPopulation() < getCensus(i).getPopulation()) { census = getCensus(i); } } return census.getYear(); } /** * Створює та повертає масив переписів зі вказаним словом в коментарях * @param word слово, яке відшукується * @return масив переписів зі вказаним словом в коментарях */ public Census[] findWord(String word) { Census[] result = null; for (Census census : getCensuses()) { if (census.containsWord(word)) { result = addToArray(result, census); } } return result; } /** * Виводить на екран дані про переписи, які містять певне слово в коментарях * @param word слово, яке відшукується */ private void printWord(String word) { Census[] result = findWord(word); if (result == null) { System.out.println("Слово \"" + word + "\" не міститься в коментарях."); } else { System.out.println("Слово \"" + word + "\" міститься в коментарях:"); for (Census census : result) { System.out.println(census); } } } /** * Допоміжна функція створення нового об'єкта "Країна" * @return посилання на новий об'єкт "Країна" */ public AbstractCountry createCountry() { setName("Україна"); setArea(603628); // Додавання переписів з виведенням результату (false / true): System.out.println(addCensus(1959, 41869000, "Перший післявоєнний перепис")); System.out.println(addCensus(1970, 47126500, "Нас побільшало")); System.out.println(addCensus(1979, 49754600, "Просто перепис")); System.out.println(addCensus(1989, 51706700, "Останній радянський перепис")); System.out.println(addCensus(2001, 48475100, "Перший перепис у незалежній Україні")); System.out.println(addCensus(2001, 48475100, "Перший перепис у незалежній Україні")); return this; } /** * Здійснює тестування методів класу */ public void testCountry() { System.out.println("Щільність населення у 1979 році: " + density(1979)); System.out.println("Рік з найбільшим населенням: " + maxYear() + "\n"); printWord("перепис"); printWord("запис"); sortByPopulation(); System.out.println("\nСортування за кількістю населення:"); System.out.println(this); sortByComments(); System.out.println("\nСортування за алфавітом коментарів:"); System.out.println(this); } }
У похідному класі використовуємо масив для представлення послідовності переписів. Для порівняння переписів
використано посилання на метод java.util.Comparator.comparing()
, який забезпечує необхідні значення
під час порівняння. Код класу CountryWithArray
буде таким:
package ua.inf.iwanoff.java.third; import java.util.Arrays; import java.util.Comparator; /** * Клас для представлення країни, в якій здійснюється перепис населення. * Дані про переписи представлені масивом */ 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(addToArray(getCensuses(), census)); return true; } /** * Створює новий перепис та додає посилання на нього в кінець послідовності. * @param year рік перепису * @param population кількість населення * @param comments текст коментаря * @return {@code true}, якщо посилання вдалося додати * {@code false} в протилежному випадку */ @Override public boolean addCensus(int year, int population, String comments) { Census census = new Census(year, population, comments); return addCensus(census); } /** * Повертає кількість переписів у послідовності * @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; } /** * Здійснює сортування послідовності переписів за кількістю населення */ @Override public void sortByPopulation() { Arrays.sort(censuses); } /** * Здійснює сортування послідовності переписів за алфавітом коментарів */ @Override public void sortByComments() { Arrays.sort(censuses, Comparator.comparing(Census::getComments)); } /** * Демонстрація роботи програми * @param args аргументи командного рядка (не використовуються) */ public static void main(String[] args) { new CountryWithArray().createCountry().testCountry(); } }
Під час виконання програми в консольне вікно спочатку виводяться результати функцій
addCensus()
– п'ять разів true
і один раз false
.
4 Вправи для контролю
- Створити ієрархію класів Книга та Підручник. Реалізувати конструктори та функції доступу. Перекрити функцію
toString()
. У функціїmain()
створити масив, який містить елементи різних типів. Вивести елементи на екран. - Створити ієрархію класів Кінофільм і Серіал. Реалізувати конструктори та функції доступу. Перекрити
функцію
toString()
. У функціїmain()
створити масив, який містить елементи різних типів. Вивести елементи на екран. - Створити ієрархію класів Місто та Столиця. Реалізувати конструктори та функції доступу. Перекрити
функцію
toString()
. У функціїmain()
створити масив, який містить елементи різних типів. Вивести елементи на екран. - Створити клас для представлення іменованої матриці з полем типу
String
– іменем матриці й полем, що представляє двовимірний масив. Реалізувати методи клонування, перевірки еквівалентності, генерації хеш-коду та отримання подання у вигляді рядка. Здійснити тестування.
5 Контрольні запитання
- У чому полягає зміст успадкування?
- У чому є сенс наявності спільного базового класу?
- Які елементи базового класу не успадковуються?
- Як здійснити ініціалізацію базового класу?
- Як викликати однойменний метод базового класу з похідного?
- Де і для чого можна застосовувати ключове слово
super
? - Як перекрити метод з модифікатором
final
? - Чи допускається множинне успадкування класів?
- Чи можна неявно приводити посилання на базовий клас до посилання на похідний клас?
- У чому є сенс застосування анотацій?
- Які можливості надає використання поліморфізму?
- Чим віртуальна функція відрізняється від невіртуальної?
- Як у Java указати, що функція віртуальна?
- Чи можна в класах, описаних як
final
, створювати віртуальні функції? - Чому функції з модифікатором
private
не є віртуальними? - Чи можна створити абстрактний клас без абстрактних методів?
- Чи можуть абстрактні класи містити неабстрактні методи?
- У чому перевага інтерфейсів у порівнянні з абстрактними класами?
- Чи можуть інтерфейси містити поля?
- Чи допускається множинне успадкування інтерфейсів?
- Яким вимогам повинен відповідати клас, який реалізує інтерфейс?
- Чи може клас реалізовувати кілька інтерфейсів?
- Яким вимогам повинен задовольняти об'єкт, щоб масив таких об'єктів можна було сортувати без визначення ознаки сортування?
- Як визначити спеціальне правило для сортування елементів масиву?
- Як звернутися до локального класу ззовні блоку?
- Чи може в одному файлі з сирцевим кодом бути визначене більш одного відкритого класу?
- Чи можна створювати об'єкт нестатичного внутрішнього класу, не створюючи об'єкту обхопного класу?
- Чи можуть нестатичні внутрішні класи містити статичні елементи?
- Чим відрізняються статичні вкладені класи від внутрішніх?
- Чи можуть статичні вкладені класи містити нестатичні елементи?
- Чи можна створювати класи усередині інтерфейсів?
- Чи є локальний клас статичним?
- Чи є безіменний клас статичним?
- Чому не можна створити явний конструктор безіменного класу?
- Що таке лямбда-вираз?
- Що таке функціональний інтерфейс?
- Які переваги надають лямбда-вирази?
- Для чого використовуються посилання на методи?
- У чому полягає процес клонування об'єктів?
- Для чого використовується перевизначення функції
equals()
? - Для чого використовується перевизначення функції
hashCode()
? - Як здійснити порівняння елементів масивів?