Skip to content

3. [TD]: Klasy

Słowa kluczowe: klasa, interfejs, dziedziczenie, wyjątek, polimorfizm

Zalecana lektura:

  • akapity 2.1, 2.2, 2.4 i 2.7 rozdziału 2 książki „[ref1]: Klasy i interfejsy”
  • akapity 3.3 (klasa String), 3.5 (klasa ArrayList), 3.6 (klasa Arrays)

W części 1 ćwiczenia ELECTIONS nie wykorzystano żadnej klasy. Rozwiązanie skonstruowano tak, jak zrobiono by to w języku C. Teraz wprowadzamy pojęcie klasy w Javie.

3.1. Support

 

Folder [support / chap-03] zawiera projekt Eclipse z tego rozdziału.

Od tej pory będziemy pracować z wersją JDK 1.8, ponieważ niektóre z kolejnych projektów wymagają tej wersji JDK. Aby sprawdzić, która wersja JDK jest używana, należy postępować w następujący sposób:

  • na [4], używany jest JRE (Java Runtime Environment). Ten plik JRE jest w rzeczywistości plikiem JDK (Java Development Kit), a tutaj plikiem [jdk1.8.0_60]. Jeśli nie jest to JDK lub jeśli posiadasz wersję starszą niż 1.8, postępuj zgodnie z poniższymi instrukcjami dotyczącymi [5-21];
  • na [8], czyli JRE, który jest obecnie domyślnie używany przez Eclipse;
  • na [11], różne wersje JDK i JRE znane obecnie przez Eclipse;
  • z [15] wybierz raczej JDK niż JRE. Niniejszy dokument wykorzystuje projekty Maven, które wymagają pliku JDK;
  • w przypadku [21] mamy JDK w wersji >=1.8;
  • w przypadku pliku [22-23] należy przejść do aspektów (różnych widoków tego samego projektu Eclipse) projektu;
  • w [24] sprawdź, czy używasz wersji Java >=1.8;

3.2. Klasa [ListeElectorale]

W języku C prawdopodobnie użylibyśmy struktury do przedstawienia listy kandydatów biorących udział w wyborach. Mogłaby ona mieć następującą postać:

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

W języku Java nie ma pojęcia struktury. Należy je zastąpić pojęciem klasy. Postanawiamy zatem utworzyć klasę do przechowywania informacji o liście kandydatów. Jej szkielet wyglądałby następująco:


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() {
   ...
    }
}
  • wiersz 8: numer jednoznacznie identyfikujący listę. Nie jest tu niezbędny, ale przewidziano go do wykorzystania w przyszłości.
  • wiersz 13: nazwa listy.
  • wiersz 17: liczba głosów oddanych na listę
  • wiersz 21: liczba mandatów listy
  • wiersz 25: wartość logiczna wskazująca, czy lista została wyeliminowana (procent uzyskanych głosów poniżej progu wyborczego), czy nie.

Każde pole prywatne o nazwie [xyz] można zainicjować za pomocą metody o nazwie [setXyz]. Metoda [getXyz] pozwala natomiast uzyskać wartość pola prywatnego [xyz]. W szczególnym przypadku, gdy [xyz] jest polem typu boolowskiego, metodę [getXyz] można zastąpić metodą [isXyz]. Sposób nazewnictwa tych metod jest zgodny z normą kodowania o nazwie JavaBean. W ten sposób definiujemy następujące metody publiczne:

  • getId (wiersz 48), setId (wiersz 57)
  • getNom (wiersz 65), setNom (wiersz 74)
  • getVoix (wiersz 82), setVoix (wiersz 90)
  • getSieges (wiersz 98), setSieges (wiersz 106)
  • isElimine (wiersz 114), setElimine (wiersz 122)
  • wiersze 30–31: definiują konstruktor bez parametrów. Pozwala on na utworzenie obiektu [ListeElectorale] bez jego inicjalizacji. Obiekt ten można następnie zainicjować za pomocą metod set.
  • wiersze 40–42: definiują konstruktor umożliwiający utworzenie obiektu [ListeElectorale] wraz z zainicjowaniem jego pięciu pól prywatnych.
  • wiersze 130–132: definiują metodę [toString], która zwraca ciąg znaków zawierający wartości pięciu pól obiektu.

Program testowy klasy ListeElectorale mógłby wyglądać następująco:


package istia.st.elections.tests;

import istia.st.elections.ListeElectorale;

public class MainTest1ListeElectorale {
    public static void main(String[] args) {
        // utworzenie listy wyborczej
        ListeElectorale listeElectorale1 = new ListeElectorale(1, "A", 32000,
                0, false);
        // wyświetlanie nazwy listy
        System.out.println("listeElectorale1=" + listeElectorale1);
        // zmiana liczby mandatów
        listeElectorale1.setSieges(2);
        // wyświetlanie danych listy nr 1
        System.out.println("listeElectorale1=" + listeElectorale1);
        // nowa lista wyborcza
        ListeElectorale listeElectorale2 = listeElectorale1;
        // wyświetlenie danych listy 2
        System.out.println("listeElectorale2=" + listeElectorale2);
        // zmiana liczby mandatów
        listeElectorale2.setSieges(3);
        // wyświetlenie tożsamości obu list
        System.out.println("listeElectorale2=" + listeElectorale2);
        System.out.println("listeElectorale1=" + listeElectorale1);
    }
}

Środowisko Eclipse dla tego testu mogłoby wyglądać następująco:

  • [1]: projekt nosi nazwę [elections-02A]
  • [2]: aplikacja zostanie umieszczona w pakiecie, w tym przypadku [istia.st.elections]
  • [3]: [ListeElectorale.java] to kod źródłowy klasy [ListeElectorale]
  • [4]: klasy testowe zostaną umieszczone w pakiecie, w tym przypadku [istia.st.elections.tests]
  • [5]: klasa testowa [MainTest1ListeElectorale]

Ekran wyświetlany po uruchomieniu powyższego programu wygląda następująco:

Image


Zadanie: korzystając z powyższych informacji, uzupełnij kod klasy ListeElectorale.


3.3. Tworzenie klasy wyjątku [ElectionsException]

Wśród różnych klas wyjątków w języku Java znajduje się jedna o nazwie [RuntimeException]. Klasa ta wywodzi się z klasy [Exception], która jest klasą bazową wszystkich klas wyjątków. Cechą charakterystyczną instancji klasy [RuntimeException] lub instancji od niej pochodnych jest to, że nie ma obowiązku ich deklarowania ani zarządzania nimi. Nazywa się je wyjątkami niekontrolowanymi.

Rozważmy pierwszy przykład. Klasa [BufferedReader] to klasa, której instancje umożliwiają odczytywanie wierszy tekstu ze strumienia danych. Posiada ona metodę [readLine] o następującej sygnaturze:

public String readLine()throws IOException

Widać, że metoda ta może wygenerować wyjątek typu [IOException]. Drzewo tej klasy wygląda następująco:

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

Klasa [IOException] wywodzi się z klasy [Exception] (wiersz 3). Kompilator wymusza na nas obsługę i deklarowanie wyjątków typu [java.lang.Exception] lub pochodnych (z wyjątkiem gałęzi [RuntimeException], którą omówimy później). Tak więc, aby odczytać wiersz tekstu wpisanego z klawiatury, będziemy zmuszeni napisać coś w rodzaju:

1
2
3
4
5
6
7
8
BufferedReader clavier=....;
String ligne=null;
try{
    ligne=clavier.readLine();
}catch (IOException ex){
     // zarządzanie wyjątkiem
    ....
}

Weźmy inny przykład. Aby przekształcić cały ciąg znaków, można użyć metody statycznej [Integer.parseInt], której sygnatura wygląda następująco:

public static int parseInt(String s) throws NumberFormatException

Argumentem [s] jest ciąg znaków, który ma zostać przekształcony w liczbę całkowitą. Widzimy, że metoda ta może wygenerować wyjątek typu [NumberFormatException]. Drzewo tej klasy wygląda następująco:

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

Klasa [NumberFormatException] wywodzi się z klasy [RuntimeException] (wiersz 4). Kompilator nie wymusza na nas obsługi ani deklarowania wyjątków typu [java.lang.RuntimeException] ani pochodnych. Dzięki temu możemy napisać coś takiego:

1
2
3
4
5
6
7
8
9
BufferedReader clavier=....;
String ligne=null;
try{
    ligne=clavier.readLine();
}catch (IOException ex){
     // obsługa wyjątku
    ....
}
int age=Integer.parseInt(ligne);

Nie musimy dodawać klauzuli [try - catch], aby obsłużyć ewentualny wyjątek wygenerowany przez [Integer.parseInt] (wiersz 9).

Tworzenie i stosowanie klas wyjątków pochodnych od [RuntimeException] ma swoje zalety i wady:

  • jeśli chodzi o zalety: kod jest lżejszy
  • wadą jest to, że możemy powrócić do metod języka C, gdzie każda funkcja zwraca kod błędu, z którego niewiele osób korzysta, właśnie po to, by uzyskać lżejszy kod. Gdy wystąpi taki nieobsługiwany błąd, program ulega awarii, zazwyczaj w niezbyt elegancki sposób.

Postanawiamy utworzyć specjalną klasę grupującą wszystkie wyjątki, które mogą wystąpić w naszej aplikacji ELECTIONS. Będzie się ona nazywać [ElectionsException] i będzie pochodną klasy [RuntimeException]. Jej kod wygląda następująco:


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);
    }
}
  • wiersz 1: umieszczamy klasę w pakiecie [istia.st.elections];
  • wiersz 3: klasa ta wywodzi się z klasy [RuntimeException]. Nie jest więc kontrolowana;
  • wiersz 4: identyfikator serializacji, który na razie można pominąć;
  • w naszej aplikacji będziemy używać dwóch rodzajów konstruktorów:
    • klasyczny konstruktor z wierszy 15–17, jak poniżej:
throw new ElectionsException("Le nombre de sièges doit être >0")

W tym przypadku metoda wywołująca metodę generującą taki wyjątek może obsłużyć go w następujący sposób:


        // test wyjątku
        try {
            listeElectorale2.setSieges(-3);
        } catch (ElectionsException ex) {
            System.err.println("L'exception suivante s'est produite : ["
                    + ex.toString() + "]");
        }
  • (ciąg dalszy)
    • lub kod z wierszy 14–20, służący do zgłoszenia wyjątku, który już wystąpił, poprzez zamknięcie go w wyjątku typu [ElectionsException]:

    try {
        ...;
        } catch (SQLException ex) {
            // enkapsulacja wyjątku
            throw new ElectionsException("erreur de fermeture de la connexion à la BD",ex);
        }

Ta druga metoda ma tę zaletę, że zachowuje informacje zawarte w pierwszym wyjątku. W tym przypadku metoda wywołująca metodę generującą taki wyjątek może obsłużyć go w następujący sposób:


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

Zadanie: zmodyfikuj kod klasy ListeElectorale tak, aby metody set zgłaszały wyjątek typu [ElectionsException], jeśli żądana inicjalizacja jest nieprawidłowa, na przykład w przypadku inicjalizacji nazwy pustym ciągiem znaków.


Projekt testowy Eclipse dla tej nowej wersji mógłby wyglądać następująco:

  • [1]: projekt nosi nazwę [elections-02B]
  • [2]: aplikacja znajduje się w pakiecie, w tym przypadku [istia.st.elections]
  • [3]: klasy [ListeElectorale] i [ElectionsException]
  • [4]: klasy testowe są umieszczone w pakiecie, w tym przypadku [istia.st.elections.tests]
  • [5]: klasa testowa [MainTest1ListeElectorale]

Omówiona wcześniej klasa testowa [MainTest1ListeElectorale] została nieznacznie zmodyfikowana w celu przetestowania przypadków wyjątkowych:


package istia.st.elections.tests;

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

public class MainTest1ListeElectorale {
    public static void main(String[] args) {
        // utworzenie listy wyborczej
        ListeElectorale listeElectorale1 = new ListeElectorale(1, "A", 32000,
                0, false);
        // wyświetlanie tożsamości listy
        System.out.println("listeElectorale1=" + listeElectorale1);
        // zmiana liczby mandatów
        listeElectorale1.setSieges(2);
        // wyświetlanie danych listy nr 1
        System.out.println("listeElectorale1=" + listeElectorale1);
        // nowa lista wyborcza
        ListeElectorale listeElectorale2 = listeElectorale1;
        // wyświetlenie danych listy 2
        System.out.println("listeElectorale2=" + listeElectorale2);
        // zmiana liczby mandatów
        listeElectorale2.setSieges(3);
        // wyświetlenie tożsamości obu list
        System.out.println("listeElectorale2=" + listeElectorale2);
        System.out.println("listeElectorale1=" + listeElectorale1);
        // test wyjątku
        try {
            listeElectorale2.setSieges(-3);
        } catch (ElectionsException ex) {
            System.err.println("L'exception suivante s'est produite : ["
                    + ex.toString() + "]");
        }

    }
}
  • wiersz 28: próba zainicjowania liczby miejsc wartością niedozwoloną
  • wiersz 30: jeśli wystąpi wyjątek, jest on wyświetlany

Wykonanie testu daje następujące wyniki:

Image

Można zauważyć, że klasa [ListeElectorale] rzeczywiście wygenerowała wyjątek, gdy próbowano zainicjować liczbę miejsc wartością nieprawidłową (wiersz 28 kodu).

3.4. Klasa testu jednostkowego

Poprzedni rodzaj testu opiera się na weryfikacji wizualnej. Sprawdzamy, czy na ekranie pojawia się to, czego oczekujemy. Jest to metoda odradzana w środowisku zawodowym. Testy powinny być zawsze w jak największym stopniu zautomatyzowane i dążyć do tego, by nie wymagały żadnej interwencji człowieka. Człowiek jest bowiem podatny na zmęczenie, a jego zdolność do weryfikacji testów słabnie w miarę upływu dnia.

Aplikacja ewoluuje z biegiem czasu. Przy każdej zmianie należy sprawdzić, czy aplikacja nie uległa „regresji”, c.a.d, oraz czy nadal przechodzi testy poprawności działania, które zostały przeprowadzone podczas jej pierwotnego tworzenia. Testy te nazywane są testami „nie-regresji”. Nieco większa aplikacja może wymagać setek testów. Testuje się bowiem każdą metodę każdej klasy w aplikacji. Nazywa się to testami jednostkowymi. Mogą one wymagać zaangażowania wielu programistów, jeśli nie zostały zautomatyzowane.

Opracowano narzędzia służące do automatyzacji testów. Jednym z nich jest [JUnit]. Jest to biblioteka klas przeznaczona do zarządzania testami. Wykorzystamy to narzędzie do przetestowania klasy [ListeElectorale].

Program testowy JUnit (wersja 4.x) ma następującą postać:


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);
    }

}
  • wiersz 1: klasa została umieszczona w pakiecie [istia.st.elections.tests];
  • wiersz 11: metoda opatrzona adnotacją [@Before] jest wykonywana przed każdym testem jednostkowym;
  • wiersz 16: metoda opatrzona adnotacją [@After] jest wykonywana po każdym teście jednostkowym;
  • wiersz 21: metoda opatrzona adnotacją [@Test] jest metodą testowaną przez test jednostkowy. Metody opatrzone adnotacją [@Test] będą wykonywane jedna po drugiej, chyba że tester postanowi inaczej, wybierając samodzielnie metody do przetestowania. Przed każdym wykonaniem metody [@Test] wykonywana jest metoda [@Before]. Po każdym wykonaniu metody [@Test] wykonywana jest metoda [@After];
  • wiersze 22–25: definiują metodę testową [t1];
  • wiersz 18: jedna z metod [Assert.assert*], która pozwala na weryfikację asercji. Istnieją następujące metody [assert]:
    • assertEquals(wyrażenie1, wyrażenie2): sprawdza, czy wartości obu wyrażeń są równe. Akceptowanych jest wiele typów wyrażeń (int, String, float, double, boolean, char, short). Jeśli oba wyrażenia nie są równe, generowany jest wyjątek typu [AssertionFailedError ],
    • assertEquals(liczbowa1, liczbowa2, delta): sprawdza, czy dwie liczby rzeczywiste są równe z dokładnością do delta, c.a.d abs(liczbowa1-liczbowa2) <= delta. Można na przykład zapisać assertEquals(liczbowa1, liczbowa2, 1E-6), aby sprawdzić, czy dwie wartości są równe z dokładnością do 10⁻⁶,
    • assertEquals(komunikat, wyrażenie1, wyrażenie2) oraz assertEquals(komunikat, liczba1, liczba2, delta) to warianty pozwalające określić komunikat o błędzie, który ma być powiązany z wyjątkiem typu [AssertionFailedError] zgłaszanym w przypadku niepowodzenia metody [assertEquals],
    • assertNotNull(Object) oraz assertNotNull(message, Object): sprawdza, czy Object nie jest równe null,
    • assertNull(Object) oraz assertNull(message, Object): sprawdzają, czy Object jest równe null,
    • assertSame(Object1, Object2) oraz assertSame(message, Object1, Object2): sprawdzają, czy odwołania Object1 i Object2 wskazują na ten sam obiekt,
    • assertNotSame(Object1, Object2) oraz assertNotSame(message, Object1, Object2): sprawdza, czy referencje Object1 i Object2 nie wskazują na ten sam obiekt;
  • wiersz 24: to sprawdzenie musi zakończyć się powodzeniem;
  • wiersz 30: to sprawdzenie musi zakończyć się niepowodzeniem;

W środowisku Eclipse klasę testową JUnit można utworzyć w następujący sposób:

  • [1]: kliknij prawym przyciskiem myszy na pakiet, do którego chcesz dodać klasę testową, a następnie wybierz opcję [JUnit / New / JUnit Test Case]
  • [1]: wybór wersji JUnit;
  • [2]: wybór folderu, w którym ma zostać utworzona klasa testowa;
  • [3]: wybór pakietu, w którym ma zostać utworzona klasa testowa;
  • [4]: nazwa klasy testowej;
  • [5]: wybór metod, które mają zostać uwzględnione w generowanej klasie;
  • [6]: klasa JUnitEssai została wygenerowana

Poprzedni kreator generuje klasę praktycznie pustą:


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 {
    }
}

Uzupełnijmy i zmodyfikujmy powyższy kod w następujący sposób:


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);
    }

}

W programie Eclipse kliknięcie prawym przyciskiem myszy na klasie testowej, a następnie wybranie opcji [Run as / JUnit test] pozwala na jej uruchomienie:

Image

Wyniki uzyskane po uruchomieniu tego testu są następujące:

Image

Powyżej metoda [test2] zakończyła się niepowodzeniem. Za każdym razem, gdy test kończy się niepowodzeniem, przypisywany jest do niego komunikat o błędzie. W przypadku [test2] jest to komunikat wyświetlony powyżej. Komunikat wskazuje numer wiersza, w którym wystąpił błąd (wiersz 30). W wierszu 30 nieudane wywołanie miało postać:


    Assert.assertEquals(1, 2);

Pierwszy parametr to wartość oczekiwana, a drugi – wartość rzeczywista. Powyższy komunikat o błędzie dla [test2] wskazuje, że wartość oczekiwana wynosiła 2, ale wartość rzeczywista wyniosła 3.

Wreszcie komunikaty wyświetlone na konsoli przez różne metody testowe były następujące:

Image

Komunikaty te wskazują, że metody [@Before] i [@After] zostały rzeczywiście wywołane odpowiednio przed i po każdej metodzie testowej.

Klasy testowe niekoniecznie są pisane przez samych programistów. Mogą je tworzyć osoby, które opracowały specyfikacje aplikacji. Niektóre metody programowania, znane jako TDD (Test Driven Development), zalecają pisanie klas testowych jeszcze przed napisaniem klas, które mają być testowane. Pozwala to czasami na doprecyzowanie specyfikacji, które w przeciwnym razie mogłyby być interpretowane na wiele sposobów.

Stwórzmy test JUnit 4, o nazwie [JUnitTest1ListeElectorale], dla klasy [ListeElectorale]. W środowisku Eclipse postępujemy zgodnie z opisem powyżej:

Uzupełniamy kod wygenerowany przez kreatora w następujący sposób:


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() {
        // utworzenie listy wyborczej
        ListeElectorale liste = new ListeElectorale(1, "a", 32000, 0, false);
        // weryfikacje
        Assert.assertEquals("a", liste.getNom());
        Assert.assertEquals(32000, liste.getVoix());
        Assert.assertEquals(false, liste.isElimine());
        Assert.assertEquals(0, liste.getSieges());
        // weryfikacja poprawności identyfikatora
        boolean erreur = false;
        try {
            liste.setId(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // weryfikacja poprawności nazwiska
        erreur = false;
        try {
            liste.setNom("");
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // weryfikacja ważności głosów
        erreur = false;
        try {
            liste.setVoix(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // weryfikacja poprawności liczby mandatów
        erreur = false;
        try {
            liste.setSieges(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
    }

}

Wykonanie testu daje następujący wynik:

Image

Testy zakończyły się powodzeniem. Od tej pory uznamy, że mamy działającą klasę [ListeElectorale].

3.5. MainElections: wersja 2

Zalecana lektura:

  • punkty 2.1, 2.2, 2.4 i 2.7 rozdziału 2 dokumentu [1]: Klasy i interfejsy
  • punkty 3.3 (klasa String), 3.5 (klasa ArrayList), 3.6 (klasa Arrays)

Chcemy przepisać aplikację [Elections], dodając do niej następujące nowe ograniczenia:

  • do reprezentacji listy kandydatów zostanie wykorzystana klasa [ListeElectorale]
  • aplikacja będzie prosić o wprowadzenie z klawiatury następujących informacji:
  • liczba miejsc do obsadzenia
  • nazwy i głosy z list. Nie wiadomo z góry, ile jest list. Ostatnia lista zostanie oznaczona nazwą równą ciągowi znaków „*”.
  • Ponieważ z góry nie znamy liczby list, zostaną one najpierw zapisane w obiekcie typu [ArrayList]. Następnie, gdy wszystkie listy zostaną wprowadzone, zostaną one przeniesione do tablicy list.
  • Wyniki zostaną wyświetlone w porządku malejącym według liczby uzyskanych mandatów.

Aby posortować tablicę T, dostępne są różne metody statyczne klasy [Arrays]:

  • Arrays.sort(T): sortuje tablicę T według naturalnej kolejności, o ile taka istnieje (rosnąco dla liczb, dat, alfabetycznie dla ciągów znaków itp.)
  • Arrays.sort(T,komparator): służy do sortowania tablic T, które nie mają naturalnej kolejności. Tak jest w tym przypadku z tablicą list, którą należy posortować według konkretnego pola listy: liczby uzyskanych mandatów.

W metodzie Arrays.sort(T,komparator) parametr „komparator” jest obiektem implementującym następujący interfejs Comparator:

Image

  • metoda „compare” pozwala porównać dwa elementy tablicy T
  • metoda equals pozwala stwierdzić, czy dwa obiekty są równe

Obie metody porównują typy Object obj1 i obj2. To, czy obiektem jest obj1<obj2, obj1=obj2 czy obj1>obj2, zależy od relacji porządkowej, jaką chcemy ustanowić między tymi dwoma obiektami. To programista implementujący ten interfejs musi określić, na podstawie czego stwierdzamy, że:

  • obj1 jest mniejszy od obj2
  • obj1 jest większy od obj2
  • obj1 jest równe obj2

Klasa Object, od której wywodzi się każda klasa w Javie, posiada już metodę [equals]. Do sortowania tablicy T zawierającej obiekty typu O metoda [equals] klasy O nie jest potrzebna. Można zatem pozostawić domyślną implementację dostarczoną przez klasę Object. Należy wówczas zaimplementować jedynie metodę [compare]. Metoda ta jest wielokrotnie wywoływana przez metodę [Arrays.sort]. Ta ostatnia za każdym razem przekazuje jako parametry obj1 i obj2 metody compare – dwa elementy tablicy T, które mają zostać posortowane. W naszym przypadku elementy te będą typu [ListeElectorale]. Należy tutaj zwrócić uwagę na działający polimorfizm. Metoda [compare] jest zdefiniowana tak, że przyjmuje parametry typu [Object]. Oznacza to, że może ona przyjmować parametry typu [Object] lub pochodne (polimorfizm). Ponieważ [Object] jest klasą nadrzędną wszystkich klas Java, rzeczywiste parametry mogą mieć typ [ListeElectorale].

W przypadku sortowania w porządku rosnącym metoda [compare] musi zwrócić:

  • -1, jeśli obj1 jest mniejsze od obj2
  • +1, jeśli obj1 jest większe od obj2
  • 0, jeśli obj1 jest równe obj2

W przypadku sortowania w porządku malejącym wartości +1 i -1 są odwrócone. Wyrażenia „jest mniejsze niż”, „jest większe niż” i „jest równe” wyrażają relację porządkową. W przypadku obiektów typu [ListeElectorale] relacja lista1 „jest mniejsza niż” lista2 zachodzi, jeśli lista1 ma mniej głosów niż lista2.

W tym samym pliku źródłowym, co klasa [MainElections], można dodać drugą klasę:

// klasa porównania list wyborczych
class CompareListesElectorales implements Comparator {

     // porównanie dwóch list wyborczych pod kątem liczby głosów
    public int compare(Object obj1, Object obj2) {
         // pobieranie list wyborczych
        ListeElectorale listeElectorale1 = (ListeElectorale) obj1;
        ListeElectorale listeElectorale2 = (ListeElectorale) obj2;
         // porównanie głosów z tych dwóch list
....        
    }
}
  • wiersz 2: klasa nie jest zadeklarowana jako publiczna. W pliku źródłowym Java może znajdować się wiele klas, ale tylko jedna może posiadać atrybut public, a mianowicie ta, która nosi nazwę pliku źródłowego.

W poprzedniej metodzie compare parametry mają typ Object,, co powoduje, że w wierszach 7 i 8 konieczne jest przekształcenie typów parametrów metody z typu Object na typ ListeElectorale. Sygnatura metody compare jest narzucona przez interfejs Comparator, który został napisany w celu porównywania dowolnych obiektów. Od wersji JDK 1.5 istnieje interfejs generyczny Comparator: Comparator<T>, gdzie T jest dowolnym typem Java. Metoda compare interfejsu Comparator<T> porównuje obiekty typu T, a nie typu Object, co pozwala uniknąć wcześniejszych konwersji typów. Klasa służąca do porównywania obiektów typu ListeElectorale mogłaby wyglądać następująco:


// klasa porównywania list wyborczych
class CompareListesElectorales implements Comparator<ListeElectorale> {

    // porównanie dwóch list wyborczych pod względem liczby mandatów
    public int compare(ListeElectorale listeElectorale1,
            ListeElectorale listeElectorale2) {
...
    }
}
  • wiersz 2: klasa implementuje interfejs Comparator<ListeElectorale>
  • wiersze 5–6: parametry metody compare są typu ListeElectorale. Konwersja typu nie jest już konieczna.

W wersji JDK 1.5 wprowadzono koncepcję klas i interfejsów generycznych dla różnych klas i interfejsów z wersji JDK 1.4, które początkowo obsługiwały wyłącznie obiekty typu Object. Dotyczy to list, słowników itp.

Jak wspomnieliśmy nieco wcześniej, ponieważ nie znaliśmy liczby list, nie mogliśmy przechowywać ich w tablicy. Można je przechowywać w obiekcie typu ArrayList, który realizuje pojęcie „listy obiektów”. Klasa ta przechowuje obiekty typu Object. Od wersji JDK 1.5 istnieją listy obiektów typowanych. W związku z tym do przechowywania list przed przeniesieniem ich do tablicy używa się obiektu ArrayList<ListeElectorale>. Jeśli tablica ta nosi nazwę tListes, jej sortowanie zostanie uzyskane za pomocą instrukcji:


// sortowanie list
Arrays.sort(tListes, new CompareListesElectorales());

gdzie CompareListesElectorales jest klasą implementującą interfejs Comparator<ListeElectorale>.


Zadanie: przepisz aplikację [Elections], uwzględniając te nowe specyfikacje.


Projekt Eclipse mógłby wyglądać następująco:

Przykładowe uruchomienie klasy [1] wygląda następująco:

Image