Object
Die Klasse Object
- Die Klasse
Object
ist die Wurzel aller Vererbungshierarchien - Jede Java-Klasse erbt direkt oder indirekt von
Object
- Wenn eine Klasse von keiner anderen erbt, erzeugt der Compiler automatisch ein
extends Object
, d. h. auspublic class Klasse {}
wirdpublic class Klasse extends Object {}
Object
enthält einige Methoden, die es an alle anderen Klassen vererbtequals()
hashCode()
toString()
clone()
Die anderen Methoden, die von der Klasse Object
zur Verfügung gestellt werden, werden wir später noch im Rahmen anderer Themen wie Threads oder Reflection diskutieren.
hashCode()
hashCode()
dient dazu, einen möglichst eindeutigen Hash-Wert für das Objekt zu erzeugen- Die Implementierung von
Object
verwendet einen „einmaligen“ Schlüssel für den Hash-Wert, unabhängig von den Daten im Objekt - Man überschreibt
hashCode()
, um einen Hash-Wert aus den Daten des Objekts zu berechnen
Möglichst eindeutig bedeutet, dass es vorkommen kann, dass zwei Objekte mit unterschiedlichem Inhalt denselben Hash-Wert haben. Dies ist nicht zu vermeiden, da der Hash-Wert nur ein einfacher 32bit int
Wert ist und daher nicht immer eindeutig sein kann. Man spricht in dem Fall, dass zwei unterschiedliche Objekte denselben Hash-Wert haben von einer Hash-Kollision (hash collision) oder einer Kollision der Hash-Werte. Solche Kollisionen führen nicht zu Fehlverhalten des Programms, sind aber trotzdem aus Gründen der Effizienz zu minimieren. Um sie zu vermeiden, muss man versuchen, die erzeugten Hash-Wert möglichst breit über den verfügbaren Wertebereich eines 32bit Integers zu streuen und dafür zu sorgen, dass ähnliche Daten nicht unbedingt ähnliche Hash-Werte bekommen.
Verwendet man die Standard-Implementierung von hashCode()
aus Object
(d. h. wenn man hashCode()
nicht überschrieben hat), so haben zwei unterschiedliche Objekte fast niemals denselben Hash-Code, auch wenn ihr Inhalt vollkommen gleich ist. Zwei unterschiedliche Objekte können auch hier denselben Hashcode haben, falls es zu einer Kollision kommt. Da die ursprüngliche Implementierung von Object
beim Überschreiben der hashCode
-Methode verloren geht, kann man sich zu jedem Objekt später über die Methode System.identityHashCode(Object o)
diesen ursprünglichen Wert beschaffen.
equals() und ==
- Der
==
Operator bestimmt, ob zwei Objekt-Referenzen identisch sind (auf dasselbe Objekt zeigen) equals()
stellt fest, ob zwei Objekte gleich sind
(den gleichen Inhalt haben)- Wenn man
equals()
nicht überschreibt, verwendet die Implementierung aus Object den==
Operator
Beispiel: equals() und hashCode()
public class Mitarbeiter {
public String name;
public double gehalt;
public Date geburtsdatum;
}
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
} else if (o == this) {
return true;
} else if (o.getClass() == Mitarbeiter.class) {
Mitarbeiter mi = (Mitarbeiter) o;
return mi.name.equals(name)
&& mi.gehalt == gehalt
&& mi.geburtsdatum.equals(geburtsdatum);
} else {
return false;
}
}
@Override
public int hashCode() {
return name.hashCode()
^ (int) Double.doubleToLongBits(gehalt)
^ geburtsdatum.hashCode();
}
Die hier vorgestellte Implementierung ist noch in einigen Punkten naiv und daher verbesserungswürdig. Für eine bessere Implementierung fehlt uns an dieser Stelle aber noch einiges an Theorie.
- Keine der Methoden behandelt null-Werte korrekt
- Die Verwendung von XOR (
^
) für die Hash-Werte funktioniert zwar, gewährleistet aber noch keine optimale Streuung der Werte
Die Konstruktion o.getClass() == Mitarbeiter.class
ist uns noch unbekannt und prüft, ob das übergebene Objekt von genau derselben Klasse ist, wie das vorhandene. Hierbei wird das sogenannte Class-Objekt verwendet, das die Klasse eines Objektes selbst wieder als Objekt repräsentiert. Es gibt pro Klasse immer nur ein einziges Class-Objekt, sodass man hier sicher mit ==
vergleichen kann. Anstatt o.getClass() == Mitarbeiter.class
hätte man auch o.getClass() == this.getClass()
schreiben können.
Alternativ könnte man o instanceof Mitarbeiter
schreiben. Der Unterschied wäre dann aber, dass equals()
dann true
liefern könnte, wenn das Objekt mit dem verglichen wird, von einer Subklasse von Mitarbeiter stammt, was häufig nicht das gewünschte Verhalten ist.
Beachten Sie weiterhin, dass die equals-Methode einen Parameter vom Typ Object
erhält. Wenn man dies ändert, z. B. in dem man im Beispiel Mitarbeiter als Typ verwendet, kommt es zu subtilen Fehlern, weil man hierdurch die Methode equals()
nicht überschrieben, sondern überladen hat. Man hat also zwei verschiedene equals-Methoden, eine von Object
geerbt und eine weitere in der Klasse Mitarbeiter hinzugefügt. Der Compiler wird anhand des statischen Typs der Variablen eine der beiden Methoden auswählen, sodass in manchen Fällen die eine und in manchen Fällen die andere Methode benutzt wird. Auch hier könnte man sich durch Verwendung der @Overrides
-Annotation vor diesem Fehler selbst schützen.
Eine bessere Implementierung von hashCode()
(wie sie z. B. von Eclipse
generiert würde) sähe wie folgt aus:
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((geburtsdatum == null) ? 0 : geburtsdatum.hashCode());
long temp;
temp = Double.doubleToLongBits(gehalt);
result = prime * result + (int) (temp ^ (temp >>> 32));
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
Beispiel: equals() und ==
MeinDatum d1 = new MeinDatum(5, 12, 2009);
MeinDatum d2 = new MeinDatum(5, 12, 2009);
MeinDatum d3 = new MeinDatum(3, 7, 1970);
System.out.println(d1 == d2);
System.out.println(d1.equals(d2));
System.out.println(d1.equals(d3));
d2 = d1;
System.out.println(d1 == d2);
false
true
false
true
Achtung: Falle bei String
- Die Java VM optimiert String-Literale und sorgt dafür, dass sie nur ein einziges Mal im Speicher liegen
- Hierzu verwendet sie die Methode
intern()
der KlasseString
String s1 = "Hallo";
String s2 = "Hallo";
String s3 = new String("Hallo");
String s4 = s3.intern();
System.out.println(s1 == s2); // -> true
System.out.println(s1 == s3); // -> false
System.out.println(s1 == s4); // -> true
Die VM führt eine so genannte globale String-Literal-Optimierung durch, um Speicher zu sparen: Immer wenn ein String-Literal verwendet wird, schaut sie zuerst nach, ob dieses schon irgendwo im Speicher liegt und falls dem so ist, gibt sie eine Referenz auf das bereits existierende Objekt zurück, statt ein neues anzulegen. Dies Optimierung ist möglich, da es sich bei String um ein immutable Objekt handelt, d. h. ein Objekt, das nach seiner Erzeugung nicht mehr verändert werden kann. Daher ist es für zwei Verwender nicht relevant, ob sie zwei verschiedene Objekte oder ein und dasselbe Objekt verwenden.
Das Anlegen eines Strings mit new String("LITERAL")
verschwendet Speicher und kostet Performance. Es wurde hier nur gewählt, um die Auswirkungen den String-Literal-Optimierung zu illustrieren.
Weiterhin ist zu beachten, dass durch die hier beschriebene Optimierung Code, der fälschlicherweise ==
statt equals()
verwendet, zufälligerweise funktionieren kann, nämlich genau dann, wenn es sich bei den beiden zu vergleichenden Objekten zufälligerweise um Literale handelt.
Es gibt einige Fälle, wo es sich lohnen kann, Strings explizit mit intern()
zu behandeln und dann mit ==
zu arbeiten. Dies ist immer dann vorteilhaft, wenn man einen String mit einer sehr großen Anzahl von Literalen vergleichen muss. Im Allgemeinen sollte man aber intern()
nur sehr selten verwenden.
Praxistipp: Test auf null vermeiden
Bei Vergleichen von Strings mit String-Literalen kann man sich den Test auf null
sparen, wenn man die Bedingung umdreht
statt
if ((s != null) && (s.equals("text"))) { ... }
schreibt man
if ("text".equals(s)) { ... }
Dieser Trick funktioniert, weil ein String-Literal selbst ein Objekt ist, auf dem man Methoden aufrufen kann und es nie null sein kann. Der Test auf null ist ohnehin bereits in der equals()
-Methode enthalten, sodass man ihn sich hier sparen kann.
toString()
Die toString()
-Methode
- liefert eine Darstellung des Objekts als String zurück
- wird automatisch bei String-Verknüpfungen aufgerufen
- soll primär beim Debuggen helfen
- dient nicht der Serialisierung von Objekten
Standardimplementierung gibt den Typ und den Hash-Code aus
System.out.println(m + " hat ein Gehalt von " + gehalt);
System.out.println(m.toString() +
" hat ein Gehalt von " +
Double.toString(gehalt));
Das folgende Beispiel zeigt, wie einem die toString
-Methode hilfreiche Informationen beim Debuggen eines Programms zur Verfügung stellen kann.
@Override
public String toString() {
return "Mitarbeiter [name: " + name + ", "
+ "gehalt: " + gehalt + ", "
+ "geburtsdatum " + geburtsdatum + "]";
}

clone()
clone()
dient dem Kopieren von Objekten- Erzeugt eine flache Kopie (shallow copy) des Objekts
- Konstruktoren werden nicht aufgerufen
- Anwendung
- Die protected
clone()
-Methode muss überschrieben werden - Klasse muss
Cloneable
implementieren - In der ersten Zeile muss
super.clone()
aufgerufen werden - Tiefe Kopien (deep copy) kann man durch rekursiven Aufruf von
clone()
auf den Instanzvariablen erzeugen
clone()
hat den Vorteil, dass man mit der Methode einfach Kopien von Objekten herstellen kann. Allerdings hat clone()
auch einige Nachteile, die es häufig sinnvoll erscheinen lassen auf diesen Ansatz zu verzichten und lieber mit einem Copy-Konstruktor zu arbeiten.
Das Interface Cloneable (zu Interfaces kommen wir später noch) enthält selbst keine Methoden und dient nur dazu die Zustimmung der Klasse dazu zu signalisieren, gecloned zu werden. Man spricht hier von einem tagging Interface.
Da es sich bei clone()
um eine protected Methode handelt, kann man es nicht zusammen mit Interfaces oder Variablen vom Type einer Superklasse verwenden, die clone()
noch nicht implementiert hat. Auch ein Cast auf Cloneable
funktioniert nicht, da Cloneable
selbst die clone()
-Methode nicht enthält. Dies ist einer der größten Nachteile von clone()
und kann als Designfehler angesehen werden.
Das folgende Beispiel zeigt eine beispielhafte Implementierung und Verwendung von clone()
.
public class Mitarbeiter implements Cloneable {
public String name;
public double gehalt;
public Date geburtsdatum;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
Mitarbeiter m1 = new Mitarbeiter("Heinz", 5000.00, null);
Mitarbeiter m2 = (Mitarbeiter) m1.clone();
System.out.println(m1 == m2); // -> false
System.out.println(m2.name); // -> Heinz
System.out.println(m2.gehalt); // -> 5000.00
Das Werfen der CloneNotSupportedException
in diesem Beispiel sollte eigentlich vermieden werden, da die Klasse durch implementieren des Cloneable Interfaces explizit zugestimmt hat gecloned zu werden und den Verwendern nur unnötig das Fangen einer Exception aufbürdet, die sowieso nicht geworfen werden wird. Da wir aber hier noch keine Ausnahmen eingeführt haben und damit noch kein try/catch kennen wurde aus didaktischen Gründen die Exception in der Signatur der Methode beibehalten. Korrekterweise würde man das Beispiel normalerweise wie folgt programmieren:
public class Mitarbeiter implements Cloneable {
public String name;
public double gehalt;
public Date geburtsdatum;
public Mitarbeiter(String name, double gehalt, Date geburtsdatum) {
this.name = name;
this.gehalt = gehalt;
this.geburtsdatum = geburtsdatum;
}
protected Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError("Cannot happen");
}
}
}
Die Hilfsklasse java.util.Objects
Die Klasse java.util.Objects
in Java bietet einige statische Methoden, die in verschiedenen Anwendungsfällen nützlich sein können.
java.util.Objects
bietet Hilfsfunktionen
equals(Object a, Object b)
prüft auf Gleichheit unter Berücksichtigung vonnull
hash(Object... vals)
Berechnet einen Hashcode für mehrere WertehashCode(Object o)
Berechnet den Hashcode für ein Objekt, dasnull
sein könnte
requireNonNull(Object o, ...)
Prüft, ob ein Objektnull
ist und wirft eine Ausnahme
requireNonNullElseGet(Object o, Object def)
Prüft, ob das Objektnull
ist und gibt das Default-Objektdef
zurückcheckIndex(int index, int length)
,checkFromToIndex(...)
Prüft Indices auf korrekte Grenzen
Die Signaturen der Methoden sind in der wirklichen Klasse anders, als hier angegeben. So sind z. B. die Methoden requireNonNull
etc. generisch mit einem Typ-Argument. Da generische Typen aber hier noch nicht eingeführt sind, ist die Signatur vereinfacht wiedergegeben.