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. aus
    public class Klasse {} wird
    public class Klasse extends Object {}
  • Object enthält einige Methoden, die es an alle anderen Klassen vererbt
    • equals()
    • 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
Wenn man equals() überschreibt, so muss man auch hashCode() überschreiben und umgekehrt

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
Wenn man equals() überschreibt, so muss man auch hashCode() überschreiben und umgekehrt

Beispiel: equals() und hashCode()

gegebene Klasse
public class Mitarbeiter {
    public String name;
    public double gehalt;
    public Date geburtsdatum;
}
equals() Methode zur Klasse Mitarbeiter
@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;
    }
}
hashCode() Methode zur Klasse Mitarbeiter
@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 Klasse String
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();
    }
}
Verwendung von 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 von null
  • hash(Object... vals)
    Berechnet einen Hashcode für mehrere Werte
  • hashCode(Object o)
    Berechnet den Hashcode für ein Objekt, das null sein könnte
  • requireNonNull(Object o, ...)
    Prüft, ob ein Objekt null ist und wirft eine Ausnahme
  • requireNonNullElseGet(Object o, Object def)
    Prüft, ob das Objekt null ist und gibt das Default-Objekt def zurück
  • checkIndex(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.


Copyright © 2025 Thomas Smits