en

Завдання для самостійної роботи

Використання записів, sealed-класів та зіставлення за зразком

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

Модифікувати раніше створену програму індивідуального завдання лабораторних робіт № 3 і № 4 побудувавши код з використанням записів (record), sealed-класів, зіставлення зі зразком та безіменних змінних. Обмежитися реалізацією двох похідних класів на свій розсуд.

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

2.1 Записи

Починаючи з JDK 14, до синтаксису Java додано нову конструкцію – так званий запис (record). Фактично це спрощений клас, призначений для зберігання даних тільки для читання. Для створення запису використовують спеціальний синтаксис:

record SomeRec(список полів - формальних параметрів конструктора) {
}

Запис надає конструктор з параметрами, функції доступу для читання, а також методи toString(), equals() і hashCode(). Запис дозволяє програмісту явно реалізовувати необхідні конструктори та інші методи, а також статичні функції. Можна також додавати статичні поля.

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

public class CircleReadOnly {
    private double radius;

    public CircleReadOnly(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }

    public double area() {
        return Math.PI * radius * radius;
    }

    public double perimeter() {
        return 2 * Math.PI * radius;
    }
}

Створення запису дозволяє скоротити необхідний код:

public record CircleRecord(double radius) {
    // поле radius створено автоматично

    public double area() {
        return Math.PI * radius * radius;
    }

    public double perimeter() {
        return 2 * Math.PI * radius;
    }
}

У наведеній нижче програмі здійснюється демонстрація обох реалізацій. Під час використання запису замість гетера (getRadius()) застосовано автоматично створений метод radius():

public class TestReadOnly {
    static void main() {
        // Робота з класом:
        CircleReadOnly circle1 = new CircleReadOnly(10);
        System.out.println(circle1.getRadius());
        System.out.println(circle1.area());
        System.out.println(circle1.perimeter());

        // Робота з записом:
        CircleRecord circle2 = new CircleRecord(10);
        System.out.println(circle2.radius());
        System.out.println(circle2.area());
        System.out.println(circle2.perimeter());
    }
}

Якщо необхідно, до запису можна додати конструктор без параметрів. Він повинен викликати автоматично створений конструктор:

public record CircleRecord(double radius) {
    
    public CircleRecord() {
        this(20);
    }

    // ...
}

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

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

Записи найчастіше застосовують для реалізації патерну проєктування DTO (Data Transfer Object), який використовують для передачі структурованих даних між різними рівнями програми. Об'єкти передачі даних (DTO) не містять логіки (лише дані). Об'єкти лише для читання є надійними з точки зору багатопотоковості. Записи часто застосовують у застосунках баз даних для відображення рядків реляційних таблиць в об'єкти Java.

2.2 Sealed-класи

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

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

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

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

final class FirstDerived extends SealedBase {

}

sealed class SecondDerived extends SealedBase permits SomeSubclass {

}

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

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

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

}

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

non-sealed class SecondDerived extends SealedBase {

}

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

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

2.3 Клонування

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

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

package ua.inf.iwanoff.java.additional;

import java.util.Arrays;

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

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

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

package ua.inf.iwanoff.java.additional;

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

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

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

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

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

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

// ...

Human human2 = human1.clone();

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

package ua.inf.iwanoff.java.additional;

import java.util.Arrays;

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

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

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

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

2.4 Зіставлення зі зразком

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

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

Механізм зіставлення зі зразком вперше з'явився у Java 14. У подальших версіях цей механізм доповнювався додатковими можливостями. У загальному випадку цей механізм дозволяє зробити код більш компактним і виразним. Крім того, код стає більш декларативним та безпечним, об’єднавши три розрізнені операції в один крок.

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

Object obj;
// ...
obj = new String("Some text");

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

// ...
if (obj instanceof String) {  // перевірка типу
    String s = (String) obj;  // приведення об'єкта до потрібного типу
    if (s.length() > 5) {     // Використання
        System.out.println(s);
    }
}

Новий підхід передбачає поєднання цих дій в одному виразі:

if (obj instanceof String s && s.length() > 5) {
    System.out.println(s); // змінна s вже готова
}

Окрім instanceof, зіставлення за зразком використовують у перемикачах (switch) для перевірки реального типу об'єктів. Наприклад, можна не тільки передбачити різні дії з певним об'єктом, але й скористатися значенням, на яке посилається obj:

switch (obj) {
    case String s:
        System.out.println("Рядок: " + s);
        break;
    case Integer i:
        System.out.println("Число: " + i);
        break;
    default: System.out.println("Невідомий тип");
}

Можна додатково перевіряти умови, пов'язані з конкретними типами. Це здійснюється за допомогою контекстно-залежного ключового слова when:

switch (obj) {
    case Integer i when i > 0:
        System.out.println("Додатне число: " + i);
        break;
    case Integer i when i < 0:
        System.out.println("Від'ємне число: " + i);
        break;
    default: System.out.println("Невідомий тип або нуль");
}

У Java 25 є додаткова можливість використовувати примітивні типи для такої перевірки (primitive patterns). Це дозволяє безпечно працювати з числами різних типів. Наведена нижче функція getGrade() дозволяє отримувати оцінку за кількістю балів:

String getGrade(Number n) {
    return switch (n) {
        case int i when i >= 90 -> "A";
        case int i when i >= 82 -> "B";
        case int i when i >= 75 -> "C";
        case int i when i >= 64 -> "D";
        case int i when i >= 60 -> "E";
        case double d when d >= 59.5 -> "E (rounded)";
        default -> "F/FX";
    };
}

void main() {
    int mark = 88;
    System.out.println(getGrade(mark)); // B
    double roundedMark = 59.9;
    System.out.println(getGrade(roundedMark)); // E (rounded)
}

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

  • Безпека типів: компілятор гарантує, що змінна буде доступна лише там, де вона точно ініціалізована; виняток ClassCastException не буде згенеровано.
  • Лаконічність: кількість однотипного коду (boilerplate) істотно зменшується.
  • Вичерпність (Exhaustiveness): у парі з sealed класами компілятор перевіряє, чи всі типи були перевірені у перемикачі.

Зіставлення за зразком для записів (record patterns) дозволяє витягли потрібні поля запису (розпакувати запис) прямо в конструкціях instanceof та switch, без викликів гетерів. Наприклад, маємо такий запис:

record Pair(double x, double y) {
}

Припустимо, було створене посилання на та ініціалізовано об'єктом типу запису:

Object obj = new Pair(1, 2);

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

if (obj instanceof Pair) {
    Pair p = (Pair) obj;
    double x = p.x();
    double y = p.y();
    System.out.printf("x = %f  y = %f\n", x, y);
}

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

if (obj instanceof Pair p) {
    double x = p.x();
    double y = p.y();
    System.out.printf("x = %f  y = %f\n", x, y);
}

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

if (obj instanceof Pair(double x, double y)) {
    System.out.printf("x = %f  y = %f\n", x, y);
}

Аналогічно можна застосувати зіставлення зі зразком для записів у конструкції switch:

record Pair(double x, double y) {
}

// ...

public void printData(Object obj) {
    switch (obj) {
        case Pair(double x, double y) ->
                System.out.println("Пара: " + x + ", " + y);
        case String s ->
                System.out.println("Рядок: " + s);
        default ->
                System.out.println("Невідомий об'єкт");
    }
}

2.5 Безіменні змінні

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

Безіменні змінні та патерни (Unnamed Variables and Patterns), які позначаються символом підкреслення _, були введені починаючи з Java 21 для запобігання захаращення коду змінними, які потрібні технічно, але не потрібні логічно.

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

if (obj instanceof Pair(double x, double _)) {
    System.out.println("x = " + x);
}

Безіменні змінні часто доцільно використовувати для опису об'єкта-винятка, коли нас цікавить не сам об'єкт, а лише його тип. Наприклад;

String s = new Scanner(System.in).next();
try {
    double d = Double.parseDouble(s);
    System.out.println(d);
}
catch (NumberFormatException _) {
    System.out.println("Wrong data!");;
}

Якщо лямбда-функція приймає два аргументи, але використовує лише один, другий можна замінити на _.

// Ігноруємо значення, використовуємо лише ключ у Map
map.forEach((key, _) -> System.out.println("Key: " + key));

// Або в циклах, де індекс не важливий
for (var _ : list) {
    count++;
}

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

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

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

Для перепису населення замість класу можна запропонувати запис. Це істотно скоротить сирцевий код:

package ua.inf.iwanoff.java.additional;

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

    @Override
    public int compareTo(Census census) {
        return Integer.compare(population, census.population);
    }
}

Окремий клас з функціями пошуку даних в коментарях зазнав мінімальних змін. Замість getComments() ми використовуємо автоматично створений метод comments():

package ua.inf.iwanoff.java.additional;

import java.util.Arrays;

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

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

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

Базовий абстрактний клас Country ми переробимо так, щоб він дозволяв створити лише два похідних класи – CountryWithArray і CountryWithArrayList. Код класу Country може бути таким:

package ua.inf.iwanoff.java.additional;

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

Клас CountryWithArray тепер повинен бути фінальним:

package ua.inf.iwanoff.java.additional;

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

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

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

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

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

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

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

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

Клас CountryWithArrayList також повинен бути фінальним:

package ua.inf.iwanoff.java.additional;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Клас для представлення країни, в якій здійснюється перепис населення.
 * Дані про переписи представлені списком ArrayList
 */
public final class CountryWithArrayList extends Country {
    private List<Census> censusesList = new ArrayList<>();

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

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

    /**
     * Додає посилання на новий перепис в кінець послідовності
     * @param census посилання на новий перепис
     * @return {@code true}, якщо посилання вдалося додати
     *         {@code false} в протилежному випадку
     */
    @Override
    public boolean addCensus(Census census) {
        if (censusesList.contains(census)) {
            return false;
        }
        return censusesList.add(census);
    }

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

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

    /**
     * Переписує дані з масиву переписів у послідовність
     * @param censusesArray довільний масив переписів
     */

    @Override
    public void setCensusesArray(Census[] censusesArray) {
        censusesList = new ArrayList<>(Arrays.asList(censusesArray));
    }

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

    /**
     * Повертає список переписів
     * @return список посилань на переписи
     */
    public List<Census> getCensusesList() {
        return censusesList;
    }

    /**
     * Заносить список переписів в об'єкт
     * @param censusesList довільний список переписів
     */
    public void setCensusesList(List<Census> censusesList) {
        this.censusesList = censusesList;
    }
}

Клас практично не потребує змін. Тільки під час роботи з переписами замість getYear() і getPopulation() ми використовуємо year() і population():

package ua.inf.iwanoff.java.additional;

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

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

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

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

    /**
     * Здійснює сортування послідовності переписів за кількістю населення
     *
     * @param country посилання на країну
     */
    public static void sortByPopulation(Country country) {
        Census[] censuses = country.getCensusesArray();
        Arrays.sort(censuses);
        country.setCensusesArray(censuses);
    }

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

Клас також майже не потребує змін (окрім гетерів для переписів):

package ua.inf.iwanoff.java.additional;

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

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

Код класу CountryDemo з функцією main() буде схожий на код класу PolymorphismDemo з прикладу лабораторної роботи № 3. Але замість класу CountryWithLinkedList ми скористаємося класом CountryWithArrayList. Головні відмінності торкнуться реалізації функції main(), у якій залежно від цілого числа, введеного користувачем (1 або 2) ми використовуємо один з раніше створених похідних класів. Якщо користувач увів неправильне ціле число, він отримує повідомлення ""Неправильний номер!". Якщо замість цілого числа введено рядок, який не можна перетворити на ціле значення, виникає виняток, обробка якого містить виведення повідомлення "Некоректні символи!".

package ua.inf.iwanoff.java.additional;

import java.util.InputMismatchException;
import java.util.Scanner;

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

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

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

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

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

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

    /**
     * Демонстрація роботи програми
     */
    static void main() {
        System.out.print("Уведіть Ваш вибір: 1 - CountryWithArray, 2 - CountryWithArrayList ");
        Scanner scanner = new Scanner(System.in);
        try {
            int i = scanner.nextInt();
            Country country = switch (i) {
                case 1 -> setCountryData(new CountryWithArray());
                case 2 -> setCountryData(new CountryWithArrayList());
                default -> null;
            };
            if (country == null) {
                System.out.println("Неправильний номер!");
                return;
            }
            String s = switch (country) {
                case CountryWithArray _ -> "------ Country With Array ------";
                case CountryWithArrayList _ -> "------ Country With Array List ------";
            };
            System.out.println(s);
            testSearch(country);
            testSorting(country);
        }
        catch (InputMismatchException _) {
            System.out.println("Некоректні символи!");
        }
    }
}

У другій конструкції ми скористалися механізмом зіставлення зі зразком та безіменними змінними. В обробці винятку також застосовано безіменну змінну.

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

  1. У чому переваги записів у порівнянні з класами, призначеними для об'єктів тільки для читання?
  2. Чи можна створити декілька конструкторів у записі?
  3. Які синтаксичні обмеження пов'язані із записами?
  4. Яка мета створення sealed-класів?
  5. Поясніть використання ключового слова non-sealed.
  6. У чому необхідність клонування об'єктів?
  7. Які методи інтерфейсу Cloneable обов'язково треба визначати?
  8. Коли треба вручну реалізовувати метод clone()?
  9. У чому полягає ідея механізму зіставлення зі зразком (pattern matching)?
  10. У яких мовних конструкціях можна використовувати зіставлення зі зразком?
  11. У чому сенс зіставлення за зразком для записів?
  12. Як і для чого використовують безіменні змінні?

 

up