i

Generische und innere Klassen

Schwächen der implementierten Stapel-Klasse

Im Abschnitt Stapel hast Du eine Stapel-Klasse implementiert. Diese Klasse funktioniert und ist für das Projekt Keep or throw völlig in Ordnung. Wenn man aber die Stapel-Klasse in anderen Projekten wiederverwenden möchte, dann geht das nicht ohne Änderungen an der Klasse. Wie man einen solchen Entwurf so abändert, dass man die programmierte Datenstruktur ohne Änderungen wiederverwenden kann, soll hier an einem anderen Beispiel dargestellt werden.

Eine vereinfachte Liste

Für die weiteren Überlegungen soll eine vereinfachte Liste die Grundlage liefern. Die Liste soll Aufgaben verwalten. Die Objekte der Liste sind ähnlich des Stapels organisiert, wobei das Listenobjekt den Anfang und das Ende der Liste kennen soll:

Objektdiagramm einfache Liste

Die Liste soll nur exemplarische Funktionalität bieten. Es sollen Aufgaben angehängt werden können und man soll die letzte Aufgabe abfragen können. Damit ergibt sich folgendes Klassendiagramm:

Klassendiagramm einfache Liste

Die Klassen lassen sich so implementieren:

class Aufgabe
{
    String beschreibung;
    Aufgabe next;

    Aufgabe(String b) {
        beschreibung = b;
    }
}
class Liste
{
    Aufgabe anfang;
    Aufgabe ende;

    void anhaengen(Aufgabe a) {
        if(anfang == null) {
            anfang = a;
            ende = anfang;
        }
        else {
            ende.next = a;
            ende = a;
        }
    }
    
    Aufgabe getEnde() {
        return ende;
    }
}

In einem Testprogramm oder im Codepad von BlueJ könnte die Liste so benutzt werden:

Liste liste = new Liste();
liste.anhaengen(new Aufgabe("Aufstehen"));
liste.anhaengen(new Aufgabe("Frühstücken"));
liste.anhaengen(new Aufgabe("In die Schule gehen"));
Aufgabe a = liste.getEnde();
System.out.println(a.beschreibung);

Modellierung mit Zwischenschicht

Die oben dargestellte Liste lässt sich nicht gut wiederverwenden. Dies lässt sich erreichen, indem man eine weitere Schicht von Objekten nutzt:

Verbesserte Liste mit Zwischenschicht

Die Referenzen auf das nächste Listenelement werden dann nicht mehr in Objekten wie den Aufgaben gespeichert, die eigentlich logisch nicht dafür zuständig sein sollten. Wenn man dann noch den Datentyp der anzuhängenden Elemente von Aufgabe zu Object (also der Oberklasse aller Klassen in Java) ändert, kann die Liste beliebige Daten verwalten:

Verbesserte Liste mit Zwischenschicht

Aus der Aufgabenklasse wird das Attribut next entfernt:

class Aufgabe
{
    String beschreibung;

    Aufgabe(String b) {
        beschreibung = b;
    }
}

Die Listenelemente erhalten eine Referenz auf den eigentlich zu verwaltenden Inhalt und das nächste Listenelement:

class Listenelement
{
    Listenelement next;
    Object content;
    
    Listenelement(Object o) {
        content = o;
    }
}

Die Liste kann nun beliebige Elemente verwalten. Beim Anhängen eines Elements muss dazu ein Listenelement erzeugt werden. Außerdem sind die Datentypen von Aufgabe zu Object zu ändern:

class Liste
{
    Listenelement anfang;
    Listenelement ende;

    void anhaengen(Object o) {
        Listenelement neuesElement = new Listenelement(o);
        if(anfang == null) {
            anfang = neuesElement;
            ende = neuesElement;
        }
        else {
            ende.next = neuesElement;
            ende = neuesElement;
        }
    }

    Object getEnde() {
        // Gib nicht das Listenelement, sondern dessen content zurück
        return ende.content;
    }
}

Man erkauft sich die Flexibilität (momentan noch) mit zwei Nachteilen:

  • Die Liste kennt den Datentyp der zu verwaltenden Objekte nicht. Deshalb gibt sie syntaktisch nur Objekte vom Typ Object zurück. Möchte man diese z.B. als Aufgabe speichern, muss man einen sogenannten Typecast, also eine explizite Typumwandlung vornehmen. Dies erreicht man durch Angabe des Datentyps in runden Klammern.
  • Da die Liste den zu verwaltenden Datentyp nicht kennt, können beliebige Objekte hinzugefügt werden. Meist ist dies unerwünscht, da man die Typsicherheit von Java hier aufgibt. Der Compiler kann also ein versehentlich vom Programmierer zugefügtes Objekt nicht mehr am Datentyp erkennen.

Ein Testprogramm könnte dann folgende Form haben:

Liste liste = new Liste();
liste.anhaengen(new Aufgabe("Aufstehen"));
liste.anhaengen(new Aufgabe("Frühstücken"));
liste.anhaengen(new Aufgabe("In die Schule gehen"));
// Geht nur mit Typecast
Aufgabe a = (Aufgabe) liste.getEnde();
System.out.println(a.beschreibung);

// Weiterer Nachteil: Man kann beliebige Objekte hinzufügen,
// was meist nicht gewünscht ist:
liste.anhaengen(new String());

Innere Klassen

Logisch gehört die Klasse Listenelement zur Klasse Liste. Man wird nie ein Listenelement außerhalb der Liste erzeugen. Für solche Fälle bietet die Sprache Java die Möglichkeit innere Klassen zu definieren. Man erreicht dies ganz einfach, indem man den Code der Klasse Listenelement in die Klasse Liste verschiebt. Da man auf Listenelement außerhalb der Liste nie zugegriffen hat, verändert sich die Benutzung in keiner Weise.

class Liste
{
    Listenelement anfang;
    Listenelement ende;

    void anhaengen(Object o) {
        // ... unverändert
    }

    Object getEnde() {
        // ... unverändert
    }

    // verschobene Klasse
    class Listenelement
    {
        Listenelement next;
        Object content;

        public Listenelement(Object o) {
            content = o;
        }

    }
}

Generische Klassen

Es bleibt noch das Problem, dass man die Typsicherheit in Java aufgibt und mit Typecasts leben muss. Dies lässt sich beheben, indem man Liste als generische Klasse oder parametrisierte Klasse definiert. Beide Begriffe meinen das gleiche. Der Begriff generische Klasse ist etwas geläufiger, der Begriff parametrisierte Klasse beschreibt das Konzept aber deutlicher. Eine generische oder parametrisierte Klasse erhält einen Typ-Parameter, der innerhalb der Klasse benutzt werden kann. Der Typ-Parameter wird in spitzen Klammern hinter den Klassennamen geschrieben und kann dann innerhalb der Klasse wie ein konkreter Datentyp benutzt werden:

class Liste<T>
{
    Listenelement anfang;
    Listenelement ende;

    void anhaengen(T o) {
        // ... unverändert
    }

    T getEnde() {
        // ... unverändert
    }

    class Listenelement
    {
        Listenelement next;
        T content;

        public Listenelement(T o) {
        // ... unverändert
        }
    }
}

Um den Typ-Parameter zu nutzen, wird der konkrete Datentyp bei der Definition von Variablen und beim Konstruktoraufruf in spitzen Klammern angegeben:

Liste<Aufgabe> liste = new Liste<Aufgabe>(); 
// als Abkürzung auch möglich:
// Liste<Aufgabe> liste = new Liste<>(); 
liste.anhaengen(new Aufgabe("Aufstehen"));
liste.anhaengen(new Aufgabe("Frühstücken"));
liste.anhaengen(new Aufgabe("In die Schule gehen"));
// Typecast ist nicht mehr notwendig
Aufgabe a = liste.getEnde();
System.out.println(a.beschreibung);

// Geht nicht mehr
// liste.anhaengen(new String());

Den Typecast benötigt man nicht mehr. Man kann nun auch keine Objekte mehr hinzufügen, die nicht dem angegebenen Typ entsprechen. Dies wird schon vom Compiler erkannt.

Die Klasse Liste ist nun völlig losgelöst von der späteren Verwendung implementiert und kann genau so in anderen Projekten benutzt werden. Durch die Verwendung einer inneren Klasse wurde der Code übersichtlicher. Durch die Verwendung einer generischen Klasse erhält man sich die Typsicherheit in Java und benötigt keine Typecasts.

Aufgabe 1 - Aufgabenliste

Nimm die erste Version der Aufgabenliste als Grundlage und vollziehe die Erklärungen nach, indem Du diese selbst schrittweise veränderst, bis Du eine wiederverwendbare Listen-Klasse hast.

Aufgabe 2 - Stapel

Verändere analog die von Dir programmierte Stapel-Klasse.

Suche

v
7.1.3.2.4
inf-schule.de/oop/java/beziehungen/keeporthrow/generics
inf-schule.de/7.1.3.2.4
inf-schule.de/@/page/FkJNMSoXnTIHPSdG

Rückmeldung geben