Dies ist eine alte Version des Dokuments!
Inhaltsverzeichnis
Trennung von Struktur und Daten
Im vorhergehenden Kapitel haben Sie eine Warteschlangen-Implementierung kennengelernt, bei der der Code zur Verwaltung der Listenstruktur mit dem Code zur Verwaltung der Daten vermischt war. Wir werden jetzt Struktur und Daten sauber trennen, so dass der Code zur Verwaltung der Listenstruktur besser wiederverwendbar wird.
Die Lösungsidee besteht darin, jedes Datenobjekt in ein Objekt einer neuen Klasse Knoten
zu "verpacken", das neben dem (Verweis auf das) Datenobjekt auch einen Verweis auf denjenigen Knoten enthält, der den Verweis auf den Nachfolger des aktuellen Inhaltsobjekts beinhaltet. "Einfach verkettet" ist diese Liste übrigens insofern, als die Knoten keinen Verweis auf ihre Vorgänger-Knoten beinhalten (das wäre dann eine "doppelt verkettete Liste").
Beispielhaftes Objektdiagramm
Klassendiagramm
Umsetzung in Java
Die Klasse Inhalt
Die Klasse Inhalt
muss für die Speicherung in der Warteschlange überhaupt nicht mehr angepasst werden:
public class Inhalt { private String text; public Inhalt(String text) { this.text = text; } public void ausgabe() { println(text); } }
Sehen wir uns bei den Klassen Warteschlange
und Knoten
zunächst nur ihre Attribute an:
public class Warteschlange { private Knoten erster; } public class Knoten { private Knoten nachfolger; private Inhalt inhalt; public Knoten(Inhalt inhalt){ this.inhalt = inhalt; } }
Die Klasse Warteschlange
übernimmt in dieser Konstellation die "Kommunikation" mit dem Nutzer der Warteschlange. Der Nutzer ruft weder Methoden der Klasse Knoten direkt auf noch "weiß" er von deren Existenz.
Die Warteschlange setzt damit das Prinzip der Trennung von Struktur und Inhalt um, d.h. bei der Verwendung der Struktur (hier: Warteschlange bzw. Liste) sind keinerlei Änderungen an der Inhaltsklasse notwendig.
Damit die Warteschlange auf die Knoten geeignet zugreifen kann, benötigt die Klasse Knoten
Methoden zum Holen und Setzen der Nachfolger-Referenz (sog. Getter- und Setter-Methoden):
public Knoten getNachfolger() { return nachfolger; } public void setNachfolger(Knoten k) { nachfolger = k; }
Die Methode erstenEntnehmen
der Warteschlange kann damit folgendermaßen umgesetzt werden:
public Inhalt erstenEntnehmen() { if(erster == null) { return null; } Inhalt inhalt = erster.getInhalt(); erster = erster.getNachfolger(); return inhalt; }
Schwieriger gestaltet sich das Einfügen des letzten Knotens, da das Warteschlangen-Objekt ja nur die Referenz auf den ersten Knoten speichert und man sich von diesem ausgehend zum letzten Knoten "durchhangeln" muss. Dies kann man entweder durch eine Iteration (d.h. unter Nutzung einer Wiederholung, z.B. for(){…}) oder durch Rekursion (d.h. dadurch, dass eine Methode sich selbst aufruft) gelöst werden.
Hintenanstellen: Iterativ
Die iterative Lösung "bewegt" sich durch die Warteschlange/Liste, bis ein Knoten ohne Nachfolger (d.h. die o.g. Methode hat den Rückgabewert null
) gefunden wird:
public void hintenAnstellenIterativ(Inhalt inhalt) { if(erster == null) { erster = new Knoten(inhalt); } else { Knoten aktuellerKnoten = erster; while(aktuellerKnoten.getNachfolger() != null) { aktuellerKnoten = aktuellerKnoten.getNachfolger(); } aktuellerKnoten.setNachfolger(new Knoten(inhalt)); } }
Hintenanstellen: Rekursiv
Die rekursive Lösung ist zweigeteilt. Der einfachste Fall (die Liste ist leer) wird direkt in der Klasse Warteschlange behandelt (es wird ein neuer Knoten erzeugt und als erster Knoten eingesetzt). Die "schwierigeren" Fälle werden von der Klasse Knoten behandelt: Jeder Knoten überprüft, ob er einen Nachfolger hat. Im positiven Fall wird das einzufügende Element an diesen übergeben (durch Aufruf derselben Methode – daher wird diese Lösungsstrategie als rekursiv bezeichnet). Anderenfalls (genannt Abbruchbedingung) erzeugt der Knoten selbst einen neuen Knoten mit dem übergebenen Inhaltselement und setzt diesen als neuen Nachfolger ein.
Klasse Warteschlange:
public void hintenAnstellenRekursiv(Inhalt inhalt) { if(erster == null) { erster = new Knoten(inhalt); } else { erster.hintenAnstellenRekursiv(inhalt); } }
Klasse Knoten:
public void hintenAnstellenRekursiv(Inhalt inhalt) { if(nachfolger == null) { nachfolger = new Knoten(inhalt); } else { nachfolger.hintenAnstellenRekursiv(inhalt); } }
Auch die Ermittlung der Anzahl der enthaltenen Elemente kann sowohl iterativ als auch rekursiv gelöst werden:
Anzahl der enthaltenen Elemente: iterativ
Klasse Warteschlange:
public int getAnzahlIterativ() { if(erster == null) { return 0; } int anzahl = 1; Knoten aktuellerKnoten = erster; while(aktuellerKnoten.getNachfolger() != null) { aktuellerKnoten = aktuellerKnoten.getNachfolger(); anzahl++; } return anzahl; }
Anzahl der enthaltenen Elemente: rekursiv
Hier kümmert sich die Methode getAnzahlRekursiv
der Klasse Warteschlange
selbst wieder nur um den trivialen Fall (erster == null
, also Anzahl == 0) und "fragt" im nichttrivialen Fall beim ersten Knoten nach:
Klasse Warteschlange:
public int getAnzahlRekursiv() { if(nachfolger == null) { return 1; } return nachfolger.getAnzahlRekursiv() + 1; }
Sequenzdiagramm am Beispiel einer Warteschlange mit drei Knoten:
Gesamtprogramm
Aufgabe 1:
Erweitere die Klasse Warteschlange um eine Methode gibDenNtenWertZurück(int n)
, die den Inhalt des n-ten Elements der Warteschlange zurückgibt.
Lösung
Aufgabe 2:
Erweitere die Klasse Warteschlange um eine Methode letztesElementLoeschen()
, die das letzte Element der Warteschlange löscht.
Lösung