Лабораторна робота 1
Використання поліморфізму. Робота з узагальненнями та колекціями в Java
1 Завдання на лабораторну роботу
1.1 Індивідуальне завдання
Розробити ієрархію класів для представлення сутностей індивідуального завдання лабораторної роботи № 5 курсу "Алгоритмізація та програмування" попереднього семестру. Базовий абстрактний клас, який представляє другу сутність індивідуального завдання, не повинен містити даних, лише абстрактні методи доступу, перевизначення функцій toString()
та equals()
, а також реалізацію функцій, визначених попереднім завданням. Цей клас також повинен реалізовувати інтерфейс Comparable
для природного порівняння об'єктів під час сортування за однією з ознак.
Базовий абстрактний клас, який представляє першу з сутностей індивідуального завдання, повинен містити:
- абстрактні функції для доступу до даних;
- абстрактні функції для доступу до послідовності елементів типу другого абстрактного класу;
- абстрактні функції сортування елементів послідовності за визначеними ознаками відповідно до індивідуального завдання;
- перевизначення функції
toString()
для виведення даних про об'єкти; - перевизначення методу
equals()
для перевірки еквівалентності об'єктів; - реалізацію методів пошуку за визначеними ознаками;
- реалізацію функції додавання об'єкта з перевіркою, чи такий елемент вже присутній;
- реалізацію методу тестування функціональності класів.
Функції пошуку повинні повертати масиви об'єктів (або null
, якщо пошук не дав результатів), замість того, щоб безпосередньо виводити ці результати.
Похідні класи від створених абстрактних класів повинні містити поля конкретних типів, зокрема, послідовність елементів другої сутності повинна бути представлена у різних похідних класах у вигляді масиву та списку.
Здійснити тестування обох реалізацій. Тест повинен включати виконання завдання попередніх лабораторних робіт. Для сортування слід використовувати методи sort()
класів Arrays
та Collections
відповідно. Для визначення другої ознаки сортування використати лямбда-вираз.
Ознаки сортування визначаються залежно від номеру студента у списку групи.
Таблиця 1.1 – Індивідуальні завдання
№№ | Перша ознака | Друга ознака | №№ | Перша ознака | Друга ознака |
---|---|---|---|---|---|
1 | За зменшенням температури | За алфавітом коментаря | 16 | За збільшенням кількості слухачів | За алфавітом місця |
2 | За збільшенням кількості студентів | За збільшенням довжини теми | 17 | За номером групи | За зменшенням кількості студентів |
3 | За збільшенням кількості пасажирів | За алфавітом коментаря | 18 | За зменшенням кількості відвідувачів | За алфавітом коментаря |
4 | За збільшенням кількості слів у темі | За алфавітом теми | 19 | За зменшенням кількості учасників | За алфавітом теми |
5 | За збільшенням температури | За зменшенням довжини коментаря | 20 | За датою у зворотному порядку | За зменшенням кількості слухачів |
6 | За збільшенням кількості учасників | За алфавітом назви | 21 | За назвою проекту | За зменшенням кількості годин |
7 | За збільшенням кількості відвідувачів | За алфавітом коментаря | 22 | За часом початку роботи | За збільшенням кількості пацієнтів |
8 | За зменшенням кількості пасажирів | За зменшенням довжини коментаря | 23 | За зменшенням кількості замовлень | За назвою піци дня |
9 | За датою у зворотному порядку | За збільшенням кількості відвідувачів | 24 | За зменшенням кількості відвідувачів | За збільшенням кількості доступних доріжок |
10 | За збільшенням кількості концертів | За алфавітом міста | 25 | За зменшенням кількості книг, що видано | За збільшенням кількості книг, що повернуто |
11 | За номером зміни | За збільшенням кількості комп'ютерів | 26 | За зменшенням кількості унікальних хостів | За збільшенням кількості завантажених сторінок |
12 | За збільшенням кількості відвідувачів | За алфавітом коментаря | 27 | За зменшенням кількості актуальних лотів | За збільшенням сумарної вартості лотів |
13 | За зменшенням кількості пасажирів | За алфавітом назви | 28 | За зменшенням кількості спам повідомлень | За збільшенням загальної кількості повідомлень |
14 | За зменшенням кількості покупців | За алфавітом коментаря | 29 | За зменшенням курсу відкриття | За збільшенням кількості курсу закриття |
15 | За датою у зворотному порядку | За збільшенням кількості глядачів | 30 | За зменшенням кількості хвилин розмов | За збільшенням кількості коштів, що використано на розмови |
1.2 Ієрархія класів
Реалізувати класи "Людина", "Громадянин", "Студент", "Співробітник". Створити масив посилань на різні об'єкти ієрархії. Для кожного об'єкта вивести на екран рядок даних про нього.
1.3 Мінімум функції
Реалізувати програму, що дозволяє знайти мінімум деякої функції на заданому інтервалі. Алгоритм знаходження мінімуму полягає в послідовному переборі з певним кроком точок інтервалу і порівнянні значень функції в поточній точці з раніше знайденим мінімумом.
Реалізувати п'ять варіантів розв'язання:
- використання абстрактного та похідних класів
- опис інтерфейсу, створення класу, який використовує інтерфейс як тип параметру функції знаходження мінімуму, створення окремих класів, які реалізують інтерфейс
- використання попередньо описаного інтерфейсу і безіменних класів
- використання лямбда-виразів
- використання посилань на методи.
Перевірити роботу програми на двох різних функціях.
1.4 Реалізація масиву точок через двовимірний масив і одновимірний масив дійсних чисел
Реалізувати функціональність абстрактного класу AbstractArrayOfPoints
, наведеного в прикладі 3.2, через використання двовимірного масиву дійсних чисел, а також одновимірного масиву дійсних чисел (кожна пара чисел у масиві має відповідати точці). Здійснити тестування класів.
Примітка: не можна вносити зміни в абстрактний клас AbstractArrayOfPoints
, окрім, можливо, імені пакету.
1.5 Створення бібліотеки узагальнених функцій для роботи з масивами та списками
Реалізувати клас зі статичними узагальненими методами, які реалізують таку функціональність:
- обмін місцями двох груп елементів
- обмін місцями усіх пар сусідніх елементів (з парним і непарним індексом)
- вставлення у масив (список) іншого масиву (списку) елементів у вказане місце
- заміна групи елементів іншим масивом (списком) елементів
Реалізувати наведені функції для масивів і для списків. Здійснити демонстрацію роботи усіх методів з використанням даних різних типів (Integer
, Double
, String
).
1.6 Реалізація інтерфейсу Comparable (додаткове завдання)
Створити клас Circle
, який реалізує інтерфейс Comparable
. Більшим вважається коло з більшим радіусом. Здійснити сортування списку об'єктів типу Circle
.
1.7 Реалізація інтерфейсу Comparator (додаткове завдання)
Створити клас Triangle
. Трикутник визначати довжинами сторін. Площа трикутника в цьому випадку може бути обчислена за формулою Герона:
де a, b і c – довжини сторін трикутника. Здійснити сортування списку трикутників за зменшенням площі. Для визначення ознаки сортування використовувати об'єкт, який реалізує інтерфейс Comparator
.
2 Методичні вказівки
2.1 Композиція класів
Під композицією класів розуміють створення нових класів з використанням об'єктів інших класів як полів. Java не дозволяє розміщення об'єктів усередині інших об'єктів, можна тільки описувати посилання. Композиція класів у цьому випадку припускає створення об'єктів безпосередньо (у тілі класу при оголошенні полів) чи в конструкторах.
class X { } class Y { } class Z { X x = new X(); Y y; Z() { y = new Y(); } }
Можна також створити внутрішній об'єкт безпосередньо перед його першим використанням.
Відношення, що моделюється композицією, часто називають відношенням "has-a".
Агрегування – це різновид композиції, який передбачає, що сутність (екземпляр) міститься в іншій сутності або не може бути створена та існувати без сутності, яка її охоплює. При цьому сутність, що охоплює, може існувати без внутрішньої, тобто час життя зовнішньої та внутрішньої сутностей може не збігатися. Більш строге трактування композиції (власне композиція) передбачає, що час життя зовнішньої та внутрішньої сутностей збігається. На рівні Java агрегування передбачає можливість створення внутрішнього об'єкта перед його використанням, тоді як строга композиція передбачає створення внутрішнього об'єкта в тілі класу, в блоці ініціалізації або в конструкторі.
2.2 Успадкування
Механізм успадкування полягає в породженні похідних класів від базових. Якщо один клас (похідний) є нащадком іншого (базового), то спадкоємець має можливість безпосередньо користуватися неприватними даними і функціями, визначеними в базовому класі. Відносини між класами і підкласами (нащадками) називаються ієрархією спадкування класів.
На відміну від 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;
2.3 Поліморфізм
2.3.1 Загальні концепції
Поліморфізм часу виконання – це властивість класів, згідно з якою поведінка об'єктів класу може визначатися не на етапі компіляції, а на етапі виконання. Класи, що надають ідентичний інтерфейс, але реалізовані під конкретні специфічні вимоги, мають назву поліморфних класів.
Підключення тіла функції до точки її виклику має назву зв'язування. Якщо воно відбувається до початку виконання програми, мова йде про раннє зв'язування. Цей тип зв'язування притаманний мовам процедурного типу, таким як C чи Pascal. Пізнє зв'язування означає, що підключення відбувається під час виконання програми та в об'єктно-орієнтованих мовах залежить від типів об'єктів. Пізнє зв'язування ще називають динамічним, або зв'язуванням часу виконання. Для реалізації поліморфізму використовується механізм пізнього зв'язування.
У мовах об'єктно-орієнтованого програмування пізнє зв'язування реалізоване через механізм віртуальних функцій. Віртуальна функція (віртуальний метод, virtual method) – це функція, визначена в базовому класі, та перевизначена (перекрита) у похідних, так, що конкретна реалізація функції для виклику визначатиметься під час виконання програми. Вибір реалізації віртуальної функції залежить від реального (а не оголошеного під час опису) типу об'єкта. Оскільки посилання на базовий тип може містити адресу об'єкта будь-якого похідного типу, поведінка раніше створених класів може бути змінена пізніше шляхом перевизначення віртуальних методів. Перевизначення передбачає відтворення імені, списку параметрів та специфікатора доступу. Фактично поліморфними є класи, які містять віртуальні функції.
У C++ для позначення віртуальної функції використовують модифікатор virtual
. У Java всі методи є віртуальними, за винятком конструкторів, статичних (static
), фінальних (final
) і закритих (private
) методів. На відміну від C++, слово virtual
не використовується.
Починаючи з Java 5, перед перевизначеними віртуальними методами розміщують директиву @Override
, яка дозволяє компілятору здійснити додаткову перевірку синтаксису – відповідність сигнатури нової функції сигнатурі перекритої функції базового класу. Використання @Override
є бажаним, але не обов'язковим.
Усі класи Java є поліморфними, оскільки таким є клас java.lang.Object
. Зокрема, завдяки поліморфізму кожен клас може визначити свою віртуальну функцію toString()
, яка буде викликана для автоматичного отримання даних про об'єкт у вигляді рядку.
У Java є ключове слово instanceof
, яке дозволяє перевірити, чи є об'єкт екземпляром певного типу (або похідних типів). Вираз
об'єкт instanceof клас
повертає значення типу boolean
, яке може бути використане для перевірки, чи можна викликати метод цього класу:
if (x instanceof SomeClass) ((SomeClass)x).someMethod();
2.3.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.3.3 Інтерфейси
У Java використовується поняття інтерфейсів. Інтерфейс може розглядатися як чисто абстрактний клас, що містить тільки абстрактні методи:
interface Int1 { void f(); int g(int x); }
Кожен клас може бути створений тільки від одного базового класу, але при цьому реалізовувати один чи кілька інтерфейсів. Клас, що реалізує інтерфейс, повинен забезпечити реалізацію всіх методів, оголошених в інтерфейсі. В іншому випадку такий клас буде абстрактним і повинен бути оголошений зі специфікатором abstract
.
Інтерфейси можуть містити поля, що є фінальними і статичними (константами часу компіляції).
Для того, щоб указати, що клас реалізує інтерфейс, ім'я інтерфейсу вказують у списку реалізованих інтерфейсів. Такий список розташовують у заголовку класу після ключового слова implements
. Методи, визначені в інтерфейсі, є абстрактними і відкритими. У класі, що реалізує інтерфейс, такі методи повинні бути оголошені як public
:
interface Int1 { void f(); int g(int x); } class SomeClass implements Int1 { public void f() { } public int g(int x) { return x; } }
Інтерфейс може мати кілька базових інтерфейсів:
interface Int1 { void f(); int g(int x); } interface Int2 { void h(int z); } interface Int3 extends Int1, Int2 { }
Клас може реалізувати кілька інтерфейсів:
interface Int1 { void f(); int g(int x); } interface Int2 { void h(int z); } class SomeClass implements Int1, Int2 { public void f() { } public int g(int x) { return x; } public void h(int z) { } }
Версія Java 8 надає нову можливість надання усталеної реалізації для методів, оголошених в інтерфейсі. Для цього перед відповідною функцією слід розмістити ключове слово default
, після чого функцію можна реалізувати всередині інтерфейсу. Наприклад, можна запропонувати інтерфейс, який представляє деяку функцію, а також реалізацію цієї функції:
package ua.inf.iwanoff.oop.first; public interface Greetings { default void hello() { System.out.println("Hello everybody!"); } }
Клас, який реалізує інтерфейс, може бути порожнім. Можна залишити усталену реалізацію методу hello()
:
package ua.inf.iwanoff.oop.first; public class MyGreetings implements Greetings { }
Під час тестування отримаємо усталене вітання.
package ua.inf.iwanoff.oop.first; public class GreetingsTest { public static void main(String[] args) { new MyGreetings().hello(); // Hello everybody! } }
Те ж саме можна отримати, використавши безіменний клас. Його тіло також буде порожнім:
package ua.inf.iwanoff.oop.first; 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.oop.first; public class MyGreetings implements Greetings { @Override public void hello() { System.out.println("Hello to me!"); } }
Тепер, створивши об'єкт цього класу, ми отримаємо нове привітання.
Якщо з перевизначеного методу необхідно викликати усталений метод інтерфейсу, можна скористатися ключовим словом super
:
Greetings.super.hello();
Можна запропонувати такий приклад. Припустимо, необхідно надрукувати значення функції на деякому інтервалі з визначеним кроком. Створюємо інтерфейс з одним абстрактним методом (обчислення деякої функції) і одним з усталеною реалізацією:
package ua.inf.iwanoff.oop.first; 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.oop.first; 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.oop.first; 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.4 Анотації (метадані)
Анотації дозволяють включити в програмний код додаткову інформацію, яка не може бути визначена за допомогою засобів мови. У тексті програми анотації починаються із символу @. Типовий приклад анотації – @Override
. Завдяки цій анотації компілятор може перевірити, чи дійсно відповідний метод був оголошений у базових класах.
public class MyClass extends Object { @Override public String toString() { return "My overridden method!"; } }
Можна навести інші приклади анотацій:
@SuppressWarnings("ідентифікатор_попередження")
-
попередження компілятора повинні бути замовчані в анотованому елементі@Deprecated
-
використання анотованого елемента не є більше бажаним
Java дозволяє визначати власні анотації.
2.5 Перевірка еквівалентності об'єктів
Побудована на посиланнях модель об'єктів Java не дозволяє порівнювати вміст об'єктів за допомогою операції порівняння (==
), оскільки при цьому порівнюються посилання. Для порівняння даних доцільно використовувати функцію equals()
, визначену в класі java.lang.Object
. Для класів, полями яких є типи-значення, метод класу Object
забезпечує поелементне порівняння. Якщо ж полями є посилання на об'єкти, необхідно явно перевизначити функцію equals()
. Типова реалізація методу equals()
передбачає перевірку посилань (чи вони збігаються), далі перевірку об'єкта, який ми порівнюємо, на значення null
, потім – перевірку типу, наприклад, за допомогою instanceof
. Якщо типи збігаються, здійснюється перевірка значень полів.
Наведемо повний приклад з класом Human
.
package ua.inf.iwanoff.oop.first; public class Human { 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 String toString() { return name + " " + surname; } public static void main(String[] args) { Human human1 = new Human("John", "Smith"); Human human2 = new Human("John", "Smith"); 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.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 Узагальнення (Generics)
2.7.1 Концепція узагальненого програмування
Часто виникає необхідність у створенні так званих класів-контейнерів – таких, які містять об'єкти довільних типів. При цьому над елементами контейнерів необхідно виконувати деякі однотипні дії. Код для обробки об'єктів різних типів може виглядати практично однаково. Наприклад, якщо для різних типів даних потрібно реалізувати алгоритми на кшталт швидкого сортування або способи обробки таких структур даних, як зв'язаний список або бінарне дерево. У таких випадках код однаковий для всіх типів об'єктів.
Парадигма узагальненого програмування передбачає опис правил зберігання даних і алгоритмів у загальному вигляді незалежно від конкретних типів даних. Конкретні типи даних, над якими виконуються дії, специфікуються пізніше. Механізми розділення структур даних і алгоритмів, а також формування абстрактних описів вимог до даних, визначаються по-різному в різних мовах програмування. Спочатку можливості узагальненого програмування були представлені в сімдесяті роки XX століття мовами CLU і Ада (узагальнені функції), пізніше були реалізовані в мові ML (параметричний поліморфізм).
Найбільш повно і гнучко ідея узагальненого програмування реалізована у мові C++ через механізм шаблонів. Шаблон (template) у C++ – це фрагмент коду, який узагальнено описує роботу з деяким абстрактним типом, заданим як параметр шаблону. Цей фрагмент коду (клас або функція) остаточно компілюється тільки після інстанціювання шаблону конкретним типом, тобто після підстановки конкретного типу замість параметра. На використанні шаблонних функцій і параметризованих класів побудована Стандартна бібліотека шаблонів (STL), що є зараз частиною Стандартної бібліотеки C++. STL включає опис стандартних контейнерних класів і незалежних від них алгоритмів.
Для реалізації узагальненого програмування в Java використовуються узагальнення – спеціальна мовна конструкція, яка з'явилася у синтаксисі мови починаючи з версії Java 5.
2.7.2 Проблеми створення універсальних контейнерів у Java 2
Припустимо, нам необхідно створити контейнер для зберігання пари об'єктів одного типу. Можна запропонувати клас Pair
(пара). Він містить два посилання на клас Object
:
public class Pair { Object first, second; public Pair(Object first, Object second) { this.first = first; this.second = second; } }
Оскільки клас Object
є базовим для усіх типів-посилань, новий клас можна, наприклад, застосувати для зберігання пари рядків:
Pair p = new Pair("Прізвище", "Ім\'я");
Такий підхід має певні недоліки:
- Для читання об'єктів необхідно застосувати явне перетворення типів:
String s = (String) p.first; // Замість String s = p.first;
Integer i = (Integer) p.second; // Помилка часу виконання
Pair p1 = new Pair("Прізвище", new Integer(2)); // Жодного повідомлення про помилку
Аналогічні проблеми в Java 2 виникали зі стандартними контейнерними класами. Наслідком реалізованого таким чином підходу стали потенційні помилки часу виконання, які не могли бути знайдені під час компіляції коду.
2.7.3 Синтаксис узагальнень
Зазначені раніше проблеми розв'язує синтаксична конструкція Java 5, так зване узагальнення – конструкція, що включає в себе параметр класу або функції, який містить додаткову інформацію про тип елементів та інших даних. Цей параметр беруть у кутові дужки. Узагальнення надають можливість створення та використання структур даних, безпечних з точки зору типів. Класи, опис яких містить такий параметр, мають назву узагальнених. Під час створення об'єкта узагальненого типу у кутових дужках вказують імена реальних типів. Можна використовувати тільки типи-посилання. Попередній приклад можна реалізувати з застосуванням узагальнень.
public class Pair<T> { T first, second; public Pair(T first, T second) { this.first = first; this.second = second; } public static void main(String[] args) { Pair<String> p = new Pair<String>("Прізвище", "Ім\'я"); String s = p.first; // Отримуємо рядок без приведення типів Pair<Integer> p1 = new Pair<Integer>(1, 2); // Можна використовувати цілі константи int i = p1.second; // Отримуємо ціле значення без приведення типів } }
Примітка:Java версії 7 і вище дозволяє не повторювати фактичний параметр узагальнення після імені конструктора. Наприклад:
Pair<Integer> p1 = new Pair<>(1, 2);
Якщо ми намагаємось додати до пари дані різних типів, компілятор згенерує помилку. Помилковою є також спроба явно перетворити тип:
Pair<String> p = new Pair<String>("1", "2"); Integer i = (Integer) p.second; // Помилка компіляції
Тип даних з параметром у кутових дужках (наприклад, Pair<String>
) має назву параметризованого типу.
Узагальнення по зовнішньому представленню і використанню аналогічні шаблонам C++. Але на відміну від шаблонів C++, існує не декілька різних типів Pair
, а один. Фактично у полях класу зберігаються посилання на Object
. Інформація про тип параметрів використовується компілятором для контролю та автоматичного приведення типів у вихідному тексті.
Окрім узагальнених класів, можна створювати узагальнені інтерфейси. Параметр може бути використаний в описі функцій, оголошених в інтерфейсі. Під час їх реалізації замість параметра узагальнення використовують деякий тип-посилання. Наприклад:
interface Function<T> { T func(T x); } class DoubleFunc implements Function<Double> { @Override public Double func(Double x) { return x * 1.5; } } class IntFunc implements Function<Integer> { @Override public Integer func(Integer x) { return x % 2; } }
Java також дозволяє створювати узагальнені функції всередині як узагальнених, так і звичайних (неузагальнених) класів:
public class ArrayPrinter { public static<T> void printArray(T[] a) { for (T x : a) { System.out.print(x + "\t"); } System.out.println(); } public static void main(String[] args) { String[] as = {"First", "Second", "Third"}; printArray(as); Integer[] ai = {1, 2, 4, 8}; printArray(ai); } }
Як видно з прикладу, виклик узагальненої функції не вимагає явного визначення типу. Іноді таке визначення необхідне, наприклад, коли в функції немає параметрів узагальненого типу. Якщо це статична функція, необхідно явно вказувати її клас. Наприклад:
public class TypeConverter { public static <T>T convert(Object object) { return (T) object; } public static void main(String[] args) { Object o = "Some Text"; String s = TypeConverter.<String>convert(o); System.out.println(s); } }
Рекомендованими іменами формальних параметрів є імена з однієї великої літери. Узагальнення може мати два і більше параметрів. У наступному прикладі пара може містити посилання на об'єкти різних типів:
public class PairOfDifferentObjects<T, E> { T first; E second; public PairOfDifferentObjects(T first, E second) { this.first = first; this.second = second; } public static void main(String[] args) { PairOfDifferentObjects<Integer, String> p = new PairOfDifferentObjects<Integer, String>(1000, "thousand"); PairOfDifferentObjects<Integer, Integer> p1 = new PairOfDifferentObjects<Integer, Integer>(1, 2); //... } }
Над даними типу параметру узагальнення можна здійснювати тільки дії, дозволені для об'єктів класу Object
. Іноді для розширення функціональності бажаною є конкретизація типу. Наприклад, ми хочемо викликати методи, оголошені у певному класі або інтерфейсі. Тоді можна застосувати такий синтаксис опису параметру: <T extends SomeBaseType>
або <T extends FirstType & SecondType>
тощо. Слово extends
використовують як для класів, так і для інтерфейсів.
Наприклад, можна створити узагальнену функцію обчислення середнього арифметичного в масиві деяких числових значень. Стандартні класи Double
, Float
, Integer
, Long
та інші класи-обгортки числових даних мають загальний абстрактний базовий клас java.lang.Number
, що декларує, зокрема, метод doubleValue()
, який дозволяє, отримати число, що зберігається в об'єкті, у вигляді значення типу double
. Цей факт можна використовувати для обчислення середнього арифметичного. Створена функція може працювати з масивами чисел різних типів:
package ua.inf.iwanoff.oop.first; public class AverageTest { public static<E extends Number> double average(E[] arr) { double result = 0; for (E elem : arr) { result += elem.doubleValue(); } return result / arr.length; } public static void main(String[] args) { Double[] doubles = { 1.0, 1.1, 1.5 }; System.out.println(average(doubles)); // 1.2 Integer[] ints = { 10, 20, 3, 4 }; System.out.println(average(ints)); // 9.25 } }
Наведемо приклад створення узагальненого класу, який зберігає масив елементів певного типу:
public class MyArray<T> { private T[] arr; public MyArray(T... arr) { this.arr = arr; } public int size() { return arr.length; } public T get(int i) { return arr[i]; } public void set(int i, T t) { arr[i] = t; } public void printAll() { for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); } }
В іншому класі здійснюємо тестування:
public class TestClass { public static void main(String[] args) { MyArray<String> a = new MyArray<>(new String[]{ "1", "2" }); String s = a.get(a.size() - 1); System.out.println(s); // 2 a.set(1, "New"); a.printAll(); // 1 New } }
Не можна створювати об'єктів узагальнених типів:
T arr = new T[10]; // Помилка!
У нашому прикладі цю проблему можна розв'язати за допомогою посилань на клас Object
. Крім того, для зручності використання конструктор можна реалізувати як функцію зі змінним числом параметрів. Корисною також буде функція додавання елемента в кінець масиву. Альтернативна реалізація може бути такою:
package ua.inf.iwanoff.oop.first; public class MyArray<T> { private Object[] arr = {}; public MyArray(T... arr) { this.arr = arr; } public MyArray(int size) { arr = new Object[size]; } public int size() { return arr.length; } public T get(int i) { return (T)arr[i]; } public void set(int i, T t) { arr[i] = t; } public void add(T t) { Object[] temp = new Object[arr.length + 1]; System.arraycopy(arr, 0, temp, 0, arr.length); arr = temp; arr[arr.length - 1] = t; } public void printAll() { for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); } }
В іншому класі здійснюємо тестування:
package ua.inf.iwanoff.oop.first; public class TestClass { public static void main(String[] args) { MyArray<String> a = new MyArray<>("1", "2"); String s = a.get(a.size() - 1); System.out.println(s); // 2 a.set(1, "New"); a.printAll(); // 1 New MyArray<Double> b = new MyArray<>(3); b.set(0, 1.0); b.set(1, 2.0); b.set(2, 4.0); b.add(8.0); b.printAll(); } }
Функціональність класу можна розширити методами додавання нового елемента всередині масиву, видалення існуючого тощо.
Синтаксис узагальнень передбачає використання так званих масок (wildcard, символ '?'). Маска застосовується, наприклад, для опису посилань на поки невідомий тип. Використання масок робить узагальнені класи та функції більш сумісними. Маска надає альтернативний спосіб створення узагальнених функцій. Такі функції самі по собі не є узагальненими, але містять аргументи узагальнених типів.
У наведеному нижче прикладі використовується раніше створений клас MyArray
, зокрема, його конструктор зі змінним числом параметрів:
package ua.inf.iwanoff.oop.first; public class GenericArrayPrinter { public static void printGenericArray(MyArray<?> a) { for (int i = 0; i < a.size(); i++) { System.out.print(a.get(i) + "\t"); } System.out.println(); } public static void main(String[] args) { MyArray<String> arr1 = new MyArray("First", "Second", "Third"); printGenericArray(arr1); MyArray<?> arr2 = new MyArray(1, 2, 3); // MyArray<?> замість MyArray<Integer> printGenericArray(arr2); } }
Можна обмежити використання типу параметра функції певними похідними класами, наприклад, MyArray<? super String>
. Тоді використання списку MyArray<Integer>
неможливе.
2.8 Контейнерні класи та інтерфейси
2.8.1 Загальні відомості
Засоби Java для роботи з колекціями надають уніфіковану архітектуру для представлення та управління наборами даних. Ця архітектура дозволяє працювати з колекціями незалежно від деталей їхньої внутрішньої організації. Засоби для роботи з колекціями включають більше десятка інтерфейсів, а також стандартні реалізації цих інтерфейсів і набір алгоритмів для роботи з ними.
Колекція – це об'єкт, який представляє групу об'єктів. Використання колекцій забезпечує підвищення ефективності програм завдяки використанню високоефективних алгоритмів, а також сприяє створенню коду, придатного для повторного використання. Основними елементами засобів роботи з колекціями є такі:
- інтерфейси
- стандартні реалізації інтерфейсів
- алгоритми
- утиліти для роботи з масивами
Крім того, Java 8 підтримує контейнери Java 1.1. Це Vector
, Enumeration
, Stack
, BitSet
і деякі інші. Наприклад, клас Vector
надає функціональність, аналогічну ArrayList
. У першій версії Java ці контейнери не забезпечували стандартизованого інтерфейсу. Вони також не дозволяють користувачеві відмовитися від надмірної синхронізації, яка актуальна лише в багатопотоковому оточенні, і отже, недостатньо ефективні. Внаслідок цього вони вважаються застарілими і не рекомендовані для використання. Замість них слід використовувати відповідні узагальнені контейнери Java 5.
Стандартні контейнерні класи Java дозволяють зберігати в колекції посилання на об'єкти, класи яких походять від класу Object
. Контейнерні класи реалізовані в пакеті java.util
. Починаючи з Java 5, усі контейнерні класи реалізовані як узагальнені.
Існує два базових інтерфейси, в яких декларована функціональність контейнерних класів: Collection
і Map
. Інтерфейс Collection
є базовим для інтерфейсів List
(список), Set
(множина), Queue
(черга) і Deque
(черга з двома кінцями).
Інтерфейс List
(список) описує пронумеровану колекцію (послідовність), елементи якої можуть повторюватися.
Інтерфейс Set
реалізований класами HashSet
і LinkedHashSet
. Інтерфейс SortedSet
, похідний від Set
, реалізований класом TreeSet
. Інтерфейс Map
реалізований класом HashMap
. Інтерфейс SortedMap
, похідний від Map
, реалізований класом TreeMap
.
Класи HashSet
, LinkedHashSet
і HashMap
використовують для для ідентифікації елементів так звані хеш-коди. Хеш-код – це унікальна послідовність бітів фіксованої довжини. Для кожного об'єкта ця послідовність вважається унікальною. Хеш-коди забезпечують швидкий доступ до даних за деякти ключем. Механізм одержання хеш-кодів забезпечує їх майже повну унікальність. Усі об'єкти Java можуть генерувати хеш-коди: метод hashCode()
визначений для класу Object
.
Для більшості колекцій існують як "звичайні" реалізації, так і реалізації, безпечні з точки зору потоків управління, наприклад CopyOnWriteArrayList
, ArrayBlockingQueue
тощо.
2.8.2 Інтерфейс Collection
Інтерфейс Collection
є базовим для багатьох інтерфейсів Collection Framework і оголошує найбільш загальні методи колекцій:
Метод | Опис |
---|---|
|
Повертає розмір колекції |
boolean isEmpty() |
Повертає true , якщо колекція порожня |
boolean contains(Object o) |
Повертає true , якщо колекція містить об'єкт |
Iterator<E> iterator |
Повертає ітератор – об'єкт, який послідовно вказує на елементи |
Object[] toArray() |
Повертає масив посилань на Object , який містить копії всіх елементів колекції |
<T> T[] toArray(T[] a) |
Повертає масив посилань на T , який містить копії всіх елементів колекції |
boolean add(E e) |
Додає об'єкт у колекцію. Повертає true , якщо об'єкт доданий |
boolean remove(Object o) |
Видаляє об'єкт з колекції |
boolean containsAll(Collection<?> c) |
Повертає
true якщо колекція містить іншу колекцію |
boolean addAll(Collection<? extends E> c) |
Додає об'єкти в колекцію. Повертає true , якщо об'єкти додані |
boolean removeAll(Collection<?> c) |
Видаляє об'єкти з колекції |
boolean retainAll(Collection<?> c) |
Залишає об'єкти, присутні в іншій колекції |
void clear() |
Видаляє всі елементи з колекції |
Примітка: в таблиці не вказані методи з усталеною реалізацією, які були додані у Java 8.
Наведений нижче приклад демонструє роботу методів інтерфейсу. В прикладі використовується клас ArrayList
як найпростіша реалізація інтерфейсу Collection
:
package ua.inf.iwanoff.oop.first; import java.util.*; public class CollectionDemo { public static void main(String[] args) { Collection<Integer> c = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); System.out.println(c.size()); // 5 System.out.println(c.isEmpty()); // false System.out.println(c.contains(4)); // true c.add(6); System.out.println(c); // [1, 2, 3, 4, 5, 6] c.remove(1); System.out.println(c); // [2, 3, 4, 5, 6] Collection<Integer> c1 = new ArrayList<>(Arrays.asList(3, 4)); System.out.println(c.containsAll(c1));// true c.addAll(c1); System.out.println(c); // [2, 3, 4, 5, 6, 3, 4] Collection<Integer> c2 = new ArrayList<>(c); // копія c.removeAll(c1); System.out.println(c); // [2, 5, 6] c2.retainAll(c1); System.out.println(c2); // [3, 4, 3, 4] c.clear(); System.out.println(c); // [] } }
2.8.3 Робота зі списками
Інтерфейс List
описує упорядковану колекцію (послідовність). Цей інтерфейс реалізують стандартні класи ArrayList
і LinkedList
. Клас ArrayList
реалізує список за допомогою масиву змінної довжини. Клас LinkedList
зберігає об'єкти за допомогою так званого зв'язаного списку.
Існує також стандартна абстрактна реалізація списку – клас AbstractList
. Цей клас, похідний від AbstractCollection
, надає низку корисних засобів. Практично в тій чи іншій формі реалізовані всі методи, крім get()
і size()
. Проте, для конкретної реалізації списку більшість функцій слід перекрити.. Клас AbstractList
– базовий для ArrayList
. Класс AbstractSequentialList
, похідний від AbstractList
, є базовим для класу LinkedList
.
Створити порожній список посилань на об'єкти деякого типу (SomeType
) можна за допомогою усталеного конструктора:
List<SomeType> al = new ArrayList<SomeType>();
Можна також одразу описати посилання на ArrayList
:
ArrayList<SomeType> al = new ArrayList<SomeType>();
Другий варіант іноді не є бажаним, оскільки в такому випадку знижується гнучкість програми. Перший варіант дозволить легко замінити реалізацію списку ArrayList
на будь-яку іншу реалізацію інтерфейсу List
, яка більше відповідає вимогам конкретної задачі. У другому випадку є спокуса викликати методи, специфічні для ArrayList
, тому перехід на іншу реалізацію буде ускладнено.
Створивши порожній список, у нього можна додавати елементи за допомогою функції add()
. Метод add()
з одним аргументом типу-посилання додає елемент у кінець списку. Якщо цю функцію викликати з двома аргументами, новий елемент буде додано в список у позиції, зазначеній першим параметром:
List<String> al = new ArrayList<String>(); al.add("abc"); al.add("def"); al.add("xyz"); al.add(2, "ghi"); // Додавання нового рядка перед "xyz"
До списку можна додати всі елементи іншого списку (або іншої колекції) за допомогою функції addAll().
Можна створити новий список із використанням існуючого. Новий список містить посилання на копії елементів. Наприклад:
List<String> a11 = new ArrayList<String>(al);
За допомогою статичної функції asList()
класу java.util.Arrays
можна створити список з існуючого масиву. Масив можна також створити безпосередньо у списку параметрів функції. Наприклад:
String[] arr = {"one", "two", "three"}; List<String> a2 = Arrays.asList(arr); List<String> a3 = Arrays.asList("four", "five");
Для списків перевантажена функція toString()
, яка дозволяє, наприклад, вивести всі елементи масиву на екран без використання циклів. Елементи виводяться у квадратних дужках:
System.out.println(a3); // [four, five]
Списки дозволяють роботу з окремими елементами. Метод size()
повертає кількість елементів, що містяться в списку. Як і в масивах, доступ до елементів списків може здійснюватися за індексом, але не через операцію []
, а за допомогою методів get()
і set()
. Метод get()
класу ArrayList
повертає елемент із зазначеним індексом (позиція в списку). Як і елементи масивів, елементи списків пронумеровані з нуля. У наведеному нижче прикладі рядки виводяться за допомогою функції println()
:
List<String> al = new ArrayList<String>(); al.add("abc"); al.add("def"); al.add("xyz"); for (int i = 0; i < al.size(); i++) { System.out.println(al.get(i)); }
Метод set()
дозволяє змінити об'єкт, що зберігається в зазначеній позиції. Метод remove()
видаляє об'єкт у зазначеній позиції:
al.set(0, "new"); al.remove(2); System.out.println(al); // [new, def]
Функція subList(fromIndex, toIndex)
повертає список, складений з елементів починаючи з елемента з індексом fromIndex
і не включаючи елемент з індексом toIndex
. Наприклад:
System.out.println(al.subList(1, 3)); // [def, xyz]
Метод removeRange(m, n)
видаляє всі елементи, індекс яких між m
включно та не включаючи n
. Усі елементи колекцій віддаляються за допомогою методу clear()
. Функція contains()
повертає true
, якщо список містить зазначений елемент. Наприклад:
if (al.contains("abc")) { System.out.println(al); }
Функція toArray()
повертає посилання на масив копій об'єктів, посилання на які зберігаються в списку.
Object [] a = al.toArray(); System.out.println(a[1]); // def (al.toArray()) [2] = "text"; // Зміна елементів нового масиву
Для збереження цілих і дійсних значень у колекціях часто використовують класи-оболонки Integer
та Double
відповідно.
У тих випадках, коли частіше, ніж вибір довільного елемента, застосовують операції додавання і видалення елементів у довільних місцях, доцільно використовувати клас LinkedList
, що зберігає об'єкти за допомогою зв'язаного списку. Зв'язний список, реалізований у Java контейнером LinkedList
, є двоспрямованим списком, де кожен елемент містить посилання на попередній і наступний елементи.
Для зручної роботи додані також методи addFirst()
, addLast()
, removeFirst()
і removeLast()
.
LinkedList<String> list = new LinkedList<String>(); list.addLast("last"); // Те саме, що list.add("last"); list.addFirst("first"); System.out.println(list); // [first, last] list.removeFirst(); list.removeLast(); System.out.println(list); // []
Ці специфічні функції додані саме в LinkedList
, оскільки вони не можуть бути ефективно реалізовані в ArrayList
із застосуванням масивів.
2.8.4 Ітератори
Для проходження по колекції (списку) об'єктів використовується ітератор – спеціальний допоміжний об'єкт. Як і самі контейнери, ітератори базуються на інтерфейсі. Інтерфейс Іterator
, визначений у пакеті java.utіl
. Будь-який ітератор має три методи:
boolean hasNext(); // перевіряє, чи є ще елементи в контейнері, // та повертає true, якщо ще є елементи послідовності. Object next(); // повертає посилання на наступний елемент void remove(); // видаляє останній обраний елемент (на який посилається ітератор)
Після першого виклику методу next()
ітератор указуватиме на початковий елемент контейнеру. Колекції Java повертають об'єкт-ітератор за допомогою методу iterator()
:
List<String> s = new ArrayList<String>(); s.add("First"); s.add("Second"); for (Iterator<String> i = s.iterator(); i.hasNext(); ) { System.out.println(i.next()); }
Як видно з наведеного прикладу, ітератор теж є узагальненим типом.
Альтернативна форма циклу for
(for each) дозволяє обійти список без явного створення ітератору:
List<Integer> a = new ArrayList<Integer>(); a.add(1); a.add(2); a.add(3); a.add(4); for (Integer i : a) { System.out.print(i + " "); }
Спеціальний вид ітератору списку, ListIterator
, надає додаткові можливості ітерації, зокрема, проходження по списку у зворотному порядку. В наступному прикладі для перевірки, чи є слово паліндромом використовується список символів і ListIterator
, який забезпечує проходження в зворотному порядку:
package ua.inf.iwanoff.oop.first; import java.util.*; public class ListIteratorTest { public static void main(String[] args) { String palStr = "racecar"; List<Character> palindrome = new LinkedList<Character>(); for (char ch : palStr.toCharArray()) { palindrome.add(ch); } System.out.println("Input string is: " + palStr); ListIterator<Character> iterator = palindrome.listIterator(); ListIterator<Character> revIterator = palindrome.listIterator(palindrome.size()); boolean result = true; while (revIterator.hasPrevious() && iterator.hasNext()) { if (iterator.next() != revIterator.previous()) { result = false; break; } } if (result) { System.out.print("Input string is a palindrome"); } else { System.out.print("Input string is not a palindrome"); } } }
Черги, стеки, множини й асоціативні контейнери будуть розглянуті пізніше.
2.8.5 Використання функцій класу Collections
Як клас Arrays
для масивів, для колекцій існує допоміжний клас Collections
. Цей клас надає низку функцій для роботи з колекціями, зокрема зі списками.
Велика група функцій призначена для створення колекцій різних типів. Наведений нижче приклад демонструє створення колекцій за допомогою статичних методів класу Collections
: відповідно, порожнього списку (emptyList()
), "одинака" (singletonList()
) і списку тільки для читання зі звичайного (unmodifiableList()
) або колекції тільки для читання (unmodifiableCollection()
):
import java.util.*; public class CollectionsCreationDemo { public static void main(String[] args) { List<Integer> emptyList = Collections.emptyList(); System.out.println(emptyList); // [] List<Integer> singletonList = Collections.singletonList(10); System.out.println(singletonList); // [10] List<Integer> list = new ArrayList<>(Arrays.<Integer>asList(1, 2, 3)); List<Integer> unmodifiableList = Collections.unmodifiableList(list); Collection<Integer> collection = Collections.unmodifiableCollection(list); } }
Всі наведені вище функції створюють набори даних тільки для читання.
Аналогічно працюють методи, що створюють відповідні множини - emptySet()
, singleton()
, unmodifiableSet()
.
Під алгоритмом в бібліотеці колекцій слід розуміти деяку функцію, що реалізує роботу з колекцією (отримання певного результату або перетворення елементів колекції). В Java Collections API ця функція як правило, статична і узагальнена.
Як і клас Arrays
, клас Collections
містить реалізацію статичних функцій sort()
та fill()
. Окрім того, є велика кількість статичних функцій, які дозволяють обробляти списки без застосування циклів. Це, наприклад, такі функції, як max()
(пошук максимального елемента), min()
(пошук мінімального елемента), indexOfSubList()
(пошук індексу першого повного входження підсписку у список), frequency()
(визначення кількості разів входження певного елемента у список), reverse()
(заміна порядку елементів на протилежний), rotate()
(циклічний зсув списку на задану кількість елементів), shuffle()
("тасування" елементів), nCopies()
(створення нового списку з визначеною кількістю однакових елементів). Використання цих функції можна проілюструвати на наступному прикладі:
List<Integer> a = Arrays.asList(0, 1, 2, 3, 3, -4); System.out.println(Collections.max(a)); // 3 System.out.println(Collections.min(a)); // -4 System.out.println(Collections.frequency(a, 2)); // 1 раз System.out.println(Collections.frequency(a, 3)); // 2 рази Collections.reverse(a); // змінюємо порядок елементів на протилежний System.out.println(a); // [-4, 3, 3, 2, 1, 0] Collections.rotate(a, 3); // зсуває список циклічно на 3 елементи System.out.println(a); // [2, 1, 0, -4, 3, 3] List<Integer> sublist = Collections.nCopies(2, 3); // новий список містить 2 трійки System.out.println(Collections.indexOfSubList(a, sublist)); // 4 Collections.shuffle(a); // "тасуємо" елементи System.out.println(a); // елементи в довільному порядку Collections.sort(a); System.out.println(a); // [-4, 0, 1, 2, 3, 3] List<Integer> b = new ArrayList<Integer>(a); Collections.fill(b, 8); System.out.println(b); // [8, 8, 8, 8, 8, 8] Collections.copy(b, a); System.out.println(b); // [-4, 0, 1, 2, 3, 3] System.out.println(Collections.binarySearch(b, 2)); // 3 Collections.swap(b, 0, 5); System.out.println(b); // [3, 0, 1, 2, 3, -4] Collections.replaceAll(b, 3, 10); System.out.println(b); // [10, 0, 1, 2, 10, -4]
Клас Collections
також надає методи спеціально для роботи зі списками, отримання першого і останнього індексу початку входження підсписку в список (indexOfSubList()
, lastIndexOfSubList()
) і т. д.
2.8.6 Сортування масивів і списків
У найпростішому випадку сортування всього масиву за зростанням здійснюється за допомогою функції 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)); } }
У Java 5 Comparable
– це узагальнений інтерфейс. Створення узагальнених класів та інтерфейсів буде розглянуто пізніше. Завдяки узагальненням у функціях, які оголошені в інтерфейсі, замість параметрів типу 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 int compareTo(Rectangle rect) { return Double.compare(area(), rect.area()); } public String toString() { return "[" + width + ", " + height + ", area = " + area() + "]"; } } 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
Інтерфейс Comparator
містить опис методу compare()
з двома параметрами. Функція повинна повернути від'ємне число, якщо перший об'єкт під час сортування необхідно вважати меншим, чим інший, значення 0, якщо об'єкти еквівалентні, і додатне число в протилежному випадку.
У Java 5 Comparator
– це також узагальнений інтерфейс. Під час його реалізації після його імені слід вказувати в кутових дужках тип об'єктів, які ми порівнюємо. Якщо використовувати узагальнення, функція compare()
повинна приймати два аргументи типу параметру узагальнення. Наприклад:
import java.util.Comparator; class Rectangle { private double width, height; public Rectangle(double width, double height) { this.width = width; this.height = height; } public double area() { return width * height; } public String toString() { return "[" + width + ", " + height + ", area = " + area() + "]"; } } class CompareByArea implements Comparator<Rectangle> { @Override public int compare(Rectangle r1, Rectangle r2) { return Double.compare(r1.area(), r2.area()); } } 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, new CompareByArea()); System.out.println(java.util.Arrays.toString(a)); } }
Сортування списків здійснюється аналогічно сортуванню масивів, але відповідна функція sort()
для контейнерів реалізована як статична функція класу Collections
.
2.9 Робота з функціональними інтерфейсами в Java 8
2.9.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.oop.first; 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.oop.first; public class PrintWithLambda { public static void main(String[] args) { FunctionToPrint.printTable(-2.0, 2.0, 0.5, x -> x * x * x); } }
2.9.2 Використання посилань на методи
Дуже часто все тіло лямбда-виразу складається лише з виклику існуючого методу. У цьому випадку замість лямбда-виразу можна використовувати посилання на цей метод. Існує кілька варіантів опису посилань на методи.
Вид посилання на метод |
Синтаксис |
Приклад |
Посилання на статичний метод |
|
|
Посилання на нестатичний метод для заданого об'єкта |
|
|
Посилання на нестатичний метод для параметра |
|
|
Посилання на конструктор |
|
|
Наприклад, є такі функціональні інтерфейси:
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.9.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
, який використовують у багатопотоковому програмуванні, а також багато інших.
Можна здійснювати композицію лямбда-виразів (використовувати лямбда-вирази як параметри). З цією метою інтерфейси пакету java.util.function
надають методи з усталеною реалізацією, що забезпечують виконання деякої функції, переданої як параметр до або після даного методу. Зокрема, в інтерфейсі Function
визначені такі методи:
// Виконується функція before, а потім функція, що викликає: Function compose(Function before) // Функція after виконується після функції, що викликає: Function andThen(Function after)
Використання цих методів та їх відмінність розглянемо на такому прикладі. Є клас зі статичною функцією calc()
, що приймає функціональний інтерфейс і аргумент типу Double
. Можна здійснити композицію лямбда-виразів:
package ua.inf.iwanoff.oop.first; 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
3 Приклади програм
3.1 Ієрархія об'єктів реального світу
Припустимо, необхідно розробити ієрархію класів "Регіон" – "Населений район" – "Країна". Окремі класи цієї ієрархії можуть стати базовими для інших класів (наприклад "Незаселений острів", "Національний парк", "Адміністративний район", "Автономна республіка" і т.д.). Ієрархію класів можна доповнити класами "Місто" і "Острів". Доцільно в кожен клас додати конструктор, який ініціалізує усі поля. Можна також створити масив посилань на різні об'єкти ієрархії і для кожного об'єкта вивести на екран рядок даних про нього.
Для того, щоб одержати рядкове представлення об'єкта, необхідно перекрити функцію toString()
Можна запропонувати таку ієрархію класів.
package ua.inf.iwanoff.oop.first; 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; } 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()); } 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; } 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; } 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; } 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
. Таке рішення – правильне з точки зору організації структури даних, але не достатньо ефективне, оскільки воно припускає розміщення в динамічної пам'яті як самого масиву, так і окремих об'єктів-точок. Альтернативні варіанти – використання двох масивів, двовимірного масиву тощо.
Остаточне рішення про структуру даних може бути прийнято тільки в контексті конкретного завдання. Поліморфізм дозволяє реалізувати необхідні алгоритми без прив'язування до конкретної структури даних. Для цього створюємо абстрактний клас, в якому функції доступу оголошені як абстрактні, а алгоритми сортування й виведення в рядок реалізовані з використанням абстрактних функцій доступу. Крім того можна визначити функцію для тестування. Для того щоб був згенерований абстрактний клас, у вікні майстра нового класу обираємо опцію abstract. Додаємо до шаблону необхідний код. Відповідний абстрактний клас буде таким:
package ua.inf.iwanoff.oop.first; 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
. У тому ж проекті створюємо клас ArrayOfPointObjects
. У вікні New Java Class вибираємо опції public і abstract, у рядку Superclass уводимо AbstractArrayOfPoints
. Крім того, доцільно вибрати опції public static void main(String[] args) і Inherited abstract methods. Отримаємо такий код:
package ua.inf.iwanoff.oop.first; public class ArrayOfPointObjects extends AbstractArrayOfPoints { @Override public void setPoint(int i, double x, double y) { // TODO Auto-generated method stub } @Override public double getX(int i) { // TODO Auto-generated method stub return 0; } @Override public double getY(int i) { // TODO Auto-generated method stub return 0; } @Override public int count() { // TODO Auto-generated method stub return 0; } @Override public void addPoint(double x, double y) { // TODO Auto-generated method stub } @Override public void removeLast() { // TODO Auto-generated method stub } public static void main(String[] args) { // TODO Auto-generated method stub } }
Клас для представлення точки можна додати в той же пакет. Клас Point
міститиме два поля і конструктор:
package ua.inf.iwanoff.oop.first; 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
створюємо поле – посилання на масив Point
та ініціалізуємо його порожнім масивом. Реалізація більшості функцій видається очевидною. Найбільшу складність являють функції додавання та видалення точок. В обох випадках необхідно створити новий масив потрібної довжини й переписати в нього вміст старого. У функції main()
здійснюємо тестування. Весь код файлу AbstractArrayOfPoints.java
матиме такий вигляд:
package ua.inf.iwanoff.oop.first; 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.oop.first; public class ArrayWithTwoArrays extends AbstractArrayOfPoints { private double[] ax = { }; private double[] ay = { }; @Override public void setPoint(int i, double x, double y) { if (i < count()) { ax[i] = x; ay[i] = y; } } @Override public double getX(int i) { return ax[i]; } @Override public double getY(int i) { return ay[i]; } @Override public int count() { return ax.length; // Можна ay.length, вони однакові } @Override public void addPoint(double x, double y) { double[] ax1 = new double[ax.length + 1]; System.arraycopy(ax, 0, ax1, 0, ax.length); ax1[ax.length] = x; ax = ax1; double[] ay1 = new double[ay.length + 1]; System.arraycopy(ay, 0, ay1, 0, ay.length); ay1[ay.length] = y; ay = ay1; } @Override public void removeLast() { if (count() == 0) { return; } double[] ax1 = new double[ax.length - 1]; System.arraycopy(ax, 0, ax1, 0, ax1.length); ax = ax1; double[] ay1 = new double[ay.length - 1]; System.arraycopy(ay, 0, ay1, 0, ay1.length); ay = ay1; } public static void main(String[] args) { new ArrayWithTwoArrays().test(); } }
Результати мають бути ідентичними.
3.3 Використання інтерфейсів з усталеною реалізацією методів
Припустимо, необхідно знайти корінь рівняння методом дотичних (Ньютона). Цей метод передбачає використання першої і другої похідної функції для знаходження кореня. Наближене значення першої похідної будь-якої функції можна знайти за формулою
f '(x) = (f(x + dx) – f(x)) / dx
Чим менше dx, тим точніше буде знайдена похідна. Другу похідну можна знайти як похідну першої похідної.
Алгоритм полягає в такому: на заданому відрізку пошуку знаходимо початкове наближення. Це буде початок відрізка (якщо знак функції і другої похідної в цій точці збігаються) або кінець відрізка (в іншому випадку). Далі обчислюємо наступні наближення за такою формулою:
xn+1 = xn – f(xn) / f '(xn)
Описуємо інтерфейс. Обчислення першої і другої похідної здійснюється методами з усталеною реалізацією:
package ua.inf.iwanoff.oop.first; 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.oop.first; 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.oop.first; public class FirstImplementation implements FunctionWithDerivatives { @Override public double f(double x) { return Math.sin(x - 0.5); } public static void main(String[] args) { System.out.println(Newton.solve(0, 1, 0.000001, new FirstImplementation())); } }
Для функцій можна перевизначити механізм обчислення першої і другої похідної. Наприклад, для кубічного поліному
f(x) = x3 -
6x2 + 12x -
9
можна так визначити першу і другу похідну:
f '(x) = 3x2 -
12x +
12
f ''(x) = 6x -
12
Тоді клас, який реалізує інтерфейс, може бути таким:
package ua.inf.iwanoff.oop.first; public class SecondImplementation implements FunctionWithDerivatives { @Override public double f(double x) { return x * x * x - 6 * x * x + 12 * x - 9; } @Override public double f1(double x) { return 3 * x * x - 12 * x + 12; } @Override public double f2(double x) { return 6 * x - 12; } public static void main(String[] args) { System.out.println(Newton.solve(0, 1, 0.000001, new SecondImplementation())); } }
Явне визначення похідних може підвищити ефективність алгоритму.
3.4 Розв'язання рівняння методом дихотомії
3.4.1 Постановка задачі
Припустимо, необхідно розв'язати методом дихотомії (ділення відрізка навпіл) довільне рівняння.
f(x) = 0
Метод дихотомії дозволяє знайти лише один корінь рівняння. Якщо коренів немає, або їх більше одного, результати можуть бути не достовірними.
Чисельні методи рішення рівнянь припускають багаторазове обчислення в різних точках значень функціональної залежності f(x), яка визначає ліву частину рівняння. Функція f(x) може мінятися в різних задачах. Необхідно реалізувати механізм передачі інформації про цю залежність класу, який відповідає за розв'язання рівняння.
3.4.2 Використання абстрактного класу
Перший варіант ґрунтується на використанні абстрактного класу. Створюємо новий клас – AbstractEquation
, який містить абстрактну функцію f()
і функцію розв'язання рівняння – solve()
:
package ua.inf.iwanoff.oop.first; 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.oop.first; public class SpecificEquation extends AbstractEquation { 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 Використання інтерфейсу і класу, який його реалізує
Інтерфейси пропонують альтернативний шлях рішення цієї проблеми. Ми можемо описати інтерфейс для представлення лівої частини рівняння. Для створення інтерфейсу в середовищі Eclіpse використовується функція New | Interafce.
package ua.inf.iwanoff.oop.first; public interface LeftSide { double f(double x); }
Клас Solver
реалізує статичний метод для розв'язання рівняння:
package ua.inf.iwanoff.oop.first; 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.oop.first; class MyEquation implements LeftSide { 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.oop.first; 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.oop.first; 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.oop.first; 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.oop.first; 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 Узагальнена функція пошуку певного елементу
Припустимо, необхідно реалізувати статичну узагальнену функцію отримання індексу першого входження елемента з визначеним значенням. Функція повинна повертати індекс цього елемента або -1, якщо елемент відсутній. Клас із необхідною функцією матиме такий вигляд:
package ua.inf.iwanoff.oop.first; public class ElementFinder { public static <E>int indexOf(E[] arr, E elem) { for (int i = 0; i < arr.length; i++) { if (arr[i].equals(elem)) { return i; } } return -1; } public static void main(String[] args) { Integer[] a = {1, 2, 11, 4, 5}; System.out.println(indexOf(a, 11)); // 2 System.out.println(indexOf(a, 12)); // -1 String[] b = {"one", "two"}; System.out.println(indexOf(b, "one")); // 0 } }
Для порівняння значень об'єктів слід використовувати метод equals()
замість ==
.
3.6 Сума елементів типу Double
Наступна програма читає дійсні числа, які вводить користувач, заносить їх у список та знаходить суму.
package ua.inf.iwanoff.oop.first; import java.util.*; public class SumOfElements { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); List<Double> a = new ArrayList<>(); double d = 1; // початкове значення не повинне бути 0 while (d != 0) { d = scanner.nextDouble(); a.add(d); } double sum = 0; for (double x : a) { // неявний ітератор sum += x; } System.out.println("Сума: " + sum); } }
Читання чисел з клавіатури здійснюється доти, доки користувач не введе значення 0.
3.7 Індекс максимального елемента
Наступна програма знаходить номер максимального елемента в списку цілих чисел. Для заповнення списку можна використати масив з початковими значеннями елементів. Масив неявно створюється під час виконання функції asList()
.
package ua.inf.iwanoff.oop.first; import java.util.*; public class MaxElement { public static void main(String[] args) { List<Integer> a = Arrays.asList(2, 3, -7, 8, 11, 0); int indexOfMax = 0; for (int i = 1; i < a.size(); i++) { if (a.get(i) > a.get(indexOfMax)) { indexOfMax = i; } } System.out.println(indexOfMax + " " + a.get(indexOfMax)); } }
Оскільки шукаємо індекс, не доцільно застосовувати ітератор.
3.8 Ієрархія класів "Країна" та "Перепис населення"
Припустимо, необхідно спроектувати ієрархію класів в якій описуються класи для представлення країни та перепису населення. Оскільки передбачені різні варіанти зберігання даних про країни і переписи населення, доцільно створити ієрархію класів. Базові абстрактні класи для опису країни та перепису населення не повинні містити будь-яких даних.
У класі AbstractCensus
будуть оголошені абстрактні методи доступу до даних. У цьому класі можна реалізувати функції отримання представлення у вигляді рядку, перевірки еквівалентності, перевірки наявності слів і послідовності літер у коментарях і тестування. Для забезпечення сортування за збільшенням населення слід реалізувати інтерфейс Comparable
і у функції compareTo()
забезпечити "природне" порівняння за кількістю населення. Похідний клас, який представляє перепис, повинен містити поля даних.
У класі AbstractCountry
, який також не містить даних, слід описати абстрактні методи доступу і сортування. У цьому класі слід реалізувати функції отримання представлення у вигляді рядку, перевірки еквівалентності, обчислення щільності населення згідно з певним переписом, визначення перепису з найбільшою кількістю населення, а також перевірки входження певного слова в коментарі.
Похідні класи, які описують країну, повинні відповідно представляти послідовність елементів у вигляді масиву та списку. Необхідно реалізувати функції сортування переписів за кількістю населення та за алфавітом коментарів, використовуючи стандартні засоби сортування масивів і колекцій.
Створюємо новий проект і додаємо новий пакет ua.inf.iwanoff.oop.first
. Створюємо абстрактний клас AbstractCensus
. Його код (частково запозичений з раніше створеного класу Census
прикладу лабораторної роботи № 5 попереднього семестру) буде таким:
public abstract class AbstractCensus implements Comparable<AbstractCensus> { @Override public String toString() { return "Перепис " + getYear() + " року. Населення: " + getPopulation() + ". Коментар: " + getComments(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || !(obj instanceof AbstractCensus)) { return false; } AbstractCensus c = (AbstractCensus) obj; return c.getYear() == getYear() && c.getPopulation() == getPopulation() && c.getComments().equals(getComments()); } @Override public int compareTo(AbstractCensus c) { return Integer.compare(getPopulation(), c.getPopulation()); } public abstract String getComments(); public abstract void setComments(String comments); public abstract int getPopulation(); public abstract void setPopulation(int population); public abstract int getYear(); public abstract void setYear(int year); public boolean containsWord(String word) { StringTokenizer st = new StringTokenizer(getComments()); String s; while (st.hasMoreTokens()) { s = st.nextToken(); if (s.equalsIgnoreCase(word)) { return true; } } return false; } public boolean containsSubstring(String substring) { return getComments().toUpperCase().indexOf(substring.toUpperCase()) >= 0; } 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("Україна"); } }
Як видно з наведеного коду, клас AbstractCensus
реалізує інтерфейс Comparable<AbstractCensus>
. Реалізація цього інтерфейсу вимагає додавання методу compareTo()
, в якому визначене "природне" порівняння – за кількістю населення.
Похідний клас CensusWithData
містить опис полів даних і реалізацію методів доступу, а також метод main()
:
package ua.inf.iwanoff.oop.first; public class CensusWithData extends AbstractCensus { private int year; private int population; private String comments; public CensusWithData() { } public CensusWithData(int year, int population, String comments) { this.year = year; this.population = population; this.comments = comments; } @Override public int getYear() { return year; } @Override public void setYear(int year) { this.year = year; } @Override public int getPopulation() { return population; } @Override public void setPopulation(int population) { this.population = population; } @Override public String getComments() { return comments; } @Override public void setComments(String comments) { this.comments = comments; } public static void main(String[] args) { new CensusWithData().testCensus(); } }
Клас AbstractCountry
також містить методи equals()
і toString()
. У класі розташовуємо допоміжну статичну функцію addToArray()
, яка додає до масиву новий елемент. Реалізуємо всі функції, не залежні від внутрішнього представлення даних. Код класу AbstractCountry
буде таким:
package ua.inf.iwanoff.oop.first; import java.util.Arrays; public abstract class AbstractCountry { public abstract String getName(); public abstract void setName(String name); public abstract double getArea(); public abstract void setArea(double area); public abstract AbstractCensus getCensus(int i); public abstract void setCensus(int i, AbstractCensus census); public abstract boolean addCensus(AbstractCensus census); public abstract boolean addCensus(int year, int population, String comments); public abstract int censusesCount(); public abstract void clearCensuses(); public abstract void sortByPopulation(); public abstract void sortByComments(); public abstract void setCensuses(AbstractCensus[] censuses); public abstract AbstractCensus[] getCensuses(); public static AbstractCensus[] addToArray(AbstractCensus[] arr, AbstractCensus item) { AbstractCensus[] newArr; if (arr != null) { newArr = new AbstractCensus[arr.length + 1]; System.arraycopy(arr, 0, newArr, 0, arr.length); } else { newArr = new AbstractCensus[1]; } newArr[newArr.length - 1] = item; return newArr; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || !(obj instanceof AbstractCountry)) { return false; } AbstractCountry c = (AbstractCountry) obj; if (!getName().equals(c.getName()) || getArea() != c.getArea()) { return false; } return Arrays.equals(getCensuses(), c.getCensuses()); } @Override public String toString() { String result = getName() + ". Територія: " + getArea() + " кв. км."; for (int i = 0; i < censusesCount(); i++) { result += "\n" + getCensus(i); } return result; } public double density(int year) { for (int i = 0; i < censusesCount(); i++) { if (year == getCensus(i).getYear()) { return getCensus(i).getPopulation() / getArea(); } } return 0; } public int maxYear() { AbstractCensus census = getCensus(0); for (int i = 1; i < censusesCount(); i++) { if (census.getPopulation() < getCensus(i).getPopulation()) { census = getCensus(i); } } return census.getYear(); } public AbstractCensus[] findWord(String word) { AbstractCensus[] result = null; for (AbstractCensus census : getCensuses()) { if (census.containsWord(word)) { result = addToArray(result, census); } } return result; } private void printWord(String word) { AbstractCensus[] result = findWord(word); if (result == null) { System.out.println("Слово \"" + word + "\" не міститься в коментарях."); } else { System.out.println("Слово \"" + word + "\" міститься в коментарях:"); for (AbstractCensus census : result) { System.out.println(census); } } } 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); } }
У похідному класі для визначення ознаки сортування (за алфавітом коментарів) замість створення окремого класу використовуємо лямбда вираз. Код класу CountryWithArray
буде таким:
package ua.inf.iwanoff.oop.first; import java.util.Arrays; public class CountryWithArray extends AbstractCountry { private String name; private double area; private AbstractCensus[] censuses; @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } @Override public double getArea() { return area; } @Override public void setArea(double area) { this.area = area; } @Override public AbstractCensus getCensus(int i) { return censuses[i]; } @Override public void setCensus(int i, AbstractCensus census) { censuses[i] = census; } @Override public boolean addCensus(AbstractCensus census) { if (getCensuses() != null) { for (AbstractCensus c : getCensuses()) { if (c.equals(census)) { return false; } } } setCensuses(addToArray(getCensuses(), census)); return true; } @Override public boolean addCensus(int year, int population, String comments) { AbstractCensus census = new CensusWithData(year, population, comments); return addCensus(census); } @Override public int censusesCount() { return censuses.length; } @Override public void clearCensuses() { censuses = null; } @Override public AbstractCensus[] getCensuses() { return censuses; } @Override public void setCensuses(AbstractCensus[] censuses) { this.censuses = censuses; } @Override public void sortByPopulation() { Arrays.sort(censuses); } @Override public void sortByComments() { Arrays.sort(censuses, (c1, c2) -> c1.getComments().compareTo(c2.getComments()); } public static void main(String[] args) { new CountryWithArray().createCountry().testCountry(); } }
Аналогічно створюємо клас CountryWithList
. Для перевірки умови сортування можна скористатись стандартним статичним методом Comparator.comparing()
і посиланням на метод getComments()
. Такий підхід можливий як для масивів, так і для списків. Отримаємо такий код:
package ua.inf.iwanoff.oop.first; import java.util.*; public class CountryWithList extends AbstractCountry { private String name; private double area; private List<AbstractCensus> list = new ArrayList<>(); @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } @Override public double getArea() { return area; } @Override public void setArea(double area) { this.area = area; } @Override public AbstractCensus getCensus(int i) { return list.get(i); } @Override public void setCensus(int i, AbstractCensus census) { list.set(i, census); } @Override public boolean addCensus(AbstractCensus census) { return list.add(census); } @Override public boolean addCensus(int year, int population, String comments) { return list.add(new CensusWithData(year, population, comments)); } @Override public int censusesCount() { return list.size(); } @Override public void clearCensuses() { list.clear(); } @Override public void sortByPopulation() { Collections.sort(list); } @Override public void sortByComments() { Collections.sort(list, Comparator.comparing(AbstractCensus::getComments)); } @Override public void setCensuses(AbstractCensus[] censuses) { list = new ArrayList<>(Arrays.asList(censuses)); } @Override public AbstractCensus[] getCensuses() { return list.toArray(new AbstractCensus[0]); } public static void main(String[] args) { new CountryWithList().createCountry().testCountry(); } }
Обидва класи повинні забезпечити однаковий результат під час тестування.
4 Вправи для контролю
- Створити ієрархію класів Книга та Підручник. Реалізувати конструктори та функції доступу. Перекрити функцію
toString()
. У функціїmain()
створити масив, який містить елементи різних типів. Вивести елементи на екран. - Створити ієрархію класів Кінофільм і Серіал. Реалізувати конструктори та функції доступу. Перекрити функцію
toString()
. У функціїmain()
створити масив, який містить елементи різних типів. Вивести елементи на екран. - Створити ієрархію класів Місто та Столиця. Реалізувати конструктори та функції доступу. Перекрити функцію
toString()
. У функціїmain()
створити масив, який містить елементи різних типів. Вивести елементи на екран. - Реалізувати статичну узагальнену функцію отримання індексу останнього входження елемента з визначеним значенням. Здійснити тестування функції на двох масивах різних типів.
- Реалізувати статичну узагальнену функцію заміни порядку елементів на протилежний. Здійснити тестування функції на двох масивах різних типів.
- Реалізувати статичну узагальнену функцію визначення кількості разів входження певного елемента у масив. Здійснити тестування функції на двох масивах різних типів.
- Реалізувати статичну узагальнену функцію циклічного зсуву масиву на задану кількість елементів. Здійснити тестування функції на двох масивах різних типів.
- Реалізувати статичну узагальнену функцію пошуку індексу елемента, починаючи з якого деякий масив повністю входить в інший. Здійснити тестування функції на двох масивах різних типів.
- Прочитати з клавіатури значення елементів списку цілих чисел. Знайти добуток елементів.
- Проініціалізувати список дійсних чисел масивом зі списком початкових значень. Знайти суму додатних елементів.
- Проініціалізувати список цілих чисел масивом зі списком початкових значень. Знайти добуток ненульових елементів.
- Проініціалізувати список цілих чисел масивом зі списком початкових значень. Створити новий список, складений з парних елементів вихідного списку.
- Проініціалізувати список дійсних чисел масивом зі списком початкових значень. Створити новий список, складений з додатних елементів вихідного списку.
- Проініціалізувати список рядків масивом зі списком початкових значень. Знайти та вивести індекс рядка з найменшою довжиною.
- Проініціалізувати список цілих чисел масивом зі списком початкових значень. Знайти суму максимального і мінімального елементів. Застосувати функції класу
Collections
. - Проініціалізувати список рядків масивом зі списком початкових значень. Змінити порядок елементів на зворотний. Застосувати функцію класу
Collections
.
5 Контрольні запитання
- У яких випадках доцільно використовувати композицію класів?
- Чи можна в Java цілком розмістити один об'єкт усередині іншого об'єкта?
- У чому полягає зміст успадкування?
- У чому є сенс наявності спільного базового класу?
- Які елементи базового класу не успадковуються?
- Як здійснити ініціалізацію базового класу?
- Як викликати однойменний метод базового класу з похідного?
- Які є недоліки й переваги множинного успадкування. Чи допускається множинне успадкування класів у Java?
- Які можливості надає використання поліморфізму?
- Чим віртуальна функція відрізняється від невіртуальної? Як у Java указати, що функція віртуальна?
- Чому функції з модифікатором
private
не є віртуальними? - Чи можна створити абстрактний клас без абстрактних методів?
- Чи можуть абстрактні класи містити неабстрактні методи?
- Чим відрізняються інтерфейси від абстрактних класів? У чому переваги інтерфейсів у порівнянні з абстрактними класами?
- Чи можуть інтерфейси містити поля?
- Чи допускається множинне успадкування інтерфейсів?
- Яким вимогам повинен відповідати клас, який реалізує інтерфейс? Чи може клас реалізовувати кілька інтерфейсів?
- Для чого використовують усталену реалізацію функцій інтерфейсу?
- У чому є сенс застосування анотацій?
- Які є правила перевірки еквівалентності двох об'єктів?
- Чи може в одному файлі з вихідним текстом бути визначене більш одного відкритого класу?
- Чи можна створювати об'єкт нестатичного внутрішнього класу, не створюючи об'єкту обхопного класу?
- Чим відрізняються статичні вкладені класи від внутрішніх?
- Чи можуть статичні вкладені класи містити нестатичні елементи?
- Чи можна створювати класи усередині інтерфейсів та інтерфейси всередині класів?
- Чи є локальний клас статичним? Чи є безіменний клас статичним?
- Чому не можна створити явний конструктор безіменного класу? Як здійснити ініціалізацію полів безіменного класу?
- У яких випадках доцільно створювати узагальнені класи?
- Чи можна створювати об'єкти й масиви узагальнених типів?
- Що таке колекція? Які базові інтерфейси описані в пакеті
java.util
? - Чому в колекції не можна зберігати цілі і дійсні числа безпосередньо, а можна тільки посилання?
- Як зберегти у списку цілі і дійсні значення?
- У чому перевага опису посилання на інтерфейс контейнера (наприклад,
List
) у порівнянні з описом посилання на клас, що реалізує контейнер (наприклад,ArrayList
)? - У чому перевага використання ітераторів у порівнянні з індексами елементів контейнера?
- Коли доцільніше використовувати
ArrayList
у порівнянні зLinkedList
і навпаки? - У чому переваги використання стандартних алгоритмів класу
Collections
? - Яким вимогам повинен задовольняти об'єкт, щоб масив таких об'єктів можна було сортувати без визначення ознаки сортування?
- Як визначити спеціальне правило для сортування елементів масиву?
- Що таке лямбда-вираз? Які переваги надають лямбда-вирази?
- Що таке функціональний інтерфейс?
- Для чого використовуються посилання на методи?
- Як створити посилання на нестатичний метод, статичний метод, конструктор?