Algorithmen und Datenstrukturen Übung 2: Generische "binäre Suchbaumknoten" 1 Aufgaben 1.1 Gib den Quellcode zu einer Java-Anwendung an, die die Definition einer Klasse für einen Knoten eines binären Suchbaums zur Aufnahme von Zeichenketten umfaßt. Die zu dieser Anwendung zugehörige Klasse soll - eine Java-Anwendung darstellen - einen binären Suchbaum mit 20 zufällig erzeugten Zeichenketten erstellen - den binären Suchbaum so durchlaufen, daß die in den Baumknoten gespeicherten Zeichenfolgen in aufsteigend sortierter Reihenfolge ausgegeben werden können. 1.2 Übersetze den unter a) vorliegenden Quellcode und führe die daraus resultierende .class-Datei aus. 2. Der unter 1. erstellte Baumknoten kann nur Daten des Typs String aufnehmen. Verallgemeinere die Anwendung so, daß beliebige Typen (zumindest solche die die Schnittstelle Comparable implementiert haben) in den Binärbaumknoten aufgenommen werden können. 2.1 Mit diesen generellen Baumknoten für einen Binärbaum ermittle im Rahmen einer Java-Anwendung Methoden für - das Zusammenstellen von Binärbaumknoten zu einem binären Suchbaum - die Ausgabe einer aufsteigend sortierte Folge der Dateninhalte, die in den zu einem binären Suchbaum zusammengestellten Binärbaumknoten aufgenommen wurden - die Ausgabe der um 90 Grad gedrehten Struktur des binären Baums 2.2 Weiterhin bestimme Methoden für - das Ermitteln des kleinsten Datenwerts, der in den zum binären Suchbaum zusammengestellten Baumknoten, abgelegt wurde - das Ermitteln des größten Datenwerts, der in den zum binären Suchbaum zusammengestellten Baumknoten, abgelegt wurde 3. Der unter 2. angegebene generische Binärbaumknoten besitzt einen entscheidenden Nachteil, den eine Übersetzung mit Java 1.5 aufdeckt. Beim Übersetzen mit Java 1.5 erhält man folgende Hinweise: 2 Note: BinaerBaumknoten.java uses unchecked or unsafe operations Note: Recompile with –Xlint:unchecked for details Führt man den letzten Hinweis aus, dann zeigt der Compiler das Problem an: 1 2 pr42000 vgl. BinaeBaumknoten.java in pr12362 1 Algorithmen und Datenstrukturen Mit Java 1.5 wird das Typsystem auf generische Typen erweitert. Eine generische Klasse, die statische Typsicherheit garantiert, muß jedes Auftreten des Typs Object (im vorliegenden Fall des Referenzdatentyps Comparable) durch einen Variablennamen ersetzen. Die Variable ist eine Typvariable, die für einen beliebigen Typ steht. Dem Klassennamen wird zusätzlich im Rahmen der Klassendefinition - in spitzen Klammern eingeschlossen - hinzugefügt, dass diese Klasse eine Typvariable benutzt. Bei der Definition der Schablone kann eingeschränkt werden, dass eine Typvariable nur für bestimmte Typen ersetzt werden darf. Es kann vorgeschrieben werden, dass der Typ eine bestimmte konkrete Schnittstelle implementieren muß. Ersetze in der unter 2. angegebenen Lösung den Typ Comparable durch eine Typvariable mit der Angabe, dass diese Variable für alle Typen steht, die Subtypen der Schnittstelle Comparable sind. 3 // Elementarer Knoten eines binaeren Baums, der nicht ausgeglichen ist class BinaerBaumknoten<T extends Comparable> { // Instanzvariable protected BinaerBaumknoten<T> links; protected BinaerBaumknoten<T> rechts; public T daten; // linker Teilbaum // rechter Teilbaum // Dateninhalt der Knoten // Konstruktor public BinaerBaumknoten(T datenElement) { this(datenElement, null, null ); } public BinaerBaumknoten(T datenElement, BinaerBaumknoten<T> l, BinaerBaumknoten<T> r) { daten = datenElement; links = l; rechts = r; } public void insert (T x) { if (x.compareTo(daten) > 0) // dann rechts { if (rechts == null) rechts = new BinaerBaumknoten<T>(x); else rechts.insert(x); } else // sonst links { if (links == null) links = new BinaerBaumknoten<T> (x); else links.insert(x); } } public BinaerBaumknoten<T> getLinks() { return links; } public BinaerBaumknoten<T> getRechts() { return rechts; } } 3 vgl. GenericBinaerBaumknoten.java in pr12362 2 Algorithmen und Datenstrukturen Die Lösung ist noch nicht perfekt. Eine abgeleitete Klasse, deren Basisklasse Comparable implementiert, wird nicht akzeptiert, weil die Definition <T extends Comparable<T>> verlangt, dass das Typargument selbst Comparable implementiert. 5. Generische Methoden: Generische Typen sind nicht an einen objektorientierten Kontext gebunden. In Java verläßt man den objektorientierten Kontext in statischen Methoden. Statische Methoden sind nicht an ein Objekt gebunden und lassen sich daher in Java definieren. Hierzu ist vor die Methodensignatur in spitzen Klammern eine Liste der für statische Methoden benutzten Typvariablen anzugeben. Bsp. : Generische Methoden der Klasse TestGenericBinaerBaumKnoten 4 public class TestGenericBinaerBaumKnoten { public static void main (String args[]) { BinaerBaumknoten<Integer> baum = null; /* for (int i = 0; i < 20; i++) // 20 Zusfallsstrings speichern { String s = "Zufallszahl " + (int)(Math.random() * 100); if (baum == null) baum = new BinaerBaumknoten(s); } print(baum); // Sortiert wieder ausdrucken */ for (int i = 0; i < 10; i++) { // Erzeuge eine Zahl zwischen 0 und 100 Integer r = new Integer((int)(Math.random()*100)); if (baum == null) baum = new BinaerBaumknoten<Integer>(r); else baum.insert(r); } System.out.println("Inorder-Durchlauf"); print(baum); System.out.println(); System.out.println("Baumdarstellung um 90 Grad versetzt"); ausgBinaerBaum(baum,0); System.out.print("Kleinster Wert: "); System.out.print(((Integer)(findeMin(baum))).intValue()); System.out.println(); System.out.print("Groesster Wert: "); System.out.print(((Integer)(findeMax(baum))).intValue()); System.out.println(); } // Generische Methoden public static <T extends Comparable> void print (BinaerBaumknoten<? extends T> baum) // Rekursive Druckfunktion { if (baum == null) return; print(baum.getLinks()); System.out.print(baum.daten + " "); print(baum.getRechts()); } public static <T extends Comparable> void ausgBinaerBaum(BinaerBaumknoten<T> b, int stufe) { if (b != null) { ausgBinaerBaum(b.getRechts(), stufe + 1); for (int i = 0; i < stufe; i++) { 4 vgl. pr12362 3 Algorithmen und Datenstrukturen System.out.print(" "); } System.out.println(b.daten); ausgBinaerBaum(b.getLinks(), stufe + 1); } } public static <T extends Comparable> T findeMin(BinaerBaumknoten<T> b) { return datenZugriff( findMin(b) ); } public static <T extends Comparable> T findeMax(BinaerBaumknoten<T> b) { return datenZugriff( findMax(b) ); } public static <T extends Comparable> T datenZugriff(BinaerBaumknoten<T> b) { return b == null ? null : b.daten; } public static <T extends Comparable> BinaerBaumknoten<T> findMin(BinaerBaumknoten<T> b) { if (b == null) return null; else if (b.getLinks() == null) return b; return findMin(b.getLinks()); } public static <T extends Comparable> BinaerBaumknoten<T> findMax(BinaerBaumknoten<T> b) { if (b != null) while (b.getRechts() != null) b = b.getRechts(); return b; } } 6. Generische Typen können ein unbestimmtes Typargument benennen. Syntaktisch wird dann als Typargument das Wildcardzeichen '?' angegeben, z.B. BinaerBaumKnoten<?> b; . Ein derartiger generischer Typ heißt Wildcardtyp. Alle gemischten Typen der gleichen generischen Klasse sind kompatibel zu diesem Wildcardtyp. Zu einem Widcardtyp mit Typargument ? sind alle generischen Typen der betreffenden generischen Klasse kompatibel. Wenn dieser Spielraum zu groß ist, können Wildcarddarstellungen mit Typebounds eingeschränkt werden, z.B. BinaerBaumknoten<? extends Number> nb; . Zu diesem eingeschränkten Wildcardtyp sind nur noch die generischen BinaerBaumknoten-Typen kompatibel, deren Typargument Number ist, z.B. nb = new BinaerBaumKnoten<Integer>(23); nb = new BinaerBaumKnoten<Object>(new Object()); // OK // Fehler Ein derartiger Typebound wird auch als "Upper-Typebound" bezeichnet, weil er aus Sicht der Vererbungshierarchie eine "Obergrenze" für die konkreten Typargumente festlegt. Ein mit extends eingeschränkter Wildcardtyp wird als covarianter Wildcardtyp bezeichnet. Der covariante Wildcardtyp BinaerBaumknoten<? extendsNumber> legt lediglich fest, daß die Knoteninformation irgendeinen zu Number kompatiblen Typ hat. Ein beliebiger anderer, ebenfalls zu Number kompatibler Typ muß dazu nicht unbedingt passen. Der Schreibzugriff wird deshalb abgewiesen. Einzige Ausnahme ist die Zuweisung des Werts null, die zu jedem Referenztyp kompatibel ist. Lesende Zugriffe sind dagegen bei covarianten Wildcardtypen zulässig. 4 Algorithmen und Datenstrukturen Alle diejenigen generischen Typen, deren Typargument vom Upper-Typbound abgeleitet ist, sind zum Wildcardtyp kompatibel. Ebenso sinnvoll ist die umgekehrte Einschränkung mit einem Lower-Typebound, z.B. BinaerBaumknoten<? super Number> nb; . Die Typargumente müssen hier Basistypen des Lower-Typebound sein. Bsp.: BinaerBaumknoten <? super Number> nb; nb = new BinaerBaumknoten<Object>(new Object()); nb = new BinaerBaumknoten<Integer>(23); // OK // Fehler Die Situation wird als Contravarianz bezeichnet, die entsprechenden Wildcardtypen als contravariante Wildcardtypen. Der generische Typ BinaerBaumknoten<Object> ist bspw. kompatibel zum kontravarianten Wildcardtyp BinaerBaumknoten<? super Object>, weil Object ein Basistyp von Number ist. Der kontravariante Wildcardtyp BinaerBaumknoten<? super Number> garantiert, dass die Knoteninformation den Typ Number oder ein Basistyp davon hat. Ein Number-Objekt ist dazu sicherlich kompatibel. Der Schreibzugriff ist deshalb erlaubt. Es kann aber nicht sichergestellt werden, dass die Knoteninformation wirklich Number ist. Deshalb ist kein Lesezugriff zulässig. Eine Ausnahme ist das Lesen eines Object, zu dem jeder Wildcardtyp kompatibel ist. Mit kontraviantern Typebounds kann das Problem der generischen Knotenklasse, z.B. class BinaerBaumKnoten<T extends Comparable<T>> gelöst werden, deren Typebound im ersten Ansatz zu streng war und nur Typargumente zuließ, die selbst das Interface Comparable implementieren. Akzeptiert werden sollen aber auch Typargumente, bei denen irgendeine Basisklasse das Interface Comparable implementiert: class BinaerBaumknoten<T extends Comparable<? super T> . 5 5 Dieser Art von Typebound befindet sich bspw. auch im Java-API der Klasse Collections. 5