Skip to content

3. [TD]: Класи

Ключові слова: клас, інтерфейс, успадкування, виняток, поліморфізм

Рекомендована література:

  • параграфи 2.1, 2.2, 2.4 та 2.7 розділу 2 книги «[ref1]: Класи та інтерфейси»
  • параграфи 3.3 (клас String), 3.5 (клас ArrayList), 3.6 (клас Arrays)

У частині 1 вправи ELECTIONS не використовувався жоден клас. Ми побудували рішення так, як би ми це зробили на мові C. Тепер ми вводимо поняття класу в Java.

3.1. Support

 

Папка [support / chap-03] містить проект Eclipse для цього розділу.

Відтепер ми працюватимемо з версією JDK 1.8, оскільки деякі з наступних проектів вимагають саме цю версію JDK. Щоб дізнатися, яка версія JDK використовується, виконайте такі дії:

  • в [4] — використовується JRE (Java Runtime Environment). Цей JRE насправді є JDK (Java Development Kit), а тут — [jdk1.8.0_60]. Якщо це не JDK або у вас версія нижче 1.8, виконайте наступні дії: [5-21];
  • на [8], JRE, який наразі використовується Eclipse за замовчуванням;
  • на [11] — різні JDK та JRE, які наразі відомі Eclipse;
  • у [15] виберіть JDK замість JRE. У цьому документі використовуються проекти Maven, яким потрібен JDK;
  • у [21] міститься JDK версії >=1.8;
  • у [22-23] перейдіть до фасетів (різних видів одного й того самого проєкту Eclipse) проєкту;
  • у [24] переконайтеся, що ви використовуєте версію Java >=1.8;

3.2. Клас [ListeElectorale]

У мові C ми, ймовірно, використали б структуру для представлення списку учасників виборів. Вона могла б мати такий вигляд:

struct t_liste
   {
     char nom[15];
     long voix;
     int  elimine;
     int sieges;
   };

Поняття «структура» не існує в мові Java. Його потрібно замінити поняттям «клас». Тому вирішено створити клас для зберігання інформації про список кандидатів. Він матиме такий каркас:


package istia.st.elections;

public class ListeElectorale {

    /**
     * identité de la liste
     */
    private int id;

    /**
     * nom de la liste
     */
    private String nom;
    /**
     * nombre de voix de la liste
     */
    private int voix;
    /**
     * nombre de sièges de la liste
     */
    private int sieges;
    /**
     * indique si la liste est éliminée ou non
     */
    private boolean elimine;

    /**
     * constructeur par défaut
     */
    public ListeElectorale() {
    }

    /**
     *
     * @param nom String : le nom de la liste
     * @param voix int : son nombre de voix
     * @param sieges int : son nombre de sieges
     * @param elimine boolean : son état éliminé ou non
     */
    public ListeElectorale(int id,String nom, int voix, int sieges, boolean elimine) {
...
    }

    /**
     *
     * @return int : l'identifiant de la liste
     */
    public int getId() {
...
    }

    /**
     * initialise l'identifiant de liste
     * @param id int : identifiant de la liste
     * @throws ElectionsException si id<1
     */
    public void setId(int id) {
...
    }

    /**
     *
     * @return String : le nom de la liste
     */
    public String getNom() {
...
    }

    /**
     * initialise le nom de la liste
     * @param nom String : nom de la liste
     *  @throws ElectionsException si le nom est vide ou blanc
     */
    public void setNom(String nom) {
...
    }

    /**
     *
     * @return int : le nombre de voix de la liste
     */
    public int getVoix() {
 ...
    }

    /**
     * initialise le nombre de voix de la liste
     * @param voix int : le nombre de voix de la liste
     */
    public void setVoix(int voix) {
 ...
    }

    /**
     *
     * @return int : le nombre de sièges de la liste
     */
    public int getSieges() {
 ...
    }

    /**
     * fixe le nombre de sièges de la liste
     * @param sieges int : le nombre de sièges de la liste
     */
    public void setSieges(int sieges) {
...
    }

    /**
     *
     * @return boolean : valeur du champ elimine
     */
    public boolean isElimine() {
  ...
    }

    /**
     *
     * @param sieges int
     */
    public void setElimine(boolean elimine) {
 ...
    }

    /**
     *
     * @return String : identité de la liste électorale
     */
    public String toString() {
   ...
    }
}
  • рядок 8: номер, що однозначно ідентифікує список. Тут він не є обов’язковим, але передбачений для використання в майбутньому.
  • рядок 13: назва списку.
  • рядок 17: кількість голосів, отриманих списком
  • рядок 21: кількість місць у списку
  • рядок 25: логічне значення, що вказує, чи список виключено (відсоток отриманих голосів нижче виборчого порогу), чи ні.

Кожне приватне поле з іменем [xyz] можна ініціалізувати за допомогою методу з іменем [setXyz]. Метод [getXyz] дозволяє отримати значення приватного поля [xyz]. У конкретному випадку, коли [xyz] є полем булевого типу, метод [getXyz] можна замінити на метод [isXyz]. Особлива назва цих методів відповідає стандарту кодування, який називається стандартом JavaBean. Таким чином, ми визначаємо такі публічні методи:

  • getId (рядок 48), setId (рядок 57)
  • getNom (рядок 65), setNom (рядок 74)
  • getVoix (рядок 82), setVoix (рядок 90)
  • getSieges (рядок 98), setSieges (рядок 106)
  • isElimine (рядок 114), setElimine (рядок 122)
  • рядки 30–31: визначають конструктор без параметрів. Він дозволяє створити об’єкт [ListeElectorale] без його ініціалізації. Потім цей об’єкт можна ініціалізувати за допомогою методів set.
  • рядки 40–42: визначають конструктор, що дозволяє створити об’єкт [ListeElectorale], одночасно ініціалізуючи його п’ять приватних полів.
  • рядки 130–132: визначають метод [toString], який повертає рядок, що містить значення п’яти полів об’єкта.

Програма для тестування класу ListeElectorale може виглядати так:


package istia.st.elections.tests;

import istia.st.elections.ListeElectorale;

public class MainTest1ListeElectorale {
    public static void main(String[] args) {
        // створення виборчого списку
        ListeElectorale listeElectorale1 = new ListeElectorale(1, "A", 32000,
                0, false);
        // відображення назви списку
        System.out.println("listeElectorale1=" + listeElectorale1);
        // зміна кількості місць
        listeElectorale1.setSieges(2);
        // перегляд даних списку № 1
        System.out.println("listeElectorale1=" + listeElectorale1);
        // новий виборчий список
        ListeElectorale listeElectorale2 = listeElectorale1;
        // перегляд даних списку 2
        System.out.println("listeElectorale2=" + listeElectorale2);
        // зміна кількості місць
        listeElectorale2.setSieges(3);
        // відображення ідентифікаційних даних обох списків
        System.out.println("listeElectorale2=" + listeElectorale2);
        System.out.println("listeElectorale1=" + listeElectorale1);
    }
}

Середовище Eclipse для цього тесту може виглядати так:

  • [1]: проект називається [elections-02A]
  • [2]: додаток буде розміщено в пакеті, в даному випадку [istia.st.elections]
  • [3]: [ListeElectorale.java] — це вихідний код класу [ListeElectorale]
  • [4]: тестові класи будуть розміщені в пакеті, в даному випадку [istia.st.elections.tests]
  • [5]: тестовий клас [MainTest1ListeElectorale]

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

Image


Завдання: скориставшись наведеним вище, доповніть код класу ListeElectorale.


3.3. Створення класу винятків [ElectionsException]

Серед різних класів винятків мови Java є один, що називається [RuntimeException]. Цей клас походить від класу [Exception], який є кореневим для всіх класів винятків. Особливістю екземплярів класу [RuntimeException] або похідних від нього класів є те, що їх не обов’язково оголошувати чи обробляти. Їх називають неконтрольованими винятками.

Розглянемо перший приклад. Клас [BufferedReader] — це клас, екземпляри якого дозволяють зчитувати рядки тексту з потоку даних. Він має метод [readLine], сигнатура якого така:

public String readLine()throws IOException

Бачимо, що метод може викликати виняток типу [IOException]. Дерево цього класу має такий вигляд:

1
2
3
4
java.lang.Object
  java.lang.Throwable
      java.lang.Exception
          java.io.IOException

Клас [IOException] походить від класу [Exception] (рядок 3). Компілятор змушує нас обробляти та оголошувати винятки типу [java.lang.Exception] або похідні від нього (за винятком гілки [RuntimeException], яку ми розглянемо далі). Отже, щоб прочитати рядок тексту, введений з клавіатури, доведеться написати щось на кшталт:

1
2
3
4
5
6
7
8
BufferedReader clavier=....;
String ligne=null;
try{
    ligne=clavier.readLine();
}catch (IOException ex){
     // обробка винятку
    ....
}

Розглянемо інший приклад. Щоб перетворити рядок на ціле число, можна використати статичний метод [Integer.parseInt], сигнатура якого така:

public static int parseInt(String s) throws NumberFormatException

Аргументом [s] є рядок символів, який потрібно перетворити на ціле число. Бачимо, що метод може викликати виняток типу [NumberFormatException]. Дерево цього класу має такий вигляд:

1
2
3
4
5
6
java.lang.Object
  java.lang.Throwable
      java.lang.Exception
          java.lang.RuntimeException
              java.lang.IllegalArgumentException
                  java.lang.NumberFormatException

Клас [NumberFormatException] походить від класу [RuntimeException] (рядок 4). Компілятор не змушує нас обробляти та оголошувати винятки типу [java.lang.RuntimeException] або похідних від нього. Таким чином, можна написати щось на кшталт:

1
2
3
4
5
6
7
8
9
BufferedReader clavier=....;
String ligne=null;
try{
    ligne=clavier.readLine();
}catch (IOException ex){
     // обробка винятку
    ....
}
int age=Integer.parseInt(ligne);

Ми не зобов’язані додавати оператор [try - catch] для обробки можливого винятку, згенерованого [Integer.parseInt] (рядок 9).

Створення та використання класів винятків, похідних від [RuntimeException], має свої переваги та недоліки:

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

Ми вирішили створити спеціальний клас, який об’єднає всі винятки, що можуть виникнути в нашому додатку ELECTIONS. Він називатиметься [ElectionsException] і буде похідним від класу [RuntimeException]. Його код такий:


package istia.st.elections;

public class ElectionsException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    public ElectionsException() {
        super();
    }

    public ElectionsException(String message) {
        super(message);
    }

    public ElectionsException(Throwable cause) {
        super(cause);
    }

    public ElectionsException(String message, Throwable cause) {
        super(message, cause);
    }
}
  • рядок 1: розміщуємо клас у пакеті [istia.st.elections];
  • рядок 3: клас походить від [RuntimeException]. Отже, він є неконтрольованим;
  • рядок 4: ідентифікатор серіалізації, який наразі можна проігнорувати;
  • у нашому додатку ми будемо використовувати два типи конструкторів:
    • класичний, як показано в рядках 15–17 нижче:
throw new ElectionsException("Le nombre de sièges doit être >0")

У цьому випадку метод, який викликає метод, що генерує таке виключення, може обробити його наступним чином:


        // тестування винятку
        try {
            listeElectorale2.setSieges(-3);
        } catch (ElectionsException ex) {
            System.err.println("L'exception suivante s'est produite : ["
                    + ex.toString() + "]");
        }
  • (продовження)
    • або код рядків 14–20, призначений для передачі винятку, що вже виник, шляхом його інкапсуляції у виняток типу [ElectionsException]:

    try {
        ...;
        } catch (SQLException ex) {
            // інкапсуляція винятку
            throw new ElectionsException("erreur de fermeture de la connexion à la BD",ex);
        }

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


        try {
            ...;
        } catch (ElectionsException ex) {
            System.out.println(ex.getMessage() + ", Cause : "+ ex.getCause().getMessage());
            System.exit(1);
        }

Завдання: переробіть код класу ListeElectorale таким чином, щоб методи set генерували виняток типу [ElectionsException], якщо запитувана ініціалізація є некоректною, наприклад, ініціалізація імені порожнім рядком.


Тестовий проект Eclipse для цієї нової версії може виглядати так:

  • [1]: проект називається [elections-02B]
  • [2]: додаток розміщений у пакеті, в даному випадку [istia.st.elections]
  • [3]: класи [ListeElectorale] та [ElectionsException]
  • [4]: тестові класи розміщені в пакеті, в даному випадку [istia.st.elections.tests]
  • [5]: тестовий клас [MainTest1ListeElectorale]

Вже розглянутий тестовий клас [MainTest1ListeElectorale] дещо змінено для тестування виняткових випадків:


package istia.st.elections.tests;

import istia.st.elections.ElectionsException;
import istia.st.elections.ListeElectorale;

public class MainTest1ListeElectorale {
    public static void main(String[] args) {
        // створення виборчого списку
        ListeElectorale listeElectorale1 = new ListeElectorale(1, "A", 32000,
                0, false);
        // відображення даних списку
        System.out.println("listeElectorale1=" + listeElectorale1);
        // зміна кількості місць
        listeElectorale1.setSieges(2);
        // відображення даних списку 1
        System.out.println("listeElectorale1=" + listeElectorale1);
        // новий виборчий список
        ListeElectorale listeElectorale2 = listeElectorale1;
        // перегляд даних списку 2
        System.out.println("listeElectorale2=" + listeElectorale2);
        // зміна кількості місць
        listeElectorale2.setSieges(3);
        // відображення ідентифікаційних даних обох списків
        System.out.println("listeElectorale2=" + listeElectorale2);
        System.out.println("listeElectorale1=" + listeElectorale1);
        // тест на виняток
        try {
            listeElectorale2.setSieges(-3);
        } catch (ElectionsException ex) {
            System.err.println("L'exception suivante s'est produite : ["
                    + ex.toString() + "]");
        }

    }
}
  • рядок 28: робиться спроба ініціалізувати кількість місць забороненим значенням
  • рядок 30: якщо виникає виняток, його виводиться на екран

Виконання тесту дає такі результати:

Image

Можна помітити, що клас [ListeElectorale] дійсно згенерував виняток, коли ми спробували ініціалізувати кількість місць за допомогою недійсного значення (рядок 28 коду).

3.4. Клас модульного тестування

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

Додаток розвивається з часом. При кожному оновленні потрібно перевіряти, чи додаток не «регресує», c.a.d, чи він продовжує успішно проходити тести на працездатність, які були виконані під час його початкового написання. Такі тести називаються «тестами на відсутність регресії». Досить великий додаток може потребувати сотень тестів. Фактично тестується кожен метод кожного класу додатка. Це називається модульними тестами. Вони можуть задіяти багато розробників, якщо не були автоматизовані.

Для автоматизації тестування було розроблено спеціальні інструменти. Один із них називається [JUnit]. Це бібліотека класів, призначена для управління тестуванням. Ми будемо використовувати цей інструмент для тестування класу [ListeElectorale].

Програма тестування JUnit (версії 4.x) має такий вигляд:


package istia.st.elections.tests;

import org.junit.Assert;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class JUnitEssai {

    @Before
    public void avant() throws Exception {
        System.out.println("tearUp");
    }

    @After
    public void après() throws Exception {
        System.out.println("tearDown");
    }

    @Test
    public void t1() {
        System.out.println("test1");
        Assert.assertEquals(1, 1);
    }

    @Test
    public void t2() {
        System.out.println("test2");
        Assert.assertEquals(1, 2);
    }

}
  • рядок 1: клас розміщено в пакеті [istia.st.elections.tests];
  • рядок 11: метод, анотований анотацією [@Before], виконується перед кожним модульним тестом;
  • рядок 16: метод, позначений анотацією [@After], виконується після кожного модульного тесту;
  • рядок 21: метод, позначений анотацією [@Test], є методом, що тестується модульним тестом. Методи, позначені анотацією [@Test], виконуватимуться один за одним, якщо тестувальник не вкаже інше; він може самостійно вибрати методи для тестування. Перед кожним виконанням методу [@Test] виконується метод [@Before]. Після кожного виконання методу [@Test] виконується метод [@After];
  • рядки 22–25: визначають метод тестування [t1];
  • рядок 18: один із методів [Assert.assert*], що дозволяє перевіряти твердження. Існують такі методи [assert]:
    • assertEquals(вираз1, вираз2): перевіряє, чи значення обох виразів рівні. Підтримуються різні типи виразів (int, String, float, double, boolean, char, short). Якщо обидва вирази не рівні, то генерується виняток типу [AssertionFailedError ],
    • assertEquals(число1, число2, дельта): перевіряє, чи два числа з дійсної області дорівнюють одне одному з похибкою дельта, c.a.d abs(число1-число2)<=дельта. Наприклад, можна написати assertEquals(число1, число2, 1E-6), щоб перевірити, чи два значення дорівнюють з похибкою 10⁻⁶,
    • assertEquals(message, expression1, expression2) та assertEquals(message, réel1, réel2, delta) — це варіанти, що дозволяють вказати повідомлення про помилку, яке буде пов’язане з винятком типу [AssertionFailedError], що генерується у разі невдачі методу [assertEquals],
    • assertNotNull(Object) та assertNotNull(message, Object): перевіряє, чи Object не дорівнює null,
    • assertNull(Object) та assertNull(message, Object): перевіряють, чи Object дорівнює null,
    • assertSame(Object1, Object2) та assertSame(message, Object1, Object2): перевіряють, чи посилання Object1 та Object2 вказують на один і той самий об’єкт,
    • assertNotSame(Object1, Object2) та assertNotSame(message, Object1, Object2): перевіряє, чи посилання Object1 та Object2 не вказують на один і той самий об’єкт;
  • рядок 24: ця перевірка має пройти успішно;
  • рядок 30: ця перевірка повинна завершитися невдачею;

У середовищі Eclipse створення тестового класу JUnit можна здійснити таким чином:

  • [1]: клацніть правою кнопкою миші на пакеті, до якого потрібно додати тестовий клас, а потім виберіть опцію [JUnit / New / JUnit Test Case]
  • [1]: вибір версії JUnit;
  • [2]: вибір папки, в якій має бути створено тестовий клас;
  • [3]: вибір пакета, в якому має бути створено тестовий клас;
  • [4]: назва класу тестування;
  • [5]: вибір методів, які слід включити до класу, що буде згенеровано;
  • [6]: клас JUnitEssai було згенеровано

Попередній майстер генерує майже порожній клас:


package istia.st.elections.tests;

import org.junit.Assert;
import org.junit.After;
import org.junit.Before;

public class JUnitEssai {

    @Before
    public void setUp() throws Exception {        
    }

    @After
    public void tearDown() throws Exception {
    }
}

Доповнимо та змінимо попередній код наступним чином:


package istia.st.elections.tests;

import org.junit.Assert;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class JUnitEssai2 {

    @Before
    public void avant() throws Exception {
        System.out.println("tearUp");
    }

    @After
    public void après() throws Exception {
        System.out.println("tearDown");
    }

    @Test
    public void t1() {
        System.out.println("test1");
        Assert.assertEquals(1, 1);
    }

    @Test
    public void t2() {
        System.out.println("test2");
        Assert.assertEquals(1, 2);
    }

}

У Eclipse можна запустити тестовий клас, клацнувши на ньому правою кнопкою миші та вибравши опцію [Run as / JUnit test]:

Image

Результати виконання цього тесту такі:

Image

Як бачимо вище, метод [test2] завершився з помилкою. Кожного разу, коли тест завершується з помилкою, до нього додається повідомлення про помилку. Для [test2] це повідомлення, показане вище. У повідомленні вказано номер рядка, у якому сталася помилка (рядок 30). У рядку 30 виклик, що завершився з помилкою, був таким:


    Assert.assertEquals(1, 2);

Перший параметр називається очікуваним значенням, другий — фактичним значенням. Повідомлення про помилку [test2], наведене вище, вказує, що очікуване значення становило 2, а фактичне — 3.

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

Image

Ці повідомлення свідчать про те, що методи [@Before] та [@After] були успішно викликані відповідно перед та після кожного тестового методу.

Тестові класи не обов’язково пишуть самі розробники. Їх можуть створювати особи, які склали технічні вимоги до додатка. Деякі методи розробки, відомі як TDD (Test Driven Development), рекомендують писати тестові класи ще до написання класів, що тестуються. Це іноді дозволяє уточнити специфікації, які інакше могли б трактуватися по-різному.

Створимо тест JUnit 4, який називатиметься [JUnitTest1ListeElectorale], для класу [ListeElectorale]. У середовищі Eclipse діятимемо так, як описано раніше:

Ми доповнюємо код, згенерований майстром, наступним чином:


package istia.st.elections.tests;

import org.junit.Assert;
import istia.st.elections.ElectionsException;
import istia.st.elections.ListeElectorale;

import org.junit.Test;

public class JUnitTest1ListeElectorale {

    @Test
    public void t1() {
        // створення виборчого списку
        ListeElectorale liste = new ListeElectorale(1, "a", 32000, 0, false);
        // перевірки
        Assert.assertEquals("a", liste.getNom());
        Assert.assertEquals(32000, liste.getVoix());
        Assert.assertEquals(false, liste.isElimine());
        Assert.assertEquals(0, liste.getSieges());
        // перевірка дійсності ідентифікатора
        boolean erreur = false;
        try {
            liste.setId(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // перевірка дійсності імені
        erreur = false;
        try {
            liste.setNom("");
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // перевірка дійсності голосів
        erreur = false;
        try {
            liste.setVoix(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // перевірка дійсності місць
        erreur = false;
        try {
            liste.setSieges(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
    }

}

Виконання тесту дає такий результат:

Image

Тести пройшли успішно. Відтепер ми вважатимемо, що маємо робочий клас [ListeElectorale].

3.5. MainElections: версія 2

Рекомендована література:

  • параграфи 2.1, 2.2, 2.4 та 2.7 розділу 2 книги «[1]: Класи та інтерфейси»
  • параграфи 3.3 (клас String), 3.5 (клас ArrayList), 3.6 (клас Arrays)

Потрібно переписати програму [Elections], додавши до неї такі нові обмеження:

  • для представлення списку кандидатів буде використовуватися клас [ListeElectorale]
  • додаток запитуватиме з клавіатури таку інформацію:
  • кількість вакантних місць
  • назви та голоси списків. Заздалегідь невідомо, скільки саме списків. Останній список буде позначений назвою, що дорівнює рядку «*».
  • Оскільки заздалегідь невідомо, скільки буде списків, спочатку їх буде збережено в об’єкті типу [ArrayList]. Потім, коли всі списки будуть введені, їх буде перенесено до масиву списків.
  • Результати будуть відображатися у порядку спадання кількості отриманих місць.

Для сортування масиву T доступні різні статичні методи класу [Arrays]:

  • Arrays.sort(T): сортує масив T за природним порядком, якщо такий існує (за зростанням для чисел, дат, за алфавітом для рядків тощо)
  • Arrays.sort(T,comparateur): для сортування масивів T, що не мають природного порядку. Це стосується масиву списків, які потрібно впорядкувати за певним полем списку: кількістю отриманих місць.

У методі Arrays.sort(T,компаратор) параметр «компаратор» є об’єктом, що реалізує наступний інтерфейс Comparator:

Image

  • метод compare дозволяє порівняти два елементи масиву T
  • метод equals дозволяє визначити, чи є два об’єкти рівними

Обидва методи порівнюють об’єкти типу Object obj1 та obj2. Визначення того, що obj1<obj2, obj1=obj2 чи obj1>obj2 є меншим, залежить від відношення порядку, яке ми хочемо встановити між цими двома об’єктами. Саме розробник, який реалізує цей інтерфейс, повинен вказати, як ми можемо визначити, що:

  • obj1 менший за obj2
  • obj1 більший за obj2
  • obj1 дорівнює obj2

Клас Object, від якого походить будь-який клас Java, вже має метод [equals]. Для сортування масиву T об’єктів типу O метод [equals] класу O не потрібен. Тому можна залишити реалізацію за замовчуванням, надану класом Object. У такому разі потрібно реалізувати лише метод [compare]. Цей метод неодноразово викликається методом [Arrays.sort]. Останній щоразу передає як параметри obj1 та obj2 методу compare — два елементи масиву T, що підлягають сортуванню. У нашому випадку ці елементи будуть типу [ListeElectorale]. Тут слід звернути увагу на прояв поліморфізму. Метод [compare] визначений як такий, що приймає параметри типу [Object]. Це означає, що вона може приймати параметри типу [Object] або похідних від нього (поліморфізм). Оскільки [Object] є батьківським класом усіх класів Java, фактичні параметри можуть мати тип [ListeElectorale].

Для сортування за зростанням метод [compare] повинен повертати:

  • -1, якщо obj1 менше за obj2
  • +1, якщо obj1 більше за obj2
  • 0, якщо obj1 дорівнює obj2

Для сортування у спадному порядку значення +1 і -1 міняються місцями. Терміни «менший за», «більший за», «дорівнює» виражають відношення порядку. Для об’єктів типу [ListeElectorale] буде існувати відношення «список1 менший за список2», якщо список1 має менше голосів, ніж список2.

У тому самому вихідному файлі, що й клас [MainElections], можна додати другий клас:

// клас порівняння виборчих списків
class CompareListesElectorales implements Comparator {

     // порівняння двох виборчих списків за кількістю голосів
    public int compare(Object obj1, Object obj2) {
         // отримання виборчих списків
        ListeElectorale listeElectorale1 = (ListeElectorale) obj1;
        ListeElectorale listeElectorale2 = (ListeElectorale) obj2;
         // порівняння голосів за цими двома списками
....        
    }
}
  • рядок 2: клас не оголошено публічним. У файлі вихідного коду Java може бути кілька класів, але лише один із них може мати атрибут public — той, що має назву файлу вихідного коду.

У попередньому методі compare параметри мають тип Object,, що змушує рядки 7 і 8 виконувати перетворення типів параметрів методу з типу Object у тип ListeElectorale. Сигнатура методу compare визначається інтерфейсом Comparator, який було написано для порівняння довільних об’єктів. Починаючи з версії JDK 1.5 існує загальний інтерфейс Comparator: Comparator<T>, де T — це довільний тип Java. Метод compare інтерфейсу Comparator<T> порівнює об’єкти типу T, а не типу Object, що дозволяє уникнути попередніх перетворень типів. Клас для порівняння об’єктів типу ListeElectorale може виглядати так:


// клас порівняння виборчих списків
class CompareListesElectorales implements Comparator<ListeElectorale> {

    // порівняння двох виборчих списків за кількістю місць
    public int compare(ListeElectorale listeElectorale1,
            ListeElectorale listeElectorale2) {
...
    }
}
  • рядок 2: клас реалізує інтерфейс Comparator<ListeElectorale>
  • рядки 5–6: параметри методу compare мають тип ListeElectorale. Перетипізація тепер не потрібна.

У версії JDK 1.5 було введено поняття генеричних класів/інтерфейсів для різних класів/інтерфейсів версії JDK 1.4, які спочатку оперували виключно об’єктами типу Object. Це стосується списків, словників тощо.

Трохи вище ми зазначали, що, оскільки кількість списків невідома, їх неможливо зберігати в масиві. Їх можна зберігати в об’єкті ArrayList, який реалізує поняття «списку об’єктів». Цей клас зберігає об’єкти типу Object. Починаючи з версії JDK 1.5, існують типізовані списки об’єктів. Отже, для зберігання списків перед їх перенесенням у масив буде використовуватися об’єкт ArrayList<ListeElectorale>. Якщо цей масив має назву tListes, його сортування буде здійснено за допомогою інструкції:


// сортування списків
Arrays.sort(tListes, new CompareListesElectorales());

де CompareListesElectorales — це клас, що реалізує інтерфейс Comparator<ListeElectorale>.


Завдання: перепишіть додаток [Elections] з урахуванням цих нових специфікацій.


Проект Eclipse може виглядати так:

Приклад виконання [1] виглядає так:

Image