Skip to content

3. [TD]: Klassen

Stichworte: Klasse, Schnittstelle, Vererbung, Ausnahme, Polymorphismus

Literaturempfehlung:

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

In Teil 1 der ELECTIONS-Übung wurden keine Klassen verwendet. Wir haben eine Lösung erstellt, wie wir sie in der Programmiersprache C erstellt hätten. Wir werden nun das Konzept der Java-Klassen einführen.

3.1. Unterstützung

 

Der Ordner [support / chap-03] enthält das Eclipse-Projekt für dieses Kapitel.

Wir werden nun mit JDK 1.8 arbeiten, da einige der kommenden Projekte dieses JDK erfordern. Um festzustellen, welches JDK verwendet wird, gehen Sie wie folgt vor:

  • in [4] die verwendete JRE (Java Runtime Environment). Bei dieser JRE handelt es sich hier eigentlich um ein JDK (Java Development Kit), genauer gesagt um [jdk1.8.0_60]. Wenn es sich nicht um ein JDK handelt oder wenn Sie eine Version unter 1.8 haben, gehen Sie wie folgt vor [5-21];
  • in [8] die JRE, die derzeit standardmäßig von Eclipse verwendet wird;
  • in [11] die verschiedenen JDKs und JREs, die derzeit von Eclipse erkannt werden;
  • Wählen Sie in [15] ein JDK anstelle einer JRE. In diesem Dokument werden Maven-Projekte verwendet, die ein JDK erfordern;
  • in [21] haben wir eine JDK-Version >=1.8;
  • in [22-23] greifen wir auf die Facetten (verschiedene Ansichten desselben Eclipse-Projekts) des Projekts zu;
  • Überprüfen Sie in [24], ob Sie eine Java-Version >=1.8 verwenden;

3.2. Die Klasse [ ListeElectorale]

In C hätten wir wahrscheinlich eine Struktur verwendet, um eine Liste von Wahlkandidaten darzustellen. Das hätte etwa so aussehen können:

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

Das Konzept einer Struktur existiert in der Programmiersprache Java nicht. Es muss durch das einer Klasse ersetzt werden. Wir beschließen daher, eine Klasse zu erstellen, um Informationen über eine Liste von Kandidaten zu speichern. Diese Klasse hätte das folgende 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 eindeutige Kennung für eine Liste. Hier nicht unbedingt erforderlich, aber für zukünftige Verwendung reserviert.
  • Zeile 13: Der Name der Liste.
  • Zeile 17: die Anzahl der Stimmen für die Liste
  • Zeile 21: die Anzahl der Sitze für die Liste
  • Zeile 25: Ein 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 mithilfe einer Methode namens [setXyz] initialisiert werden. Die Methode [getXyz] ruft den Wert des privaten Feldes [xyz] ab. In dem speziellen Fall, in dem [xyz] ein boolesches Feld ist, kann die Methode [getXyz] durch die Methode [isXyz] ersetzt werden. Die spezifische Benennung dieser Methoden folgt einem Codierungsstandard, der als JavaBean-Standard bezeichnet wird. Daher definieren wir die folgenden öffentlichen Methoden:

  • getId (Zeile 48), setId (Zeile 57)
  • getName (Zeile 65), setName (Zeile 74)
  • getVoice (Zeile 82), setVoice (Zeile 90)
  • getSeats (Zeile 98), setSeats (Zeile 106)
  • isEliminated (Zeile 114), setEliminated (Zeile 122)
  • Zeilen 30–31: Definieren Sie einen Konstruktor ohne Parameter. Damit können Sie ein [VoterList]-Objekt erstellen, ohne es zu initialisieren. Es kann anschließend mit den set-Methoden initialisiert werden.
  • Zeilen 40–42: Definieren Sie einen Konstruktor, der ein [VoterList]-Objekt erstellt und dabei dessen fünf private Felder initialisiert.
  • Zeilen 130–132: Definieren Sie 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 [VoterList] könnte wie folgt aussehen:


package istia.st.elections.tests;
 
import istia.st.elections.ListeElectorale;
 
public class MainTest1ListeElectorale {
    public static void main(String[] args) {
        // creation of an electoral list
        ListeElectorale listeElectorale1 = new ListeElectorale(1, "A", 32000,
                0, false);
        // display identity list
        System.out.println("listeElectorale1=" + listeElectorale1);
        // change in number of seats
        listeElectorale1.setSieges(2);
        // display identity list 1
        System.out.println("listeElectorale1=" + listeElectorale1);
        // a new electoral roll
        ListeElectorale listeElectorale2 = listeElectorale1;
        // display identity list 2
        System.out.println("listeElectorale2=" + listeElectorale2);
        // change in number of seats
        listeElectorale2.setSieges(3);
        // display identity of the 2 lists
        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]: [VoterList.java] ist der Quellcode für die Klasse [VoterList]
  • [4]: Die Testklassen werden in einem Paket abgelegt, hier [istia.st.elections.tests]
  • [5]: Die Testklasse [MainTest1VoterList]

Die Bildschirmdarstellung nach Ausführung des obigen Programms sieht wie folgt aus:

Image


Aufgabe: Vervollständigen Sie anhand der obigen Informationen den Code für die Klasse VoterList.


3.3. Erstellen einer Ausnahmeklasse [ElectionsException]

Unter den verschiedenen Ausnahmeklassen in 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 Instanzen ist, dass Sie diese weder deklarieren noch behandeln müssen. Sie werden als nicht abgefangene Ausnahmen bezeichnet.

Sehen wir uns ein erstes Beispiel an. Die Klasse [BufferedReader] ist eine Klasse, deren Instanzen es Ihnen ermöglichen, Textzeilen aus einem Datenstrom zu lesen. Sie verfügt über eine Methode [readLine] mit der folgenden Signatur:

public String readLine()throws IOException

Wir sehen, dass die Methode eine [IOException] auslösen kann. Die Klassenhierarchie für diese 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 verlangt, dass wir Ausnahmen vom Typ [java.lang.Exception] oder abgeleiteten Typen behandeln und deklarieren (mit Ausnahme des Zweigs [RuntimeException], auf den wir später noch eingehen werden). Um also eine über die Tastatur eingegebene Textzeile zu lesen, müssen wir etwa Folgendes schreiben:

1
2
3
4
5
6
7
8
BufferedReader clavier=....;
String ligne=null;
try{
    ligne=clavier.readLine();
}catch (IOException ex){
    // gérer l'exception
    ....
}

Nehmen wir ein weiteres Beispiel. Um eine Zeichenkette in eine Ganzzahl umzuwandeln, können wir 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. Wir sehen, dass die Methode eine [NumberFormatException] auslösen kann. Die Klassenhierarchie für diese 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] erweitert die Klasse [RuntimeException] (Zeile 4). Der Compiler verlangt nicht, dass wir Ausnahmen vom Typ [java.lang.RuntimeException] oder deren Unterklassen behandeln oder 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){
    // gérer l'exception
    ....
}
int age=Integer.parseInt(ligne);

Es ist nicht erforderlich, einen [try-catch]-Block einzufügen, um Ausnahmen zu behandeln, die von [Integer.parseInt] (Zeile 9) generiert werden.

Die Erstellung und Verwendung von von [RuntimeException] abgeleiteten Ausnahmeklassen hat Vor- und Nachteile:

  • Vorteil: Der Code ist prägnanter
  • Nachteil: Wir könnten letztendlich auf Methoden im C-Stil zurückgreifen, bei denen jede Funktion einen Fehlercode zurückgibt – eine Praxis, die nur wenige anwenden, gerade um den Code schlank zu halten. Wenn ein solcher unbehandelte Fehler auftritt, stürzt das Programm ab, meist auf unelegante Weise.

Wir haben uns entschieden, eine spezielle Klasse zu erstellen, die alle Ausnahmen gruppiert, die in unserer ELECTIONS-Anwendung 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 platzieren die Klasse im Paket [istia.st.elections];
  • Zeile 3: Die Klasse erweitert [RuntimeException]. Sie ist daher ungeprüft;
  • Zeile 4: eine Serialisierungs-ID, die wir vorerst ignorieren können;
  • In unserer Anwendung werden wir zwei Arten von Konstruktoren verwenden:
    • den klassischen in 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:


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

    try {
        ...;
        } catch (SQLException ex) {
            // on encapsule l'exception
            throw new ElectionsException("erreur de fermeture de la connexion à la BD",ex);
        }

Diese zweite Methode hat den Vorteil, dass die in der ersten Ausnahme enthaltenen Informationen 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: Ändern Sie den Code der Klasse „ListeElectorale“ so, dass die Set-Methoden eine [ElectionsException] auslösen, wenn die angeforderte Initialisierung fehlerhaft ist, z. B. wenn der Name mit einer leeren Zeichenfolge initialisiert wird.


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 [VoterList] und [ElectionsException]
  • [4]: Die Testklassen befinden sich in einem Paket, hier [istia.st.elections.tests]
  • [5]: Die Testklasse [MainTest1VoterList]

Die zuvor besprochene Testklasse [MainTest1VoterList] wurde 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) {
        // creation of an electoral list
        ListeElectorale listeElectorale1 = new ListeElectorale(1, "A", 32000,
                0, false);
        // display identity list
        System.out.println("listeElectorale1=" + listeElectorale1);
        // change in number of seats
        listeElectorale1.setSieges(2);
        // display identity list 1
        System.out.println("listeElectorale1=" + listeElectorale1);
        // a new electoral roll
        ListeElectorale listeElectorale2 = listeElectorale1;
        // display identity list 2
        System.out.println("listeElectorale2=" + listeElectorale2);
        // change in number of seats
        listeElectorale2.setSieges(3);
        // display identity of the 2 lists
        System.out.println("listeElectorale2=" + listeElectorale2);
        System.out.println("listeElectorale1=" + listeElectorale1);
        // test exception
        try {
            listeElectorale2.setSieges(-3);
        } catch (ElectionsException ex) {
            System.err.println("L'exception suivante s'est produite : ["
                    + ex.toString() + "]");
        }
 
    }
}
  • Zeile 28: Wir versuchen, die Anzahl der Plätze mit einem ungültigen Wert zu initialisieren
  • Zeile 30: Wenn eine Ausnahme auftritt, wird diese angezeigt

Die Ausführung des Tests liefert folgende Ergebnisse:

Image

Beachten Sie, dass die Klasse [VoterList] tatsächlich eine Ausnahme ausgelöst hat, als wir versucht haben, die Anzahl der Sitze mit einem ungültigen Wert zu initialisieren (Zeile 28 des Codes).

3.4. Eine Unit-Test-Klasse

Die zuvor beschriebene Art von Test stützt sich auf eine visuelle Überprüfung. Wir überprüfen, ob das, was auf dem Bildschirm angezeigt wird, mit den Erwartungen übereinstimmt. Diese Methode wird in einem professionellen Umfeld nicht empfohlen. Tests sollten immer so weit wie möglich automatisiert werden und darauf abzielen, keinen menschlichen Eingriff zu erfordern. Menschen neigen nun einmal zu Ermüdung, und ihre Fähigkeit, Tests zu überprüfen, nimmt im Laufe des Tages ab.

Eine Anwendung entwickelt sich im Laufe der Zeit weiter. Bei jedem Update müssen wir überprüfen, ob die Anwendung keine „Regression“ aufweist, d. h., ob sie weiterhin die Funktionstests besteht, die während ihrer ursprünglichen Entwicklung durchgeführt wurden. Diese Tests werden als „Non-Regressionstests“ bezeichnet. Eine mittelgroße Anwendung kann Hunderte von Tests erfordern. Tatsächlich wird jede Methode in jeder Klasse der Anwendung getestet. Diese werden als Unit-Tests bezeichnet. Sie können viele Entwickler binden, wenn sie nicht automatisiert wurden.

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

Ein JUnit-Testprogramm (Version 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] versehene Methode wird vor jedem Unit-Test ausgeführt;
  • Zeile 16: Die mit der Annotation [@After] versehene Methode wird nach jedem Unit-Test ausgeführt;
  • Zeile 21: Eine mit der Annotation [@Test] versehene Methode ist eine Methode, die durch den Unit-Test getestet wird. Mit [@Test] annotierte Methoden werden nacheinander ausgeführt, sofern der Tester nichts anderes festlegt; dieser kann die zu testenden Methoden selbst auswählen. Vor jeder Ausführung einer [@Test]-Methode wird die [@Before]-Methode ausgeführt. Nach jeder Ausführung einer [@Test]-Methode wird die [@After]-Methode ausgeführt;
  • Zeilen 22–25: Definieren eine [t1]-Testmethode;
  • Zeile 18: Eine der [Assert.assert*]-Methoden, die zur Überprüfung von Assertions verwendet werden. Die folgenden [assert]-Methoden stehen zur Verfügung:
    • assertEquals(expression1, expression2): prüft, ob die Werte der beiden Ausdrücke gleich sind. Es werden viele Ausdrucksarten akzeptiert (int, String, float, double, boolean, char, short). Sind die beiden Ausdrücke nicht gleich, wird eine [AssertionFailedError]-Ausnahme ausgelöst,
    • assertEquals(real1, real2, delta): Prüft, ob zwei reelle Zahlen bis auf delta gleich sind, d. h. abs(real1-real2) <= delta. Man könnte 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, real1, real2, delta) sind Varianten, mit denen Sie die Fehlermeldung angeben können, die der [AssertionFailedError]-Ausnahme zugeordnet wird, die ausgelöst wird, wenn die [assertEquals]-Methode fehlschlägt,
    • assertNotNull(Object) und assertNotNull(message, Object): Prüft, ob Object nicht gleich null ist.
    • assertNull(Object) und assertNull(message, Object): Prüft, 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): prü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 eine JUnit-Testklasse wie folgt erstellt werden:

  • [1]: Klicken Sie mit der rechten Maustaste auf das Paket, dem Sie die Testklasse hinzufügen möchten, und wählen Sie dann [JUnit / Neu / JUnit-Testfall]
  • [1]: Wählen Sie eine JUnit-Version aus;
  • [2]: Wählen Sie den Ordner aus, in dem die Testklasse erstellt werden soll;
  • [3]: Wählen Sie das Paket aus, in dem die Testklasse erstellt werden soll;
  • [4]: Geben Sie den Namen der Testklasse ein;
  • [5]: Wählen Sie die Methoden aus, die in die zu generierende Klasse aufgenommen werden sollen;
  • [6]: Die Klasse „JUnitEssai“ wurde generiert

Der vorherige Assistent generiert eine fast 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 {
    }
}

Lassen Sie uns den vorherigen Code wie folgt vervollständigen und ändern:


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

Klicken Sie in Eclipse mit der rechten Maustaste auf die Testklasse und wählen Sie dann [Ausführen als / JUnit-Test], um sie auszuführen:

Image

Die Ergebnisse dieses Tests lauten wie folgt:

Image

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


    Assert.assertEquals(1, 2);

Der erste Parameter wird als erwarteter Wert bezeichnet, der zweite als tatsächlicher Wert. Die Fehlermeldung für [test2] oben zeigt an, dass der erwartete Wert 2 war, der tatsächliche Wert jedoch 3.

Schließlich lauteten die von den verschiedenen Testmethoden an die Konsole geschriebenen Meldungen wie folgt:

Image

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

Testklassen werden nicht unbedingt von den Entwicklern selbst geschrieben. Sie können auch von den Personen verfasst werden, die die Anwendungsspezifikationen erstellt haben. Einige als TDD (Test-Driven Development) bekannte Entwicklungsmethoden befürworten das Schreiben von Testklassen noch vor dem Schreiben der zu testenden Klassen. Dies trägt manchmal dazu bei, Spezifikationen zu präzisieren, die andernfalls auf verschiedene Weise interpretiert werden könnten.

Erstellen wir einen JUnit-4-Test mit dem Namen [JUnitTest1VoterList] für die Klasse [VoterList]. In Eclipse gehen wir dabei wie zuvor beschrieben vor:

Wir vervollständigen 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() {
        // electoral list creation
        ListeElectorale liste = new ListeElectorale(1, "a", 32000, 0, false);
        // checks
        Assert.assertEquals("a", liste.getNom());
        Assert.assertEquals(32000, liste.getVoix());
        Assert.assertEquals(false, liste.isElimine());
        Assert.assertEquals(0, liste.getSieges());
        // validity check id
        boolean erreur = false;
        try {
            liste.setId(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // name validity check
        erreur = false;
        try {
            liste.setNom("");
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // voice validity check
        erreur = false;
        try {
            liste.setVoix(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // seat validity check
        erreur = false;
        try {
            liste.setSieges(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
    }
 
}

Die Ausführung des Tests liefert das folgende Ergebnis:

Image

Die Tests wurden bestanden. Wir betrachten die Klasse [VoterList] nun als funktionsfähig.

3.5. MainElections: Version 2

Empfohlene Lektüre:

  • Abschnitte 2.1, 2.2, 2.4 und 2.7 von Kapitel 2 in [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:

  • Wir verwenden die Klasse [VoterList], um eine Liste von Kandidaten darzustellen
  • Die Anwendung fordert den Benutzer zur Eingabe der folgenden Informationen auf:
  • die Anzahl der zu besetzenden Sitze
  • die Namen und Stimmen für die Listen. Wir wissen im Voraus nicht, wie viele Listen es gibt. Die letzte Liste wird durch einen Namen gekennzeichnet, der der Zeichenkette „*“ entspricht.
  • Da wir die Anzahl der Listen nicht im Voraus kennen, werden diese zunächst in einem [ArrayList]-Objekt gespeichert. Sobald alle Listen eingegeben wurden, werden sie in ein Array von Listen übertragen.
  • Die Ergebnisse werden in absteigender Reihenfolge der Anzahl der gewonnenen Sitze angezeigt.

Um ein Array T zu sortieren, können wir verschiedene statische Methoden der Klasse [Arrays] verwenden:

  • Arrays.sort(T): sortiert das Array T in einer natürlichen Reihenfolge, sofern es eine solche aufweist (aufsteigend für Zahlen, Datumsangaben, alphabetisch für Zeichenfolgen usw.)
  • Arrays.sort(T,comparator): zum Sortieren von Arrays T, die keine natürliche Reihenfolge haben. Dies ist hier bei dem Array von Listen der Fall, das nach einem bestimmten Feld der Liste sortiert werden muss: der Anzahl der erzielten Sitze.

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

Image

  • Die Methode compare ermöglicht den Vergleich zweier Elemente des Arrays T
  • Die Methode equals bestimmt, ob zwei Objekte gleich sind

Beide Methoden vergleichen die Objekttypen obj1 und obj2. Die Feststellung, ob obj1 < obj2, obj1 = obj2 oder obj1 > obj2 gilt, hängt von der Ordnungsbeziehung ab, die wir zwischen den beiden Objekten herstellen möchten. Es liegt im Ermessen des Entwicklers, der diese Schnittstelle implementiert, festzulegen, wie wir wissen, dass:

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

Die Klasse „Object“, von der alle Java-Klassen abgeleitet sind, verfügt bereits über eine [equals]-Methode. Um ein Array T von Objekten des Typs O zu sortieren, ist die [equals]-Methode der Klasse O nicht geeignet. Wir können daher die von der Klasse „Object“ bereitgestellte Standardimplementierung beibehalten. Es muss dann nur die Methode [compare] implementiert werden. Diese Methode wird wiederholt von der Methode [Arrays.sort] aufgerufen. Jedes Mal übergibt [Arrays.sort] obj1 und obj2 – zwei Elemente des zu sortierenden Arrays T – als Parameter an die Methode compare. In unserem Fall sind diese Elemente vom Typ [VoterList]. Beachten Sie den hier zum Tragen kommenden Polymorphismus. Die Methode [compare] ist so definiert, dass sie Parameter vom Typ [Object] akzeptiert. Das bedeutet, dass sie Parameter vom Typ [Object] oder jedem abgeleiteten Typ akzeptieren kann (Polymorphismus). Da [Object] die Oberklasse aller Java-Klassen ist, können die tatsächlichen Parameter vom Typ [VoterList] sein.

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

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

Für die 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 Ordnungsrelation aus. Bei Objekten vom Typ [VoterList] gilt die Relation list1 „ist kleiner als“ list2, wenn list1 weniger Stimmen hat als list2.

In derselben Quelldatei wie die Klasse [MainElections] können wir eine zweite Klasse hinzufügen:

// classe de comparaison de listes électorales
class CompareListesElectorales implements Comparator {

    // comparaison de deux listes électorales selon le nombre de voix
    public int compare(Object obj1, Object obj2) {
        // on récupère les listes électorales
        ListeElectorale listeElectorale1 = (ListeElectorale) obj1;
        ListeElectorale listeElectorale2 = (ListeElectorale) obj2;
        // on compare les voix de ces deux listes
....        
    }
}
  • 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 denselben Namen wie die Quelldatei trägt.

In der vorherigen compare-Methode sind die Parameter vom Typ Object, weshalb in den Zeilen 7 und 8 die Methodenparameter vom Typ Object in den Typ VoterList umgewandelt werden müssen. Die Signatur der compare-Methode wird durch die Comparator-Schnittstelle ( ) vorgegeben, die zum Vergleich beliebiger Objekte geschrieben wurde. Seit JDK 1.5 gibt es eine generische Comparator-Schnittstelle: Comparator<T>, wobei T ein beliebiger Java-Typ ist. Die compare-Methode der Comparator<T>-Schnittstelle vergleicht Objekte vom Typ T anstelle des Typs Object, wodurch die bisherigen Typumwandlungen entfallen. Die Vergleichsklasse für Objekte vom Typ VoterList könnte wie folgt aussehen:


// classe de comparaison de listes électorales
class CompareListesElectorales implements Comparator<ListeElectorale> {
 
    // comparaison de deux listes électorales selon le nombre de sièges
    public int compare(ListeElectorale listeElectorale1,
            ListeElectorale listeElectorale2) {
...
    }
}
  • Zeile 2: Die Klasse implementiert die Schnittstelle Comparator<VoterList>
  • Zeilen 5–6: Die Parameter der compare-Methode sind vom Typ VoterList. Ein Typcasting ist 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 nur Objekte vom Typ Object verarbeiteten. Dies gilt für Listen, Wörterbücher, ...

Wir haben bereits erwähnt, dass wir die Listen nicht in einem Array speichern konnten, da wir die Anzahl der Listen nicht kannten. Sie können in einem ArrayList-Objekt gespeichert werden, das das Konzept einer „Objektliste“ umsetzt. Diese Klasse speichert Objekte vom Typ Object. Seit JDK 1.5 gibt es typisierte Objektlisten. Daher werden wir ein ArrayList<VoterList>-Objekt verwenden, um die Listen zu speichern, bevor wir sie in ein Array übertragen. Wenn das Array den Namen tLists trägt, wird es mit der folgenden Anweisung sortiert:


// tri des listes
Arrays.sort(tListes, new CompareListesElectorales());

wobei CompareVoterLists die Klasse ist, die die Schnittstelle Comparator<VoterList> 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