10 Besondere Typen der Java SE
»Einen Rat befolgen heißt, die Verantwortung verschieben.«
– Johannes Urzidil (1896–1970)
Programmieren wir mit Java, nutzen wir oftmals unbewusst Typen aus der Standardbibliothek. Das fällt oft gar nicht auf, da zum einen das Paket java.lang automatisch importiert wird – und damit Typen wie String, Object immer eingebunden sind – und weil zum anderen einiges hinter den Kulissen passiert. Sechs Beispiele:
Erweitert eine Oberklasse keine eigene Klasse, so erbt sie automatisch von java.lang. Object.
Ist ein primitiver Datentyp gegeben, aber ein Objekttyp gewünscht, so konvertiert der Compiler den einfachen Datentyp in ein Wrapper-Objekt. Das nennt sich Boxing.
Hängen wir Strings mit + zusammen, so erzeugt der Compiler – zumindest bis Java 8 – automatisch einen java.lang.StringBuilder, hängt die Segmente mit append(…) zusammen und liefert dann mit toString() einen neuen String. Bei keinem anderen Referenztyp erlaubt der Compiler die »Addition«, sondern nur Vergleiche mit == oder !=.
Beim erweiterten for erwartet der Compiler entweder ein Array oder etwas vom Typ Iterable: Von diesen Objekten erfragt er den Iterator und läuft selbstständig durch die Sammlung.
Damit try mit Ressourcen verwendet werden kann, erwartet der Compiler ein AutoCloseable und ruft auf diesen Objekten im finally-Block die close()-Methode auf.
Bei der Deklaration eines Aufzählungstyps mit enum generiert der Compiler eine von java.lang.Enum abgeleitete Klasse. Selbst darf ein Programmierer keine Unterklassen von Enum bilden, das verbietet der Compiler.
Dieses Kapitel stellt unterschiedliche Typen vor, die in irgendeiner Weise bevorzugt werden oder eine Sonderstellung in Java einnehmen, weil sie allgegenwärtig sind. Dazu zählen:
die Basisklasse Object
Vergleichsobjekte
Wrapper-Klassen
Aufzählungstypen, enum und die Sonderklasse Enum
Nicht zufällig liegen einige Typen im Paket java.lang, da die Typen so elementar sind.
10.1 Object ist die Mutter aller Klassen
java.lang.Object (siehe Abbildung 10.1) ist die oberste aller Elternklassen. Somit spielt diese Klasse eine ganz besondere Rolle, da alle anderen Klassen automatisch Unterklassen sind und die Methoden erben bzw. überschreiben.
10.1.1 Klassenobjekte
Zwar ist jedes Objekt ein Exemplar einer Klasse – doch was ist eine Klasse? In einer Sprache wie C++ existieren Klassen nicht zur Laufzeit, und der Compiler übersetzt die Klassenstruktur in ein ausführbares Programm. Im absoluten Gegensatz dazu steht Smalltalk: Diese Laufzeitumgebung verwaltet Klassen selbst als Objekte. Diese Idee, Klassen als Objekte zu repräsentieren, übernimmt auch Java – Klassen sind Objekte vom Typ java.lang.Class.
class java.lang.Object
final Class<? extends Object> getClass()
Liefert die Referenz auf das Klassenobjekt, die das Objekt konstruiert hat. Das Class-Objekt ist immer eindeutig in der JVM, sodass ein Aufruf von x.getClass() von unterschiedlichen Exemplaren x vom Typ X immer dasselbe Class<X>-Objekt liefert. Die Class-Exemplare lassen sich also sicher mit == prüfen.
[zB] Beispiel
Die Objektmethode getName() eines Class-Objekts liefert den Namen der Klasse:
System.out.println( "Klaviklack".getClass().getName() ); // java.lang.String
Klassen-Literale
Ein Klassen-Literal (engl. class literal) ist ein Ausdruck der Form Datentyp.class, wobei Datentyp eine Klasse, eine Schnittstelle, ein Array oder ein primitiver Typ ist. Beispiele sind:
Der Ausdruck ist immer vom Typ Class. Bei primitiven Typen liefert die Schreibweise primitiverTyp.class das gleiche Ergebnis wie WrapperTyp.TYPE. Integer.TYPE ist also identisch mit int.class. Class-Objekte spielen insbesondere bei dynamischen Abfragen über die sogenannte Reflection eine Rolle. Zur Laufzeit können so beliebige Klassen geladen, Objekte erzeugt und Methoden aufgerufen werden.
10.1.2 Objektidentifikation mit toString()
Jedes Objekt sollte sich durch die Methode toString() mit einer Zeichenkette identifizieren und den Inhalt der interessanten Attribute als Zeichenkette liefern.
[zB] Beispiel
Die Klasse Point implementiert toString() so, dass der Rückgabe-String die Koordinaten enthält:
System.out.println( new java.awt.Point() ); // java.awt.Point[x=0,y=0]
Das Angenehme ist, dass toString() automatisch aufgerufen wird, wenn die Methoden printXXX(…) mit einer Objektreferenz als Argument aufgerufen werden. Ähnliches gilt für den Zeichenkettenoperator + mit einer Objektreferenz als Operand:
public class Player {
String name;
int age;
@Override
public String toString() {
return getClass().getName() + "[name=" + name + ",age=" + age + "]";
}
}
Die Ausgabe mit den Zeilen
Player tinkerbelle = new Player();
tinkerbelle.name = "Tinkerbelle";
tinkerbelle.age = 32;
System.out.println( tinkerbelle.toString() );
System.out.println( tinkerbelle );
ist damit:
com.tutego.insel.object.tostring.Player[name=Tinkerbelle,age=32]
com.tutego.insel.object.tostring.Player[name=Tinkerbelle,age=32]
Bei einer eigenen Implementierung müssen wir darauf achten, dass die Sichtbarkeit public ist, da toString() in der Oberklasse Object öffentlich vorgegeben ist und wir in der Unterklasse die Sichtbarkeit nicht einschränken können. Zwar bringt die Spezifikation nicht deutlich zum Ausdruck, dass toString() nicht null als Rückgabe liefern darf, doch ist dann der Leer-String "" allemal besser. Die Annotation @Override macht das Überschreiben deutlich.
[ ! ] Warnung
Einige kreative Programmierer nutzen die toString()-Repräsentation für Objektvergleiche, etwa so: Wenn wir zwei Point-Objekte p und q haben und p.toString().equals(q.toString()) ist, dann sind beide Punkte eben gleich. Doch ist es hochgradig gefährlich, sich auf die Rückgabe von toString() zu verlassen, und zwar aus mehreren Gründen: Offensichtlich ist, dass toString() nicht unbedingt überschrieben sein muss. Zweitens muss toString() nicht unbedingt alle Elemente repräsentieren, und die Ausgabe könnte abgekürzt sein. Drittens können natürlich Objekte equals-gleich sein, auch wenn ihre String-Repräsentation nicht gleich ist, was etwa bei URL-Objekten der Fall ist. Der einzige erlaubte Fall für so eine Konstruktion wäre String/StringBuilder/StringBuffer/CharSequence, wo es ausdrücklich um Zeichenketten geht. Neben dem fehlerhaften Verhalten gibt es in der Regel ein massives Performance-Problem. equals(…) nimmt ja in der Regel Abkürzungen, sodass zum Beispiel obj.equals(obj) sofort true liefert oder dass bei Datenstrukturen erst einmal auf die gleiche Länge getestet wird, bevor es zum Elementvergleich kommt.
Standardimplementierung
Neue Klassen sollten toString() überschreiben. Ist dies nicht der Fall, gelangt das Programm zur Standardimplementierung in Object, wo lediglich der Klassenname und der wenig aussagekräftige Hashwert hexadezimal zusammengebunden werden:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
class java.lang.Object
String toString()
Liefert eine String-Repräsentation des Objekts aus Klassenname und Hashwert.
Zwar sagt der Hashwert selbst wenig aus, allerdings ist er ein erstes Indiz dafür, dass bei Klassen, die keine toString()- und hashCode()-Methoden überschreiben, zwei Referenzen nicht identisch sind.
[zB] Beispiel
Ein Objekt der class A {} wird gebildet, und toString() liefert die ID:
class A {}
A a = new A();
System.out.println( "1. " + a + ", 2. " + a + ", 3. " + new A() );
Die Ausgabe kann sein:
1. Main$1A@4554617c, 2. Main$1A@4554617c, 3. Main$1A@74a14482
Bei mehrfachen Aufrufen von toString() auf einem Exemplar – Ausgabe 1 und 2 – bleibt die Rückgabe konstant. Wird das Programm neu gestartet, kann der Hashwert anders aussehen.
toString()-Methode generieren lassen
Die Methode eignet sich gut zum Debugging, doch ist das manuelle Tippen der Methoden lästig. Zwei Lösungen vereinfachen das Implementieren der Methode toString():
Eclipse und IntelliJ können standardmäßig über das Kontextmenü eine toString()-Methode anhand ausgewählter Attribute generieren. Das Gleiche gilt im Übrigen auch für equals(…) und hashCode().
Die Zustände werden automatisch über Reflection ausgelesen. Hier führt Apache Commons Lang (http://commons.apache.org/proper/commons-lang) auf den richtigen Weg.
10.1.3 Objektgleichwertigkeit mit equals(…) und Identität
Ob zwei Referenzen dasselbe Objekt repräsentieren, stellt der Vergleichsoperator == fest, != prüft das Gegenteil. Die Operatoren testen die Identität, wissen aber nichts von einer möglichen inhaltlichen Gleichwertigkeit. Am Beispiel mit Zeichenketten ist das gut zu erkennen: Ein Vergleich mit firstname == "Christian" hat im Allgemeinen einen falschen, unbeabsichtigten Effekt, obwohl er syntaktisch korrekt ist. An dieser Stelle sollte der inhaltliche Vergleich stattfinden: Stimmen alle Zeichen der Zeichenkette überein?
Eine equals(…)-Methode sollte Objekte auf Gleichwertigkeit prüfen. So besitzt das String-Objekt eine Implementierung, die Zeichen um Zeichen vergleicht:
String firstname = "Christian";
if ( "Christian".equals( firstname ) )
...
class java.lang.Object
boolean equals(Object o)
Testet, ob das andere Objekt gleich dem eigenen ist. Die Gleichwertigkeit definiert jede Klasse für sich anders, doch die Basisklasse vergleicht nur die Referenzen o == this.
equals(…)-Implementierung aus Object und Unterklassen
Die Standardimplementierung aus der absoluten Oberklasse Object kann über die Gleichwertigkeit von speziellen Objekten nichts wissen und testet lediglich die Referenzen:
public boolean equals( Object obj ) {
return this == obj;
}
Überschreibt eine Klasse equals(Object) nicht, ist das Ergebnis von o1.equals(o2) gleichwertig mit o1 == o2. Unterklassen überschreiben diese Methode, um einen inhaltlichen Vergleich mit ihren Zuständen vorzunehmen. Die Methode ist in Unterklassen gut aufgehoben, denn jede Klasse benötigt eine unterschiedliche Logik, um festzulegen, wann ein Objekt gleich einem anderen Objekt ist.
Nicht jede Klasse implementiert eine eigene equals(Object)-Methode, sodass die Laufzeitumgebung unter Umständen ungewollt bei Object und seinem Referenzvergleich landet. Dies hat ungeahnte Folgen, und diese Fehleinschätzung kommt leider bei Exemplaren der Klassen StringBuilder und StringBuffer vor, die kein eigenes equals(…) implementieren. Wir haben dies bereits in Kapitel 5, »Der Umgang mit Zeichenketten«, erläutert.
equals(…)-Methode überschreiben
Bei selbst deklarierten Methoden ist Vorsicht geboten, da wir genau auf die Signatur achten müssen. Die Methode muss ein Object akzeptieren und boolean zurückgeben. Wird diese Signatur falsch verwendet, kommt es statt zu einer Überschreibung der Methode zu einer Überladung und bei einer Rückgabe ungleich boolean zu einer zweiten Methode mit gleicher Signatur, was Java nicht zulässt (Java erlaubt bisher keine kovarianten Parametertypen). Um das Problem zu minimieren, sollte die Annotation @Override an equals(Object) angeheftet sein.
Die equals(Object)-Methode stellt einige Anforderungen:
Heißt der Vergleich equals(null), so ist das Ergebnis immer false.
Kommt ein this hinein, lässt sich eine Abkürzung nehmen und true zurückliefern.
Das Argument ist zwar vom Typ Object, aber dennoch vergleichen wir immer konkrete Typen. Eine equals(Object)-Methode einer Klasse X wird sich daher nur mit Objekten vom Typ X vergleichen lassen. Eine spannende Frage ist, ob equals(Object) auch Unterklassen von X beachten soll.
Eine Implementierung von equals(Object) sollte immer eine Implementierung von hashCode() bedeuten, denn wenn zwei Objekte equals(…)-gleich sind, müssen auch die Hashwerte gleich sein. Bei einer geerbten hashCode()-Methode aus Object ist das aber nicht in jedem Fall erfüllt.
[»] Hinweis
Der Datentyp für den Parameter in der equals(Object)-Methode ist immer Object und niemals etwas anderes, da sonst equals() nicht überschrieben, sondern überladen wird. Folgendes für eine Klasse Player ist also falsch:
public class Player {
private int age;
public boolean equals( Player that ) { return this.age == that.age; }
}
Im Vokabular der Informatiker gesprochen: Java unterstützt bisher keine kovarianten Parametertypen, wohl aber kovariante Rückgabetypen. Daher ist es gut, die Annotation @Override zu setzen, denn sie schlägt Alarm, falls wir glauben, eine Methode zu überschreiben, es dann aber doch nicht tun.
Grundlegender Aufbau der equals(…)-Methode
Die Punkte 1 bis 4 sind nur die Vorarbeit bis zum eigentlichen Vergleich. Dann geht es darum, Attribut für Attribut vom eigenen Objekt mit dem Zustand eines anderen Objekts zu vergleichen. Wir unterscheiden Tests von primitiven Werten, Tests von Arrays und Tests von Referenztypen.
Für boolean und alle ganzzahligen primitiven Werte ist ein einfacher ==-Vergleich möglich. Beim ==-Vergleich von Fließkommazahlen kommt es wegen des Sonderwertes NaN zu einem Problem; das umgeht die Konvertierung in ein Ganzzahl-Bit-Muster. Grundsätzlich könnte auch ein Test mit den statischen compareTo(…)-Methoden in den Wrapper-Klassen helfen, in dem mit compareTo(…) == 0 auf die Gleichheit geprüft wird.
Für den Vergleich von Arrays bietet sich Arrays.equals(array1, array2) an.
Für den Vergleich von Referenzen wird der Vergleich an die Objekte weitergegeben, wobei darauf zu achten ist, ob die Referenzvariablen null sind. Die praktische Hilfsmethode Objects.equals(obj1, obj2) kümmert sich darum.
[+] Tipp
Natürlich ließe sich auf die Objektmethode equals(…) der numerischen Wrapper-Klassen zurückgreifen, doch das würde bedeuten, für jeden primitiven Vergleich immer neue Objekte aufzubauen. Das kostet unnötig Zeit, denn equals(…) und auch hashCode()-Methoden müssen schnell sein, da sie bei Operationen in Datenstrukturen oft aufgerufen werden.
Beispiel einer eigenen equals(…)-Methode
Die beiden ersten Punkte sind leicht erfüllbar, und ein Beispiel für einen Club mit den Attributen numberOfPersons und sm (für die Quadratmeter) ist schnell implementiert:
@Override
public boolean equals( Object o ) {
if ( o == null )
return false;
if ( o == this )
return true;
Club that = (Club) o;
return this.numberOfPersons == that.numberOfPersons
&& this.sm == that.sm;
}
Diese Lösung erscheint offensichtlich, führt aber spätestens bei einem Nicht-Club-Objekt zu einer ClassCastException. Das Problem scheint schnell behoben:
if ( ! o instanceof Club )
return false;
Jetzt sind wir auf der sicheren Seite, aber ist das Ziel erreicht?
[»] Hinweis
Die equals(…)-Methode gibt bei nicht passenden Typen immer false zurück und löst keine Ausnahme aus.
Das Problem der Symmetrie *
Zwar funktioniert die aufgeführte Implementierung bei finalen Klassen schön, doch bei Unterklassen ist die Symmetrie gebrochen. Warum? Ganz einfach: instanceof testet Typen in der Hierarchie, liefert also auch dann true, wenn das an equals(…) übergebene Argument eine Unterklasse von Club ist. Diese Unterklasse wird wie die Oberklasse die gleichen Attribute haben, sodass – aus der Sicht von Club – alles in Ordnung ist. Nehmen wir einmal die Variablen club und superClub an, die die Typen Club und SuperClub (die fiktive Unterklasse von Club) besitzen. Sind beide Objekte gleich, so ergibt club.equals(superClub) das Ergebnis true. Drehen wir den Spieß um, und fragen wir, was superClub.equals(club) ergibt. Zwar haben wir SuperClub nicht implementiert, wir nehmen aber an, dass dort eine equals(…)-Methode steckt, die nach dem gleichen instanceof-Schema implementiert wurde wie Club. Dann wird dort bei einem Test Folgendes ausgeführt: club instanceof superClub – und das ist false. Damit wird aber die Fallunterscheidung mit return false beendet. Fassen wir zusammen:
club.equals( superClub ) == true
superClub.equals( club ) == false
Das darf nicht sein, und zur Lösung dürfen wir nicht instanceof verwenden, sondern müssen fragen, ob der Typ exakt ist. Das geht mit getClass() und einem einfachen Referenzvergleich.[ 193 ](Class-Objekte müssen nicht über equals(…) verglichen werden, denn Class-Objekte haben kein eigenes equals(…), sondern erben nur die Implementierung von Object, und dort wird auch nur ein Referenzvergleich vorgenommen. ) Korrekt ist daher Folgendes:
public class Club {
int numberOfPersons;
int sm;
@Override
public boolean equals( Object o ) {
if ( o == null )
return false;
if ( o == this )
return true;
if ( o.getClass() != getClass() )
return false;
Club that = (Club) o;
return this.numberOfPersons == that.numberOfPersons
&& this.sm == that.sm;
}
@Override
public int hashCode() {
return (31 + numberOfPersons) * 31 + sm;
}
}
Die hashCode()-Methode besprechen wir in Abschnitt 10.1.5, »Hashwerte über hashCode() liefern *«. Sie steht nur der Vollständigkeit halber hier, da equals(…) und hashCode() immer Hand in Hand gehen sollten.
Es ist günstig, bei erweiterten Klassen ein neues equals(…) anzugeben, sodass auch die neuen Attribute in den Test einbezogen werden. Bei hashCode()-Methoden müssen wir eine ähnliche Strategie anwenden.
Einmal gleich, immer gleich *
Ein weiterer Aspekt von equals(…)[ 194 ](Eine korrekte Implementierung der Methode equals(…) bildet eine Äquivalenzrelation. Lassen wir die null-Referenz außen vor, ist sie reflexiv, symmetrisch und transitiv. ) ist der folgende: Wenn das Objekt nicht verändert wird, muss das Ergebnis während der gesamten Lebensdauer eines Objekts gleich bleiben. Ein kleines Problem steckt dabei in equals(…) der Klasse URL, die vergleicht, ob zwei URL-Adressen auf die gleiche Ressource zeigen. In der Dokumentation heißt es:
»Two URL objects are equal if they have the same protocol, reference equivalent hosts, have the same port number on the host, and the same file and fragment of the file.«
Hostnamen gelten als gleich, wenn entweder beide auf dieselbe IP-Adresse zeigen oder – falls eine IP-Adresse nicht auflösbar ist – beide Hostnamen gleich (ohne Groß-/Kleinschreibung) oder null sind. Da hinter den URLs http://tutego.com/ und http://www.tutego.com/ aber letztendlich http://www.tutego.de/ steckt, liefert equals(…) die Rückgabe true:
URL url1 = new URL( "http://tutego.com/" );
URL url2 = new URL( "http://www.tutego.com/" );
System.out.println( url1.equals( url2 ) ); // true
Die dynamische Abbildung der Hostnamen auf die IP-Adresse des Rechners kann aus mehreren Gründen problematisch sein:
Wenn keine Netzwerkverbindung besteht, wird keine Namensauflösung durchgeführt und der Vergleich liefert false. Die Rückgabe sollte jedoch nicht davon abhängig sein, ob eine Netzwerkverbindung besteht.
Dass die beiden URLs auf den gleichen Server zeigen, könnte sich zur Laufzeit ändern.
10.1.4 Klonen eines Objekts mit clone() *
Zum Replizieren eines Objekts gibt es oft zwei Möglichkeiten:
einen Konstruktor (auch Copy-Konstruktor genannt), der ein vorhandenes Objekt als Vorlage nimmt, ein neues Objekt anlegt und die Zustände kopiert
Was eine Klasse nun anbietet, ist in der API-Dokumentation zu erfahren.
[zB] Beispiel
Erzeuge ein Punkt-Objekt, und klone es:
java.awt.Point p = new java.awt.Point( 12, 23 );
java.awt.Point q = (java.awt.Point) p.clone();
System.out.println( q ); // java.awt.Point[x=12,y=23]
Mehr als 300 Klassen der Java-Bibliothek unterstützen ein clone(), das ein neues Exemplar mit dem gleichen Zustand zurückgibt. Eine überschriebene Methode kann den Typ der Rückgabe dank kovarianter Rückgabetypen anpassen. Die clone()-Methode bei java.awt.Point bleibt allerdings bei Object.
Array-Objekte bieten standardmäßig clone(). Speichern die Arrays jedoch nichtprimitive Werte, liefert clone() nur eine flache Kopie, was bedeutet, dass das neue Array-Objekt, der Klon, die exakt gleichen Objekte wie das Original referenziert und die Einträge selbst nicht klont.
clone() aus java.lang.Object
Da clone() nicht automatisch unterstützt wird, stellt sich die Frage, wie wir clone() für unsere Klassen mit geringstem Aufwand umsetzen können. Einfach clone() aufzurufen, funktioniert jedoch nicht, da die Methode protected ist, also erst einmal nicht sichtbar ist.
class java.lang.Object
protected Object clone() throws CloneNotSupportedException
Liefert eine Kopie des Objekts.
Eine eigene clone()-Methode
Eigene Klassen überschreiben die protected-Methode clone() aus der Oberklasse Object und machen sie public. Für die Implementierung kommen zwei Möglichkeiten in Betracht:
Wir könnten von Hand ein neues Objekt anlegen, alle Attribute kopieren und die Referenz auf das neue Objekt zurückgeben.
Das Laufzeitsystem soll selbst eine Kopie anlegen, und diese geben wir zurück. Lösung zwei verkürzt die Entwicklungszeit und ist auch spannender.
Um das System zum Klonen zu bewegen, müssen zwei Dinge getan werden:
Der Aufruf super.clone() stößt die Methode clone() aus Object an und veranlasst so die Laufzeitumgebung, ein neues Objekt zu bilden und die nichtstatischen Attribute zu kopieren. Die Methode kopiert elementweise die Daten des aktuellen Objekts in das neue. Die Methode ist in der Oberklasse protected, aber das ist der Trick: Nur Unterklassen können clone() aufrufen, keiner von außen, der sich nicht in der Vererbungshierarchie befindet.
Die Klasse implementiert die Markierungsschnittstelle Cloneable. Falls von außen ein clone() auf einem Objekt aufgerufen wird, dessen Klasse nicht Cloneable implementiert, ist das Ergebnis eine CloneNotSupportedException. Natürlich implementiert Object die Schnittstelle Cloneable nicht selbst, denn sonst hätten ja Klassen schon automatisch diesen Typ, was sinnlos wäre.
clone() gibt eine Referenz auf das neue Objekt zurück, und wenn es keinen freien Speicher mehr gibt, folgt ein OutOfMemoryError.
Nehmen wir an, für ein Spiel sollen Player geklont werden:
package com.tutego.insel.object;
public class Player implements Cloneable {
public String name;
public int age;
@Override
public Player clone() {
try {
return (Player) super.clone();
}
catch ( CloneNotSupportedException e ) {
// Kann eigentlich nicht passieren, da Cloneable
throw new InternalError();
}
}
}
Da es kovariante Rückgabetypen gibt, gibt clone() nicht lediglich Object, sondern den Untertyp Player zurück (siehe Abbildung 10.2). Testen wir die Klasse etwa so:
Player susi = new Player();
susi.age = 29;
susi.name = "Susi";
Player dolly = susi.clone();
System.out.println( dolly.name + " ist " + dolly.age ); // Susi ist 29
[»] Hinweis
Erben wir von einer Klasse mit implementierter clone()-Methode, die ihrerseits mit super. clone() arbeitet, bekommen wir von oben gleich auch die eigenen Zustände kopiert.
clone() und equals(…)
Die Methode clone() und die Methode equals(…) hängen, wie auch equals(…) und hashCode(), zusammen. Wenn die clone()-Methode überschrieben wird, sollte auch equals(…) angepasst werden, denn ohne ein überschriebenes equals(…) bleibt Folgendes in Object stehen:
public boolean equals( Object obj ) {
return this == obj;
}
Das bedeutet aber, dass ein geklontes Objekt – das ja im Allgemeinen ein neues Objekt ist – durch seine neue Objektidentität nicht mehr equals(…)-gleich zu seinem Erzeuger ist. Formal heißt das: o.clone().equals(o) == false. Diese Semantik dürfte nicht erwünscht sein.
Flach oder tief?
Das clone() vom System erzeugt standardmäßig eine flache Kopie (engl. shallow copy). Bei untergeordneten Objekten werden nur die Referenzen kopiert, und das Originalobjekt sowie die Kopie verweisen anschließend auf dieselben untergeordneten Objekte (sie verwenden diese gemeinsam). Wenn zum Beispiel die Bedienung ein Attribut für einen Arbeitgeber besitzt und eine Kopie der Bedienung erzeugt wird, wird der Klon auf den gleichen Arbeitgeber zeigen. Bei einem Arbeitgeber mag das noch stimmig sein, aber bei Datenstrukturen ist mitunter eine tiefe Kopie (engl. deep copy) erwünscht. Bei dieser Variante werden rekursiv alle Unterobjekte ebenfalls geklont. Die Bibliotheksimplementierung hinter Object kann das nicht.
Keine Klone bitte!
Wenn wir weder flach noch tief kopieren wollen, aber aus der Oberklasse eine clone()-Implementierung erben, ist folgende Lösung denkbar, um das Klonen zu unterbinden: Wir überschreiben clone(), lösen aber eine CloneNotSupportedException aus und signalisieren so, dass wir nicht geklont werden wollen. Allerdings gibt es ein Problem, wenn eine Klasse schon die clone()-Methode überschreibt und dabei die Signatur verändert. In Object sieht der Methodenkopf so aus:
public class Object {
...
protected native Object clone() throws CloneNotSupportedException;
...
}
Eine Unterklasse überschreibt clone() und lässt in der Regel das throws CloneNotSupportedException weg. Bei Point2D (von der Point die clone()-Methode erbt) ist Folgendes abzulesen:
public abstract class Point2D implements Cloneable {
...
public Object clone()
...
}
public class Point extends Point2D implements java.io.Serializable {
...
}
Erbt eine Klasse eine clone()-Methode, von der throws CloneNotSupportedException entfernt wurde, so kann sie diese nicht mehr wieder einführen – Unterklassen können throws-Klauseln weglassen, aber nicht hinzufügen. Folgendes ist daher nicht möglich:
public class PointSubclass extends java.awt.Point {
@Override // aus Point2D
public Object clone() throws CloneNotSupportedException // Compilerfehler!
...
}
Da die Signatur keine Exception-Klausel mehr aufnehmen kann, müssen wir einen Trick nutzen und die CloneNotSupportedException in eine Laufzeitausnahme verpacken:
public class ColoredPoint extends java.awt.Point {
public int rgb;
@Override // aus Point2D
public Object clone() {
throw new RuntimeException( new CloneNotSupportedException() );
}
}
Ein Klonversuch führt zu etwas wie:
Exception in thread "main" java.lang.RuntimeException:
java.lang.CloneNotSupportedException
at com.tutego.insel.object.ColoredPoint.clone(ColoredPoint.java:10)
at ...
Caused by: java.lang.CloneNotSupportedException
... 2 more
Technisch löst das Ummanteln der CloneNotSupportedException in eine RuntimeException unser Problem, allerdings sollten wir uns bewusst sein, dass wir ein Verhalten, das vorher erlaubt war, nun »abschalten«. Unterklassen sollten Verhalten nicht wegnehmen.
10.1.5 Hashwerte über hashCode() liefern *
Die Methode hashCode() soll zu jedem Objekt eine möglichst eindeutige Integer-Zahl (sowohl positiv als auch negativ) liefern, die das Objekt identifiziert. Die Ganzzahl heißt Hashcode oder Hashwert, und hashCode() ist die Implementierung einer Hashfunktion. Nötig sind Hashwerte, wenn die Objekte in speziellen Datenstrukturen untergebracht werden, die nach dem Hashing-Verfahren arbeiten. Datenstrukturen mit Hashing-Algorithmen bieten einen effizienten Zugriff auf ihre Elemente. Die Klasse java.util.HashMap implementiert eine solche Datenstruktur.
class java.lang.Object
int hashCode()
Liefert den Hashwert eines Objekts. Die Basisklasse Object implementiert die Methode nativ.
Spieler mit Hashfunktion
Im folgenden Beispiel soll die Klasse Player die Methode hashCode() aus Object überschreiben. Um die Objekte erfolgreich in einem Assoziativspeicher abzulegen, ist gleichfalls equals(…) nötig, das die Klasse Player ebenfalls implementiert:
package com.tutego.insel.object.hashcode;
public class Player {
String name;
int age;
double weight;
/**
* Returns a hash code value for this {@code Player} object.
*
* @return A hash code value for this object.
*
* @see java.lang.Object#equals(java.lang.Object)
* @see java.util.HashMap
*/
@Override public int hashCode() {
int result = 31 + age;
result = 31 * result + ((name == null) ? 0 : name.hashCode());
long temp = Double.doubleToLongBits( weight );
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
/**
* Determines whether or not two players are equal. Two instances of
* {@code Player} are equal if the values of their {@code name}, {@code age}
* and {@code weight} member fields are the same.
*
* @param that an object to be compared with this {@code Player}
*
* @return {@code true} if the object to be compared is an instance of
* {@code Player} and has the same values; {@code false} otherwise.
*/
@Override
public boolean equals( Object that ) {
if ( this == that )
return true;
if ( that == null )
return false;
if ( getClass() != that.getClass() )
return false;
Player other = (Player) that;
if ( age != other.age )
return false;
if ( Double.compare( weight, other.weight ) != 0 )
return false;
if ( ! Objects.equals( name, other.name ) )
return false;
return true;
}
}
Testen können wir die Klasse etwa mit den folgenden Zeilen:
Player bruceWants = new Player();
bruceWants.name = "Bruce Wants";
bruceWants.age = 32;
bruceWants.weight = 70.3;
Player bruceLii = new Player();
bruceLii.name = "Bruce Lii";
bruceLii.age = 32;
bruceLii.weight = 70.3;;
System.out.println( bruceWants.hashCode() ); // -340931147
System.out.println( bruceLii.hashCode() ); // 301931244
System.out.println( System.identityHashCode( bruceWants ) ); // 1671711
System.out.println( System.identityHashCode( bruceLii ) ); // 11394033
System.out.println( bruceLii.equals( bruceWants ) ); // false
bruceWants.name = "Bruce Lii";
System.out.println( bruceWants.hashCode() ); // 301931244
System.out.println( bruceLii.equals( bruceWants ) ); // true
Die statische Methode System.identityHashCode(…) liefert für ein Objekt den Hashcode, wie ihn die Standardimplementierung von Object liefern würde, wenn wir sie nicht überschrieben hätten.
[»] Hinweis
Da der Hashcode negativ sein kann, sind Ausdrücke wie array[o.hashCode() % array. length()] problematisch. Ist o.hashCode() negativ, ist auch das Ergebnis des Restwerts negativ, und die Folge ist eine ArrayIndexOutOfBoundsException.
Eclipse kann die Methoden hashCode() und equals(…) automatisch generieren, wenn wir im Kontextmenü unter Source • Generate hashCode() and equals() auswählen.
Tiefe oder flache Vergleiche/Hashwerte
Referenziert ein Objekt Unterobjekte (etwa eine Person ein String-Objekt für den Namen – keine primitiven Datentypen), so geben die Methoden equals(…) und hashCode() den Vergleich bzw. die Berechnung des Hashcodes an das referenzierte Unterobjekt weiter (wenn es denn nicht null ist). Ablesen können wir das an folgendem Ausschnitt unserer equals(…)-Methode:
if ( name == null )
if ( ((Player)that).name != null )
return false;
else if ( !name.equals( ((Player)that).name ) )
return false;
Es ist demnach die Aufgabe der String-Klasse (name ist vom Typ String), den Gleichwertigkeitstest vorzunehmen. Das heißt, dass zwei Personen problemlos equals(…)-gleich sein können, auch wenn sie zwei nicht identische, aber equals(…)-gleiche String-Objekte referenzieren.
Auch bei hashCode() ist diese Delegation an das referenzierte Unterobjekt abzulesen:
result = 31 * result + ((name == null) ? 0 : name.hashCode());
Dass eine equals(…)-Methode bzw. hashCode()-Methode einer Klasse den Vergleich bzw. die Hashcode-Berechnung nicht an die Unterobjekte delegiert, sondern selbst umsetzt, ist unüblich.
hashCode()-Methoden der Wrapper-Klassen
Jede Wrapper-Klasse deklariert eine statische hashCode(…)-Methode, mit der sich der Hashwert eines primitiven Elements berechnen lässt. (Genauer geht Abschnitt 10.5, »Wrapper-Klassen und Autoboxing«, darauf ein.) Um den Hashwert eines ganzen Objekts zu errechnen, müssen folglich alle einzelnen Hashwerte berechnet und dann zu einer Ganzzahl verknüpft werden. Schematisch sieht das so aus:
int h1 = WrapperKlasse.hashCode( value1 );
int h2 = WrapperKlasse.hashCode( value2 );
int h3 = WrapperKlasse.hashCode( value3 );
...
Eclipse nutzt zur Verknüpfung der Hashwerte folgendes Muster, das ein guter Ausgangspunkt ist:
int result = h1;
result = 31 * result + h2;
result = 31 * result + h3;
...
Nutzen wir die statischen hashCode(…)-Methoden der Wrapper-Klassen, müssen wir nur noch mit dem Datentyp int arbeiten und brauchen nicht zu wissen, wie etwa aus einem double der Hashwert berechnet wird. Uninteressant ist es aber nicht, daher kurz die Implementierung:
Klasse | Klassenmethode, static int | Implementierung |
---|---|---|
Boolean | hashCode(boolean value) | value ? 1231 : 1237 |
Byte | hashCode(byte value) | (int)value |
Short | hashCode(short value) | (int)value |
Integer | hashCode(int value) | value |
Long | hashCode(long value) | (int)(value ^ (value >>> 32)) |
Float | hashCode(float value) | floatToIntBits(value) |
Double | hashCode(double value) | (int)(doubleToLongBits(value) ^ |
Character | hashCode(char value) | (int)value |
equals(…)- und hashCode()-Berechnung bei (mehrdimensionalen) Arrays
Einen gewissen Sonderfall bei equals(…)/hashCode() nehmen mehrdimensionale Arrays ein. Mehrdimensionale Arrays sind nichts anderes als Arrays von Arrays. Das erste Array für die erste Dimension referenziert jeweils auf Unter-Arrays für die zweite Dimension. Wichtig wird diese Realisierung bei der Frage, wie diese Verweise der ersten Dimension nun bei equals(…) betrachtet werden sollen. Denn hier stellt sich die Frage, ob die Unter-Arrays von zwei zu testenden Arrays nur identisch oder auch gleich sein dürfen. Diese Frage hatten wir schon in Abschnitt 4.6, »Die Klasse Arrays zum Vergleichen, Füllen, Suchen und Sortieren nutzen«, angesprochen.
Enthält unsere Klasse ein Array und soll es in einem equals(…) mitberücksichtigt werden, so sind prinzipiell drei Varianten zum Umgang mit diesem Array möglich. Arrays selbst mit == wie primitive Werte zu vergleichen ist in Ordnung, wenn die Identität der Arrays beim Vergleich gewünscht ist. Während viele Klassen die equals(…)-Methode von Object überschreiben, bieten Array-Objekte keine eigene equals(…)-Methode. Ergebnis eines arrays1.equals (arrays2)-Aufrufs wäre folglich ein Identitätsvergleich. Ein wirklicher inhaltlicher Vergleich ist mit Methoden der Utility-Klasse Arrays möglich. Hier gibt es jedoch zwei Methoden, die infrage kämen:
Arrays.equals(Object[] a, Object[] a2) geht jedes Element von a durch, also bei mehrdimensionalen Arrays jede Referenz auf ein Unter-Array, und testet, ob es identisch mit einem zweiten Array a2 ist.
Wenn also zwei gleiche, aber nicht identische Haupt-Arrays identische Unter-Arrays besitzen, liefert Arrays.equals(…) die Rückgabe true. Sie liefert diese Rückgabe aber nicht, wenn die Unter-Arrays zwar gleich, aber nicht identisch sind.
Spielt die Gleichheit der Unter-Arrays eine Rolle, so ist Arrays.deepEquals(…) die passende Methode, denn sie fragt immer mit equals(…) die Unter-Arrays ab.
Bei der Berechnung des Hashwerts gibt es eine vergleichbare Frage. Die Arrays-Klasse bietet zur Berechnung des Hashwerts eines ganzen Arrays die Methoden Arrays.hashCode(…) und Arrays.deepHashCode(…). Die erste Methode fragt jedes Unterelement über die von Object angebotene Methode hashCode() nach dem Hashwert.
Nehmen wir ein mehrdimensionales Array an. Dann ist das Unterelement ebenfalls ein Array. Arrays.hashCode(…) wird dann, wie erwähnt, nur die hashCode()-Methode auf dem Array-Objekt aufrufen, während Arrays.deepHashCode(…) auch in das Unter-Array hinabsteigt und so lange Arrays.deepHashCode(…) auf allen Unter-Arrays aufruft, bis ein equals(…)-Vergleich auf einem Nicht-Array möglich ist.
Was heißt das nun für unsere equals(…)/hashCode()-Methode? Üblich ist der Einsatz von Arrays.equals(…) und nicht von Arrays.deepEquals(…), genauso wie Arrays.hashCode(…) üblicher als Arrays.deepHashCode(…) ist.
Das folgende Beispiel zeigt das in der Anwendung. Die Methoden wurden von Eclipse generiert und etwas kompakter geschrieben:
char[][] chessboard = new char[8][8];
@Override public int hashCode() {
return 31 + Arrays.hashCode( chessboard );
}
@Override public boolean equals( Object obj ) {
if ( this == obj )
return true;
if ( obj == null )
return false;
if ( getClass() != obj.getClass() )
return false;
if ( ! Arrays.equals( chessboard, ((Chess) obj).chessboard ) )
return false;
return true;
}
Hashwert einer Fließkommazahl
Abhängig von den Datentypen sehen die Berechnungen immer etwas unterschiedlich aus. Während Ganzzahlen direkt in einen Ganzzahlausdruck für den Hashwert eingebracht werden können, sind im Fall von double die statischen Konvertierungsmethoden Double.doubleToLongBits(double) und Float.floatToIntBits(float) im Einsatz.
Die Datentypen double und float haben eine weitere Spezialität, da Double.NaN und Float.NaN und das Vorzeichen der 0 zu beachten sind, wie Kapitel 21, »Bits und Bytes, Mathematisches und Geld«, näher ausführt. Fazit: Sind x = +0.0 und y = –0.0, gilt x == y, aber Double.doubleToLongBits(x) != Double.doubleToLongBits(y). Sind x = y = Double.NaN, gilt x != y, aber Double.doubleToLongBits(x) == Double.doubleToLongBits(y). Wollen wir die beiden Nullen nicht unterschiedlich behandeln, sondern als gleich werten, ist Folgendes ein übliches Idiom:
x == 0.0 ? 0L : Double.doubleToLongBits( x )
Double.doubleToLongBits(0.0) liefert die Rückgabe »0«, aber der Aufruf Double.doubleToLongBits(-0.0) gibt »–9.223.372.036.854.775.808« zurück.
Equals, die Null und das Hashen
Inhaltlich gleichwertige Objekte (gemäß der Methode equals(…)) müssen denselben Hash-Wert bekommen.
Die beiden Methoden hashCode() und equals(…) hängen zusammen, sodass in der Regel bei der Implementierung einer Methode auch eine Implementierung der anderen notwendig wird. Denn es gilt, dass bei Gleichwertigkeit natürlich auch die Hashwerte übereinstimmen müssen. Formal gesehen heißt das:
x.equals( y ) => x.hashCode() == y.hashCode()
So berechnet sich der Hashwert bei Point-Objekten aus den Koordinaten. Zwei Punkt-Objekte, die inhaltlich gleich sind, haben die gleichen Koordinaten und damit auch den gleichen Hashwert.
Wenn Objekte den gleichen Hashwert aufweisen, aber nicht gleich sind, handelt es sich um eine Kollision und den Fall, dass in der Gleichung nicht die Äquivalenz gilt. Anders ausgedrückt: Es ist falsch, davon auszugehen, dass, wenn der Hashwert von zwei Objekten gleich ist, auch die Objekte gleichwertig sind. Die Wahrscheinlichkeit kann hoch sein, aber zwingend muss es nicht so sein.
10.1.6 System.identityHashCode(…) und das Problem der nicht eindeutigen Objektverweise *
Die Gleichwertigkeit von Objekten wird mit der Methode equals(…) neu definiert. Wenn equals(…) neu implementiert wird, dann gilt das in der Regel auch für die Methode hashCode(), die ebenfalls überschrieben werden soll. So wird hashCode() bei unterschiedlichen Objektzuständen unterschiedliche Werte zurückgeben, und gleiche Objektinhalte müssen den gleichen Hashwert liefern.
Die Standardimplementierung von Object sieht nun so aus, dass auch bei Objekten, die gleiche Werte annehmen, unterschiedliche Hashwerte herauskommen – das ist auch ein Grund dafür, warum wir hashCode() überschreiben sollten. Doch was liefert denn hashCode() von Object eigentlich? Es sieht so aus, als ob dies eine Objekt-ID wäre, die das Objekt eindeutig kennzeichnet. Die Ur-ID geht verloren, wenn hashCode() neu implementiert wird. Doch interessiert der ursprüngliche hashCode()-Wert, so bietet sich System.identityHashCode(…) an.
[»] Hinweis
Obwohl die Hashwerte zu zwei equals(…)-gleichen Objekten gleich sind, liefert identityHashCode() in der Regel unterschiedliche Werte:
Point p = new Point( 0, 0 );
Point q = new Point( 0, 0 );
System.out.println( System.identityHashCode(p) ); // z. B. 16032330
System.out.println( System.identityHashCode(q) ); // z. B. 13288040
System.out.println( p.hashCode() ); // 0
System.out.println( q.hashCode() ); // 0
Wenn hashCode() nicht überschrieben wird, dann stimmt der Hashwert mit dem identityHashCode(…) überein.
[zB] Beispiel
Einige Klassen überschreiben hashCode() nicht, sodass identityHashCode(…) gleich dem Hashwert ist. Dazu zählt etwa die Klasse StringBuilder:
StringBuilder sb1 = new StringBuilder(), sb2 = new StringBuilder();
System.out.printf( "%d %d%n", System.identityHashCode(sb1), sb1.hashCode() );
// zum Beispiel 1829164700 1829164700
System.out.printf( "%d %d%n", System.identityHashCode(sb2), sb2.hashCode() );
// zum Beispiel 460141958 460141958
Diese statische Methode identityHashCode(…) liefert den Original-Identifizierer der Objekte. Auf den ersten Blick sieht sie nach einer eindeutigen ID aus, das stimmt aber nicht immer. Es kann problemlos zwei unterschiedliche Objekte im Speicher geben, für die System.identityHashCode(…) gleich ist.
10.1.7 Aufräumen mit finalize() *
Wenn die automatische Speicherbereinigung feststellt, dass es keine Referenz mehr auf ein bestimmtes Objekt gibt, so ruft sie automatisch die besondere Methode finalize() auf diesem Objekt auf. Danach kann die automatische Speicherbereinigung das Objekt entfernen. Wir können diese Methode für eigene Aufräumarbeiten überschreiben. Die dadurch neu entstandene Methode wird Finalizer genannt. (Ein Finalizer hat übrigens nichts mit dem finally-Block einer Exception-Behandlung zu tun.)
Seit Java 9 ist finalize() deprecated[ 195 ](http://bugs.openjdk.java.net/browse/JDK-8165641), um Entwickler zu ermutigen, auf diese Methode zu verzichten, obwohl sie natürlich weiterhin von der JVM aufgerufen wird. Es gibt mit finalize() mehrere Probleme: Einige Entwickler verstehen die Arbeitsweise der Methode nicht richtig und denken, sie wird immer aufgerufen. Doch hat die virtuelle Maschine Fantastillionen Megabyte an Speicher zur Verfügung und wird dann beendet, gibt sie den Heap-Speicher als Ganzes dem Betriebssystem zurück. In so einem Fall gibt es keinen Grund für eine automatische Speicherbereinigung als Grabträger und folglich keinen Aufruf von finalize(). Und wann genau der Garbage-Collector in Aktion tritt, ist auch nicht vorhersehbar, sodass im Gegensatz zu C++ in Java keine Aussage über den Zeitpunkt möglich ist, zu dem das Laufzeitsystem finalize() aufruft – alles ist vollständig nichtdeterministisch und von der Implementierung der automatischen Speicherbereinigung abhängig. Üblicherweise werden Objekte mit finalize() von einem Extra-Garbage-Collector behandelt, und der arbeitet langsamer als der normale GC, was somit ein Nachteil ist.
[»] Sprachvergleich
Einen Destruktor, der wie in C++ am Ende eines Gültigkeitsbereichs einer Variablen aufgerufen wird, gibt es in Java nicht.
class java.lang.Object
@Deprecated(since="9") protected void finalize() throws Throwable
Die Methode wird von der automatischen Speicherbereinigung aufgerufen, wenn es auf dieses Objekt keinen Verweis mehr gibt. Die Methode ist geschützt, weil sie von uns nicht aufgerufen wird. Auch wenn wir die Methode überschreiben, sollten wir die Sichtbarkeit nicht erhöhen, also nicht auf public setzen.
Einmal Finalizer, vielleicht mehrmals die automatische Speicherbereinigung
Objekte von Klassen, die eine finalize()-Methode besitzen, kann Oracles JVM nicht so schnell erzeugen und entfernen wie Klassen ohne finalize(). Das liegt auch daran, dass die automatische Speicherbereinigung vielleicht mehrmals laufen muss, um das Objekt zu löschen. Es gilt zwar, dass der Garbage-Collector aus dem Grund finalize() aufruft, weil das Objekt nicht mehr benötigt wird, es kann aber sein, dass die finalize()-Methode die this-Referenz nach außen gibt, sodass das Objekt wegen einer bestehenden Referenz nicht gelöscht werden kann und so von den Toten zurückgeholt wird. Das Objekt wird zwar irgendwann entfernt, aber der Finalizer läuft nur einmal und nicht immer pro GC-Versuch.[ 196 ](Einige Hintergründe erfährt der Leser unter http://www.iecc.com/gclist/GC-lang.html#Finalization. )
Löst eine Anweisung in finalize() eine Ausnahme aus, so wird diese ignoriert. Das bedeutet aber, dass die Finalisierung des Objekts stehen bleibt. Die automatische Speicherbereinigung beeinflusst das in ihrer Arbeit aber nicht.
super.finalize()
Überschreiben wir in einer Unterklasse finalize(), dann müssen wir auch gewährleisten, dass die Methode finalize() der Oberklasse aufgerufen wird. So besitzt zum Beispiel die Klasse Font ein finalize(), das durch eine eigene Implementierung nicht verschwinden darf. Wir müssen daher in unserer Implementierung super.finalize() aufrufen. (Es wäre gut, wenn der Compiler das wie beim Konstruktoraufruf immer automatisch machen würde.) Leere finalize()-Methoden ergeben im Allgemeinen keinen Sinn, es sei denn, das finalize() der Oberklasse soll explizit übergangen werden:
@Deprecated @Override
protected void finalize() throws Throwable {
try {
// ...
}
finally {
super.finalize();
}
}
Der Block vom finally wird immer ausgeführt, auch wenn es im oberen Teil eine Ausnahme gab. Die Methode von Hand aufzurufen ist ebenfalls keine gute Idee, denn das kann zu Problemen führen, wenn der GC-Thread die Methode auch gerade aufruft. Um das Aufrufen von außen einzuschränken, sollte die Sichtbarkeit von protected bleiben und nicht erhöht werden.
[»] Hinweis
Da beim Programmende vielleicht nicht alle finalize()-Methoden abgearbeitet wurden, haben die Entwickler schon früh einen Methodenaufruf System.runFinalizersOnExit(true); vorgesehen. Mittlerweile ist diese Methode veraltet und sollte auf keinen Fall mehr aufgerufen werden, denn in zukünftigen Versionen wird sie entfernt.
Gültige Alternativen
Gedacht war die überschriebene Methode finalize(), um wichtige Ressourcen zur Not freizugeben, etwa File-Handles via close() oder Grafikkontexte des Betriebssystems, wenn ein Programmierer das vergessen hat. Alle diese Freigaben müssten eigentlich vom Entwickler angestoßen werden, und finalize() ist nur ein Helfer, der rettend eingreifen kann. Doch da die automatische Speicherbereinigung finalize() nur dann aufruft, wenn sie tote Objekte freigeben möchte, dürfen wir uns nicht auf die Ausführung verlassen. Gehen zum Beispiel die File-Handles aus, wird der Garbage-Collector nicht aktiv; es erfolgen keine finalize()-Aufrufe, und nicht mehr erreichbare, aber noch nicht weggeräumte Objekte belegen weiter die knappen File-Handles. Es muss also ein Mechanismus her, der korrekt ist und immer funktioniert. Hier gibt es zwei Ansätze:
try mit Ressourcen ruft automatisch die close()-Methode auf und gibt so Ressourcen frei. Der Nachteil ist, dass dann die Freigabe der Ressource an dem Aufruf von close() hängt. Fehlt das Schließen, erfolgt immer noch keine Freigabe.
Die Klasse java.lang.ref.Cleaner hilft beim Aufräumen. Dazu folgt mehr in Abschnitt 10.2, »Schwache Referenzen und Cleaner«.
10.1.8 Synchronisation *
Threads können miteinander kommunizieren und dabei Daten teilen. Sie können außerdem auf das Eintreten bestimmter Bedingungen warten, zum Beispiel auf neue Eingabedaten. Die Klasse Object deklariert insgesamt fünf Versionen der Methoden wait(…), notify() und notifyAll() zur Beendigungssynchronisation von Threads.