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:
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:
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:
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:
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.