Stellen wir uns vor, wir erhalten die Aufgabe einen Binärbaum mit ganzen Zahlen (Integer) in der Programmiersprache Pascal oder C zu implementieren. Unsere Datenstruktur würde etwa wie folgt aussehen:
(* Pascal Version *) Tree = POINTER TO TreeRecord; TreeRecord = RECORD value: INTEGER; left, right: Tree; END; (* TreeRecord *)
(* C Version *) struct Tree { int value; struct Tree *left, *right; };
Warum haben wir diese Datenstruktur gewählt? Wir haben diese Datenstruktur deshalb gewählt, weil wir apriori nicht wissen, wieviele Elemente in den Baum eingefügt werden. Die Anzahl Elemente im Baum schwankt während der Laufzeit, da Elemente hinzugefügt oder aus dem Baum entfernt werden. Mit Zeiger können wir hingegen Datenstrukturen konstruieren, welche sich zur Laufzeit, sprich dynamisch, ändern.
Beispiel: Einfügen von 12, 4 und 70 in einen Binärbaum
Der Verwaltungsaufwand für dynamische Datenstrukturen mit Zeigern ist sehr gross, und Fehler sind schwierig zu finden. Probleme bereitet vor allem die Freigabe des Speichers. Entfernen wir ein Element aus dem Baum, dann können wir nicht ohne weiteres entscheiden, ob der Speicher freigegeben werden kann, da eventuell noch andere Zeiger auf das Element verweisen. Löschen wir es trotzdem, kann das Programm später mit einer Fehlermeldung abbrechen oder, noch viel schlimmer, es liefert falsche Resultate zurück. Dies ist Ihnen sicherlich auch schon geschehen, und die Fehlersuche war bestimmt mühsam.
Ein erster Lösungsansatz: wir geben den Speicher niemals frei. Die oben genannten Probleme verschwinden, doch dürfte nach kurzer Zeit ein neues Problem auftauchen: aller Speicher wird sehr schnell aufgebraucht sein. Dieser Ansatz führt uns also nicht weiter.
Ein zusätzliches Problem, das in der Programmiersprache C auftritt, liegt in der Adressenarithmetik mit Zeigern. Machen wir bei den Berechnungen Fehler, dann sind wir vielleicht mehrere Stunden mit dem Debugger beschäftigt. Noch schlimmer sind die in C üblichen Zeigerkonvertierungen. Ein Zeiger wird explizit von einem Datentyp in einen anderen umgewandelt. Geht dabei etwas schief, dann läuft das Programm meistens weiter, doch dürften die Resultate nicht mehr stimmen oder das Programm stürzt in einer anderen Funktion ab. Solche Fehler sind meistens nur mit grossem Aufwand zu finden. Deswegen verzichtet Java auf Zeiger.
Java kennt zwei verschiedene Arten von Typen: einfache Typen (engl. primitive types) und Referenztypen (engl. reference types). Zu den einfachen Typen zählen boolean, byte, short, int, long, char sowie float und double. Referenztypen sind Klassen-, Interface- und Array-Typen. Zusätzlich kennt Java noch den namenlosen Referenztyp Null, den Rückgabetyp des Objektes null. Jeder Wert, welcher in Variablen gespeichert, als Parameter übergeben oder als Wert eines Methodenaufrufs zurückgegeben wird, ist entweder ein einfacher Wert oder ein Referenzwert. Zeigertypen gibt es in Java nicht, doch schränkt dies die Sprache nicht ein, wie unser folgendes Beispiel mit dynamischen Datenstruktur zeigt:
public Class BinTree { Element tree; // Referenzwert public BinTree(); // Konstruktor public void insert(int value); // einfügen von value public void delete(int value); // löschen von value }class Element { // nur in der Klasse BinTree sichtbar int value; // einfacher Wert Element left, right; // Referenzwerte public Element(int v); }
tree, left und right sind Referenztypen, welche entweder den Wert null besitzen oder Objekte vom Typ Element referenzieren.
Bestimmt haben Sie schon erkannt, dass Java Referenztypen mit Zeiger implementiert. Diese sind jedoch vor uns verborgen. Zeigerarithmetiken und Zeigerkonvertierungen, von denen C Programmierer oft Gebrauch machen, sind nicht mehr möglich. Somit werden unsere Programme robuster. In Bild 1 sehen Sie grafisch wie der Baum beim Einfügen der Elemente 12, 4 und 70 wächst. Die Striche sind Referenzen auf Elemente. Ein Strich ohne Element ist ein null Wert.
Um Programme noch sicherer zu machen geht Java noch weiter: Das Laufzeitsystem von Java nimmt uns die Arbeit des Freigebens von Speicher ab. Dies funktioniert wie folgt: In regelmässigen Abständen wird eine Routine Namens Speichermüllsammler aufgerufen, welche Objekte, die nicht mehr referenziert werden einsammelt, und den nicht mehr benützten Speicher der Objekte freigibt. Die obengenannten Probleme des zu frühen Freigebens oder Nicht-Freigebens verschwinden. In Englisch wird diese Routine Garbage Collector genannt. Dieser Begriff hat sich im deutschsprachigen Raum durchgesetzt, so dass wir ab jetzt von Garbage Collection sprechen.
Beispiel: Ein Binärbaum, aus dem wir Elemente gelöscht haben (rot markiert). Bild 2 zeigt die Speichersituation vor dem Aufruf des Garbage Collectors, Bild 3 nach dem Aufruf.
Refenzen in Java - Tips und Hinweise - Lösungsmöglichkeit
OOP - Werkzeuge - Referenzen - Fäden - Synchronisation - Applets - Dokumentation
Werkstatt - Bibliographie