Skip to content

3. [TD]: Klassen

Schlüsselwörter: Klasse, Schnittstelle, Vererbung, Ausnahme, Polymorphismus

Empfohlene Lektüre:

  • Absätze 2.1, 2.2, 2.4 und 2.7 in Kapitel 2 von [ref1]: Klassen und Schnittstellen
  • Abschnitte 3.3 (Klasse „String“), 3.5 (Klasse „ArrayList“), 3.6 (Klasse „Arrays“)

In Teil 1 der Übung ELECTIONS wurde keine Klasse verwendet. Wir haben eine Lösung erstellt, wie wir sie auch in der Programmiersprache C erstellt hätten. Nun führen wir den Begriff der Java-Klasse ein.

3.1. Support

 

Der Ordner [support / chap-03] enthält das Eclipse-Projekt zu diesem Kapitel.

Wir werden fortan mit der Version JDK 1.8 arbeiten, da einige der folgenden Projekte diese Version erfordern. Um die verwendete Version JDK zu ermitteln, gehen Sie wie folgt vor:

  • in [4], das verwendete JRE (Java Runtime Environment). Bei diesem JRE handelt es sich hier tatsächlich um ein JDK (Java Development Kit), hier um [jdk1.8.0_60]. Falls es sich nicht um ein JDK handelt oder Sie eine Version unter 1.8 haben, gehen Sie wie folgt vor: [5-21];
  • in [8], das derzeit von Eclipse standardmäßig verwendete JRE;
  • in [11], die verschiedenen derzeit von Eclipse bekannten Versionen JDK und JRE;
  • bei [15] sollten Sie lieber ein JDK als ein JRE wählen. Dieses Dokument verwendet Maven-Projekte, die ein JDK benötigen;
  • Bei [21] gibt es ein JDK mit der Version >=1.8;
  • Bei [22-23] können Sie auf die Facetten (verschiedene Ansichten desselben Eclipse-Projekts) des Projekts zugreifen;
  • In [24]: Stellen Sie sicher, dass Sie eine Java-Version >=1.8 verwenden;

3.2. Die Klasse [ListeElectorale]

In der Programmiersprache C hätten wir wahrscheinlich eine Struktur verwendet, um eine Wahlliste darzustellen. Diese hätte etwa wie folgt aussehen können:

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

Der Begriff „Struktur“ existiert in der Programmiersprache Java nicht. Er muss durch den Begriff „Klasse“ ersetzt werden. Wir beschließen daher, eine Klasse zu erstellen, um die Informationen zu einer Kandidatenliste zu speichern. Diese hätte folgendes Grundgerüst:


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() {
   ...
    }
}
  • Zeile 8: Eine Nummer, die eine Liste eindeutig identifiziert. Ist hier nicht zwingend erforderlich, ist aber für eine zukünftige Verwendung vorgesehen.
  • Zeile 13: Der Name der Liste.
  • Zeile 17: Die Stimmenzahl der Liste
  • Zeile 21: Anzahl der Sitze der Liste
  • Zeile 25: Boolescher Wert, der angibt, ob die Liste ausgeschieden ist (Prozentsatz der erhaltenen Stimmen unterhalb der Wahlhürde) oder nicht.

Jedes private Feld mit dem Namen [xyz] kann über eine Methode namens [setXyz] initialisiert werden. Die Methode [getXyz] ermöglicht es wiederum, den Wert des privaten Feldes [xyz] abzurufen. In dem speziellen Fall, dass [xyz] ein Feld vom Typ „boolesch“ ist, kann die Methode [getXyz] durch die Methode [isXyz] ersetzt werden. Die spezielle Benennung dieser Methoden folgt einem Kodierungsstandard namens JavaBean. Daher definieren wir die folgenden öffentlichen Methoden:

  • getId (Zeile 48), setId (Zeile 57)
  • getNom (Zeile 65), setNom (Zeile 74)
  • getVoix (Zeile 82), setVoix (Zeile 90)
  • getSieges (Zeile 98), setSieges (Zeile 106)
  • isElimine (Zeile 114), setElimine (Zeile 122)
  • Zeilen 30–31: Definieren einen Konstruktor ohne Parameter. Dieser ermöglicht es, ein Objekt vom Typ [ListeElectorale] zu erstellen, ohne es zu initialisieren. Das Objekt kann anschließend mithilfe der set-Methoden initialisiert werden.
  • Zeilen 40–42: Definieren einen Konstruktor, mit dem ein Objekt vom Typ [ListeElectorale] erstellt und gleichzeitig dessen fünf private Felder initialisiert werden können.
  • Zeilen 130–132: Definieren die Methode [toString], die eine Zeichenkette zurückgibt, die die Werte der fünf Felder des Objekts enthält.

Ein Testprogramm für die Klasse ListeElectorale könnte wie folgt aussehen:


package istia.st.elections.tests;

import istia.st.elections.ListeElectorale;

public class MainTest1ListeElectorale {
    public static void main(String[] args) {
        // Erstellung einer Wahlliste
        ListeElectorale listeElectorale1 = new ListeElectorale(1, "A", 32000,
                0, false);
        // Anzeige der Listenidentität
        System.out.println("listeElectorale1=" + listeElectorale1);
        // Änderung der Anzahl der Sitze
        listeElectorale1.setSieges(2);
        // Anzeige der Angaben zur Liste 1
        System.out.println("listeElectorale1=" + listeElectorale1);
        // eine neue Wählerliste
        ListeElectorale listeElectorale2 = listeElectorale1;
        // Anzeige der Identität der Liste 2
        System.out.println("listeElectorale2=" + listeElectorale2);
        // Änderung der Anzahl der Sitze
        listeElectorale2.setSieges(3);
        // Anzeige der Identitäten beider Listen
        System.out.println("listeElectorale2=" + listeElectorale2);
        System.out.println("listeElectorale1=" + listeElectorale1);
    }
}

Die Eclipse-Umgebung für diesen Test könnte wie folgt aussehen:

  • [1]: Das Projekt heißt [elections-02A]
  • [2]: Die Anwendung wird in einem Paket abgelegt, hier [istia.st.elections]
  • [3]: [ListeElectorale.java] ist der Quellcode der Klasse [ListeElectorale]
  • [4]: Die Testklassen werden in ein Paket eingefügt, hier [istia.st.elections.tests]
  • [5]: Die Testklasse [MainTest1ListeElectorale]

Nach Ausführung des obigen Programms erscheint folgende Bildschirmausgabe:

Image


Aufgabe: Vervollständigen Sie anhand der obigen Angaben den Code der Klasse ListeElectorale.


3.3. Erstellung einer Ausnahmeklasse [ElectionsException]

Unter den verschiedenen Ausnahmeklassen der Programmiersprache Java gibt es eine namens [RuntimeException]. Diese Klasse leitet sich von der Klasse [Exception] ab, der Basis aller Ausnahmeklassen. Das Besondere an Instanzen von [RuntimeException] oder davon abgeleiteten Klassen ist, dass man sie weder deklarieren noch verwalten muss. Man bezeichnet sie als unkontrollierte Ausnahmen.

Betrachten wir ein erstes Beispiel. Die Klasse [BufferedReader] ist eine Klasse, deren Instanzen das Einlesen von Textzeilen aus einem Datenstrom ermöglichen. Sie verfügt über eine Methode [readLine] mit folgender Signatur:

public String readLine()throws IOException

Man sieht, dass die Methode eine Ausnahme vom Typ [IOException] auslösen kann. Die Struktur dieser Klasse sieht wie folgt aus:

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

Die Klasse [IOException] leitet sich von der Klasse [Exception] ab (Zeile 3). Der Compiler zwingt uns dazu, Ausnahmen vom Typ [java.lang.Exception] oder davon abgeleitete Ausnahmen zu behandeln und zu deklarieren (mit Ausnahme des Zweigs [RuntimeException], den wir später vorstellen werden). Um also eine über die Tastatur eingegebene Textzeile zu lesen, müssen wir Folgendes schreiben:

1
2
3
4
5
6
7
8
BufferedReader clavier=....;
String ligne=null;
try{
    ligne=clavier.readLine();
}catch (IOException ex){
     // Ausnahme bearbeiten
    ....
}

Nehmen wir ein weiteres Beispiel. Um eine Zeichenkette in eine Ganzzahl umzuwandeln, kann man die statische Methode [Integer.parseInt] verwenden, deren Signatur wie folgt lautet:

public static int parseInt(String s) throws NumberFormatException

Das Argument [s] ist die Zeichenkette, die in eine Ganzzahl umgewandelt werden soll. Man sieht, dass die Methode eine Ausnahme vom Typ [NumberFormatException] auslösen kann. Die Struktur dieser Klasse sieht wie folgt aus:

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

Die Klasse [NumberFormatException] leitet sich von der Klasse [RuntimeException] ab (Zeile 4). Der Compiler zwingt uns nicht dazu, Ausnahmen vom Typ [java.lang.RuntimeException] oder davon abgeleitete Typen zu behandeln und zu deklarieren. Daher können wir beispielsweise Folgendes schreiben:

1
2
3
4
5
6
7
8
9
BufferedReader clavier=....;
String ligne=null;
try{
    ligne=clavier.readLine();
}catch (IOException ex){
     // Ausnahme behandeln
    ....
}
int age=Integer.parseInt(ligne);

Wir sind nicht verpflichtet, eine [try - catch]-Klausel einzufügen, um die mögliche Ausnahme zu behandeln, die von [Integer.parseInt] (Zeile 9) ausgelöst wird.

Das Erstellen und Verwenden von von [RuntimeException] abgeleiteten Ausnahmeklassen hat Vor- und Nachteile:

  • Was die Vorteile angeht: Der Code ist schlanker
  • Was die Nachteile betrifft: Man läuft Gefahr, zu den Methoden aus der C-Programmiersprache zurückzukehren, bei denen jede Funktion einen Fehlercode zurückgibt, den nur wenige nutzen – gerade um den Code schlanker zu halten. Wenn ein solcher nicht abgefangener Fehler auftritt, stürzt das Programm ab, meist auf wenig elegante Weise.

Wir beschließen, eine spezielle Klasse zu erstellen, die alle Ausnahmen zusammenfasst, die in unserer Anwendung ELECTIONS auftreten können. Sie wird [ElectionsException] heißen und von der Klasse [RuntimeException] abgeleitet sein. Ihr Code lautet wie folgt:


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);
    }
}
  • Zeile 1: Wir ordnen die Klasse dem Paket [istia.st.elections] zu;
  • Zeile 3: Die Klasse leitet sich von [RuntimeException] ab. Sie ist daher nicht geprüft;
  • Zeile 4: Eine Serialisierungs-ID, die wir vorerst ignorieren können;
  • Wir werden in unserer Anwendung zwei Arten von Konstruktoren verwenden:
    • den klassischen Konstruktor aus den Zeilen 15–17 wie unten gezeigt:
throw new ElectionsException("Le nombre de sièges doit être >0")

In diesem Fall kann die Methode, die eine Methode aufruft, die eine solche Ausnahme auslöst, diese wie folgt behandeln:


        // Ausnahmetest
        try {
            listeElectorale2.setSieges(-3);
        } catch (ElectionsException ex) {
            System.err.println("L'exception suivante s'est produite : ["
                    + ex.toString() + "]");
        }
  • (Fortsetzung)
    • oder der Code in den Zeilen 14–20, der dazu dient, eine bereits aufgetretene Ausnahme weiterzuleiten, indem er in eine Ausnahme vom Typ [ElectionsException] gekapselt wird:

    try {
        ...;
        } catch (SQLException ex) {
            // Die Ausnahme wird gekapselt
            throw new ElectionsException("erreur de fermeture de la connexion à la BD",ex);
        }

Diese zweite Methode hat den Vorteil, dass die Informationen, die die erste Ausnahme enthalten kann, erhalten bleiben. In diesem Fall kann die Methode, die eine solche Ausnahme auslösende Methode aufruft, diese wie folgt behandeln:


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

Aufgabe: Überarbeiten Sie den Code der Klasse ListeElectorale so, dass die set-Methoden eine Ausnahme vom Typ [ElectionsException] auslösen, wenn die angeforderte Initialisierung fehlerhaft ist, wie beispielsweise die Initialisierung des Namens mit einer leeren Zeichenkette.


Das Eclipse-Testprojekt für diese neue Version könnte wie folgt aussehen:

  • [1]: Das Projekt heißt [elections-02B]
  • [2]: Die Anwendung befindet sich in einem Paket, hier [istia.st.elections]
  • [3]: die Klassen [ListeElectorale] und [ElectionsException]
  • [4]: Die Testklassen sind in einem Paket abgelegt, hier [istia.st.elections.tests]
  • [5]: die Testklasse [MainTest1ListeElectorale]

Die bereits behandelte Testklasse [MainTest1ListeElectorale] wird leicht modifiziert, um Ausnahmefälle zu testen:


package istia.st.elections.tests;

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

public class MainTest1ListeElectorale {
    public static void main(String[] args) {
        // Erstellung eines Wählerverzeichnisses
        ListeElectorale listeElectorale1 = new ListeElectorale(1, "A", 32000,
                0, false);
        // Anzeige der Identität der Liste
        System.out.println("listeElectorale1=" + listeElectorale1);
        // Änderung der Anzahl der Sitze
        listeElectorale1.setSieges(2);
        // Anzeige der Identität von Liste 1
        System.out.println("listeElectorale1=" + listeElectorale1);
        // eine neue Wählerliste
        ListeElectorale listeElectorale2 = listeElectorale1;
        // Anzeige der Listenidentität 2
        System.out.println("listeElectorale2=" + listeElectorale2);
        // Änderung der Anzahl der Sitze
        listeElectorale2.setSieges(3);
        // Anzeige der Identitäten beider Listen
        System.out.println("listeElectorale2=" + listeElectorale2);
        System.out.println("listeElectorale1=" + listeElectorale1);
        // Ausnahmetest
        try {
            listeElectorale2.setSieges(-3);
        } catch (ElectionsException ex) {
            System.err.println("L'exception suivante s'est produite : ["
                    + ex.toString() + "]");
        }

    }
}
  • Zeile 28: Es wird versucht, die Anzahl der Sitze mit einem unzulässigen Wert zu initialisieren
  • Zeile 30: Tritt eine Ausnahme auf, wird diese angezeigt

Die Ausführung des Tests liefert folgende Ergebnisse:

Image

Es ist zu erkennen, dass die Klasse [ListeElectorale] tatsächlich eine Ausnahme ausgelöst hat, als versucht wurde, die Anzahl der Sitze mit einem ungültigen Wert zu initialisieren (Zeile 28 des Codes).

3.4. Eine Unit-Test-Klasse

Die zuvor beschriebene Testart basiert auf einer visuellen Überprüfung. Es wird überprüft, ob auf dem Bildschirm das erwartete Ergebnis angezeigt wird. Diese Methode ist im beruflichen Umfeld nicht zu empfehlen. Tests sollten stets so weit wie möglich automatisiert werden und darauf abzielen, keinerlei menschliches Eingreifen zu erfordern. Der Mensch ist nämlich anfällig für Ermüdung, und seine Fähigkeit, Tests zu überprüfen, lässt im Laufe des Tages nach.

Eine Anwendung entwickelt sich im Laufe der Zeit weiter. Bei jeder Weiterentwicklung muss überprüft werden, ob die Anwendung keine „Regression“ aufweist, c.a.d, und ob sie weiterhin die Funktionstests besteht, die bei ihrer ursprünglichen Erstellung durchgeführt wurden. Diese Tests werden als „Regressionstests“ bezeichnet. Eine etwas umfangreichere Anwendung kann Hunderte von Tests erfordern. Tatsächlich wird jede Methode jeder Klasse der Anwendung getestet. Man nennt dies Unit-Tests. Diese können viele Entwickler in Anspruch nehmen, wenn sie nicht automatisiert wurden.

Es wurden Tools entwickelt, um die Tests zu automatisieren. Eines davon heißt [JUnit]. Es handelt sich um eine Klassenbibliothek zur Verwaltung von Tests. Wir werden dieses Tool verwenden, um die Klasse [ListeElectorale] zu testen.

Ein Testprogramm JUnit (Versionen 4.x) hat folgende Form:


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

}
  • Zeile 1: Die Klasse wurde im Paket [istia.st.elections.tests] abgelegt;
  • Zeile 11: Die mit der Annotation [@Before] annotierte Methode wird vor jedem Unit-Test ausgeführt;
  • Zeile 16: Die mit der Annotation [@After] annotierte Methode wird nach jedem Unit-Test ausgeführt;
  • Zeile 21: Eine mit der Annotation [@Test] versehene Methode ist eine durch den Unit-Test getestete Methode. Die mit [@Test] annotierten Methoden werden nacheinander ausgeführt, sofern der Tester nichts anderes angibt; dieser kann die zu testenden Methoden selbst auswählen. Vor jeder Ausführung einer Methode mit der Annotation [@Test] wird die Methode [@Before] ausgeführt. Nach jeder Ausführung einer Methode mit der Annotation [@Test] wird die Methode [@After] ausgeführt;
  • Zeilen 22–25: definieren eine Testmethode [t1];
  • Zeile 18: eine der Methoden [Assert.assert*], mit denen Assertions überprüft werden können. Es gibt die folgenden Methoden [assert]:
    • assertEquals(Ausdruck1, Ausdruck2): Prüft, ob die Werte der beiden Ausdrücke gleich sind. Es werden zahlreiche Ausdruckstypen akzeptiert (int, String, float, double, boolean, char, short). Sind die beiden Ausdrücke nicht gleich, wird eine Ausnahme vom Typ [AssertionFailedError ] ausgelöst,
    • assertEquals(real1, real2, delta): Prüft, ob zwei reelle Zahlen bis auf delta gleich sind, c.a.d abs(real1-real2) <= delta. Man kann beispielsweise assertEquals(real1, real2, 1E-6) schreiben, um zu überprüfen, ob zwei Werte bis auf 10⁻⁶ gleich sind,
    • assertEquals(message, expression1, expression2) und assertEquals(message, réel1, réel2, delta) sind Varianten, mit denen die Fehlermeldung festgelegt werden kann, die der Ausnahme vom Typ [AssertionFailedError] zugeordnet wird, die ausgelöst wird, wenn die Methode [assertEquals] fehlschlägt,
    • assertNotNull(Object) und assertNotNull(message, Object): Prüft, ob Object nicht gleich null ist,
    • assertNull(Object) und assertNull(message, Object): Prüfen, ob Object gleich null ist,
    • assertSame(Object1, Object2) und assertSame(message, Object1, Object2): Prüft, ob die Referenzen Object1 und Object2 auf dasselbe Objekt verweisen,
    • assertNotSame(Object1, Object2) und assertNotSame(message, Object1, Object2): Überprüft, ob die Referenzen Object1 und Object2 nicht auf dasselbe Objekt verweisen;
  • Zeile 24: Diese Assertion muss erfolgreich sein;
  • Zeile 30: Diese Assertion muss fehlschlagen;

In der Eclipse-Umgebung kann die Erstellung einer Testklasse JUnit wie folgt erfolgen:

  • [1]: Rechtsklick auf das Paket, in das die Testklasse hinzugefügt werden soll, dann Option [JUnit / New / JUnit Test Case]
  • [1]: Auswahl einer Version JUnit;
  • [2]: Auswahl des Ordners, in dem die Testklasse angelegt werden soll;
  • [3]: Auswahl des Pakets, in dem die Testklasse angelegt werden soll;
  • [4]: Name der Testklasse;
  • [5]: Auswahl der Methoden, die in die zu generierende Klasse aufgenommen werden sollen;
  • [6]: Die Klasse JUnitEssai wurde generiert

Der vorherige Assistent generiert eine nahezu leere Klasse:


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

Ergänzen und ändern wir den vorherigen Code wie folgt:


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

}

In Eclipse kann die Testklasse durch einen Rechtsklick auf die Klasse und Auswahl der Option „[Run as / JUnit test]“ ausgeführt werden:

Image

Die Ergebnisse der Testausführung lauten wie folgt:

Image

Oben ist die Methode „[test2]“ fehlgeschlagen. Jedes Mal, wenn ein Test fehlschlägt, wird eine Fehlermeldung angezeigt. Für [test2] ist dies die oben angezeigte Meldung. Die Meldung gibt die Zeilennummer an, in der der Fehler aufgetreten ist (Zeile 30). In Zeile 30 lautete der fehlgeschlagene Aufruf:


    Assert.assertEquals(1, 2);

Der erste Parameter wird als erwarteter Wert bezeichnet, der zweite als tatsächlicher Wert. Die oben angezeigte Fehlermeldung von [test2] gibt an, dass der erwartete Wert 2 war, der tatsächliche Wert jedoch 3 betrug.

Schließlich wurden von den verschiedenen Testmethoden folgende Meldungen auf die Konsole geschrieben:

Image

Diese Meldungen zeigen, dass die Methoden [@Before] und [@After] tatsächlich jeweils vor und nach jeder Testmethode aufgerufen wurden.

Die Testklassen werden nicht unbedingt von den Entwicklern selbst geschrieben. Sie können auch von den Personen verfasst werden, die die Spezifikationen der Anwendung erstellt haben. Bestimmte Entwicklungsmethoden, die als TDD (Test Driven Development) bezeichnet werden, empfehlen, die Testklassen noch vor dem Schreiben der zu testenden Klassen zu erstellen. Dies ermöglicht es manchmal, Spezifikationen zu präzisieren, die andernfalls auf verschiedene Weise interpretiert werden könnten.

Erstellen wir einen JUnit-4-Test namens [JUnitTest1ListeElectorale] für die Klasse [ListeElectorale]. Unter Eclipse gehen wir dabei wie zuvor beschrieben vor:

Wir ergänzen den vom Assistenten generierten Code wie folgt:


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() {
        // Erstellung der Wahlliste
        ListeElectorale liste = new ListeElectorale(1, "a", 32000, 0, false);
        // Prüfungen
        Assert.assertEquals("a", liste.getNom());
        Assert.assertEquals(32000, liste.getVoix());
        Assert.assertEquals(false, liste.isElimine());
        Assert.assertEquals(0, liste.getSieges());
        // Überprüfung der Gültigkeit der ID
        boolean erreur = false;
        try {
            liste.setId(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // Überprüfung der Gültigkeit des Namens
        erreur = false;
        try {
            liste.setNom("");
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // Überprüfung der Gültigkeit der Stimmen
        erreur = false;
        try {
            liste.setVoix(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // Überprüfung der Gültigkeit der Sitze
        erreur = false;
        try {
            liste.setSieges(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
    }

}

Die Ausführung des Tests liefert folgendes Ergebnis:

Image

Die Tests sind erfolgreich verlaufen. Wir gehen nun davon aus, dass wir über eine funktionsfähige Klasse [ListeElectorale] verfügen.

3.5. MainElections: Version 2

Empfohlene Lektüre:

  • Abschnitte 2.1, 2.2, 2.4 und 2.7 in Kapitel 2 von [1]: Klassen und Schnittstellen
  • Abschnitte 3.3 (Klasse „String“), 3.5 (Klasse „ArrayList“), 3.6 (Klasse „Arrays“)

Wir möchten die Anwendung [Elections] umschreiben und dabei die folgenden neuen Einschränkungen hinzufügen:

  • Zur Darstellung einer Kandidatenliste wird die Klasse [ListeElectorale] verwendet
  • Die Anwendung fragt über die Tastatur folgende Informationen ab:
  • die Anzahl der zu besetzenden Sitze
  • die Namen und Stimmen der Listen. Es ist nicht von vornherein bekannt, wie viele Listen es gibt. Die letzte Liste wird durch einen Namen gekennzeichnet, der der Zeichenkette „*“ entspricht.
  • Da die Anzahl der Listen zunächst nicht bekannt ist, werden diese zunächst in einem Objekt vom Typ [ArrayList] gespeichert. Sobald alle Listen erfasst sind, werden sie in ein Listenarray übertragen.
  • Die Ergebnisse werden in absteigender Reihenfolge der erzielten Sitze angezeigt.

Zum Sortieren eines Arrays T stehen verschiedene statische Methoden der Klasse [Arrays] zur Verfügung:

  • Arrays.sort(T): Sortiert das Array T nach einer natürlichen Reihenfolge, sofern vorhanden (aufsteigend für Zahlen, Datumsangaben, alphabetisch für Zeichenfolgen usw.)
  • Arrays.sort(T,Vergleicher): zum Sortieren von Arrays T, die keine natürliche Reihenfolge haben. Dies ist hier der Fall bei dem Array der Listen, die nach einem bestimmten Feld der Liste sortiert werden müssen: der Anzahl der gewonnenen Sitze.

In der Methode Arrays.sort(T,Vergleicher) ist der Parameter „Vergleicher“ ein Objekt, das die folgende Comparator-Schnittstelle implementiert:

Image

  • Die Methode „compare“ ermöglicht den Vergleich zweier Elemente des Arrays T
  • Die Methode `equals` ermöglicht es, festzustellen, ob zwei Objekte gleich sind

Beide Methoden vergleichen die Typen „Object obj1“ und „Object obj2“. Ob nun obj1<obj2, obj1=obj2 oder obj1>obj2 gilt, hängt von der Ordnungsbeziehung ab, die man zwischen den beiden Objekten herstellen möchte. Es ist Aufgabe des Entwicklers, der diese Schnittstelle implementiert, festzulegen, woran man erkennt, dass:

  • obj1 kleiner ist als obj2
  • obj1 größer ist als obj2
  • obj1 gleich obj2 ist

Die Klasse Object, von der alle Java-Klassen abgeleitet sind, verfügt bereits über eine Methode [equals]. Zum Sortieren eines Arrays T mit Objekten vom Typ O ist die Methode [equals] der Klasse O nicht erforderlich. Man kann daher die von der Klasse Object bereitgestellte Standardimplementierung beibehalten. Es muss dann lediglich die Methode [compare] implementiert werden. Diese Methode wird wiederholt von der Methode [Arrays.sort] aufgerufen. Letztere übergibt jedes Mal obj1 und obj2 der Methode compare als Parameter, zwei Elemente des zu sortierenden Arrays T. In unserem Fall sind diese Elemente vom Typ [ListeElectorale]. Hier ist der Polymorphismus zu beachten. Die Methode [compare] ist so definiert, dass sie Parameter vom Typ [Object] entgegennimmt. Das bedeutet, dass sie Parameter vom Typ [Object] oder davon abgeleitete Typen (Polymorphismus) entgegennehmen kann. Da [Object] die übergeordnete Klasse aller Java-Klassen ist, können die tatsächlichen Parameter den Typ [ListeElectorale] haben.

Für eine Sortierung in aufsteigender Reihenfolge muss die Methode [compare] Folgendes zurückgeben:

  • -1, wenn obj1 kleiner als obj2 ist
  • +1, wenn obj1 größer als obj2 ist
  • 0, wenn obj1 gleich obj2 ist

Für eine Sortierung in absteigender Reihenfolge werden die Werte +1 und -1 vertauscht. Die Begriffe „ist kleiner als“, „ist größer als“ und „ist gleich“ drücken eine Ordnungsbeziehung aus. Bei Objekten vom Typ [ListeElectorale] gilt die Beziehung „Liste1 ist kleiner als Liste2“, wenn Liste1 weniger Stimmen hat als Liste2.

In derselben Quelldatei wie die Klasse [MainElections] kann eine zweite Klasse hinzugefügt werden:

// Vergleichsklasse der Wählerverzeichnisse
class CompareListesElectorales implements Comparator {

     // Vergleich zweier Wählerlisten nach der Anzahl der Stimmen
    public int compare(Object obj1, Object obj2) {
         // Die Wählerlisten werden abgerufen
        ListeElectorale listeElectorale1 = (ListeElectorale) obj1;
        ListeElectorale listeElectorale2 = (ListeElectorale) obj2;
         // Die Stimmen dieser beiden Listen werden verglichen
....        
    }
}
  • Zeile 2: Die Klasse ist nicht als „public“ deklariert. In einer Java-Quelldatei kann es mehrere Klassen geben, aber nur eine darf das Attribut „public“ haben, nämlich diejenige, die den Namen der Quelldatei trägt.

In der vorherigen Methode compare sind die Parameter vom Typ Object,, was in den Zeilen 7 und 8 eine Typumwandlung der Methodenparameter vom Typ Object in den Typ ListeElectorale erforderlich macht. Die Signatur der Methode compare wird durch die Schnittstelle Comparator vorgegeben, die zum Vergleich beliebiger Objekte geschrieben wurde. Seit JDK 1.5 gibt es eine generische Schnittstelle Comparator: Comparator<T>, wobei T ein beliebiger Java-Typ ist. Die Methode compare der Schnittstelle Comparator<T> vergleicht Objekte vom Typ T und nicht vom Typ Object, wodurch die bisherigen Typumwandlungen vermieden werden. Die Vergleichsklasse für Objekte vom Typ ListeElectorale könnte wie folgt aussehen:


// Klasse zum Vergleich von Wahllisten
class CompareListesElectorales implements Comparator<ListeElectorale> {

    // Vergleich zweier Wahllisten nach der Anzahl der Sitze
    public int compare(ListeElectorale listeElectorale1,
            ListeElectorale listeElectorale2) {
...
    }
}
  • Zeile 2: Die Klasse implementiert die Schnittstelle `Comparator<ListeElectorale>`
  • Zeilen 5–6: Die Parameter der Methode compare sind vom Typ ListeElectorale. Eine Typumwandlung ist nun nicht mehr erforderlich.

Mit JDK 1.5 wurde das Konzept der generischen Klassen/Schnittstellen für verschiedene Klassen/Schnittstellen aus JDK 1.4 eingeführt, die ursprünglich ausschließlich Objekte vom Typ Object verarbeiteten. Dies gilt beispielsweise für Listen, Wörterbücher usw.

Wir haben weiter oben erwähnt, dass man die Listen nicht in einem Array speichern konnte, da die Anzahl der Listen nicht bekannt war. Sie können in einem Objekt vom Typ ArrayList gespeichert werden, das das Konzept einer „Objektliste“ implementiert. Diese Klasse speichert Objekte vom Typ Object. Seit JDK 1.5 gibt es typisierte Objektlisten. Daher wird ein Objekt vom Typ ArrayList<ListeElectorale> verwendet, um die Listen zu speichern, bevor sie in ein Array übertragen werden. Wenn dieses Array den Namen tListes trägt, erfolgt die Sortierung durch die Anweisung:


// Sortierung der Listen
Arrays.sort(tListes, new CompareListesElectorales());

wobei CompareListesElectorales die Klasse ist, die die Schnittstelle Comparator<ListeElectorale> implementiert.


Aufgabe: Schreiben Sie die Anwendung [Elections] unter Berücksichtigung dieser neuen Spezifikationen neu.


Das Eclipse-Projekt könnte wie folgt aussehen:

Ein Beispiel für die Ausführung von [1] lautet wie folgt:

Image