Die Klasse Object, von der alle Java-Klassen implizit erben, definiert einige nützliche Methoden. Diese können je
nach Bedarf von eigenen Klassen überschrieben werden.

Die Methode toString() dient der Ausgabe von beliebigen Objektinhalten auf der Konsole, beispielsweise zu Testzwecken.
Mit den bislang bekannten Mitteln musste hierfür stets auf eine komplizierte Verkettung von Strings zurückgegriffen
werden.
Ausgaben auf diese Weise zu programmieren ist in vielerlei Hinsicht nachteilig:
Wird die Methode toString() überschrieben, kann sie zur Erzeugung einer Zeichenkette mit Attributwerten herangezogen werden:

Wird sie nicht überschrieben, gibt sie stets den Namen der Klasse, gefolgt von einem hashCode() des Objektes aus (Beispiel: Kunde@1c8697ce).
Das Überschreiben von toString() hat einen weiteren großen Vorteil: Wenn an einer beliebigen Programmstelle ein String
erwartet wird (z.B. als Parameter oder bei der Verkettung von Strings mit dem "+"-Operator), wird automatisch die
toString()-Methode des Objekts aufgerufen:

Primitive Datentypen enthalten Werte. Je nach Typ können diese Werte Zahlen, Buchstaben oder Wahrheitswerte sein. Solche
Werte miteinander zu vergleichen ist eine der grundlegendsten Aufgaben von Prozessoren und daher aus Sicht des Programmierers
trivial.
Die Darstellung primitiver Datentypen im Hauptspeicher:

Anders verhält es sich mit komplexen Datentypen. Ein solcher Datentyp enthält anstelle eines Wertes eine Referenz auf
das entsprechende Objekt. Daher werden diese Datentypen auch als "Referenztypen" bezeichnet.

Werden solche komplexen Datentypen miteinander verglichen, werden immer die Referenzen verglichen. Das bedeutet, es wird
überprüft, ob die beiden Datentypen auf dasselbe Objekt verweisen. Werden zwei komplexe Datentypen miteinander
verglichen, die zwar die gleichen Attributswerte enthalten, jedoch in unterschiedliche Objekte gespeichert werden, handelt es
sich in Java nicht um das gleiche Objekt (siehe Warenkörbe w2 und w3 in folgender Abbildung).

Als Konsequenz ist also zu beachten, dass der "=="-Operator bei primitiven Datentypen anders funktioniert als bei komplexen. Er kann insbesondere nicht verwendet werden, um inhaltliche Vergleiche durchzuführen.
Für den Vergleich anhand des Inhaltes, also für die Identität relevanter Attributwerte, muss die Methode equals() der Klasse Object überschrieben werden. Sie erwartet als Parameter ein beliebiges Objekt und liefert einen Wahrheitswert zurück, der im Falle der Gleichheit "true" ist, andernfalls "false".
Eine equals()-Methode sollte wie folgt aufgebaut sein:

Der Kunde besitzt nunmehr eine Kundennummer, die beim Vergleich zweier Kunden-Objekte als relevantes Identifikationsmerkmal
herangezogen werden soll. Wenn es sich bei den zu vergleichenden Objekten bereits um die gleichen Referenzen handelt, ist ein
solcher Vergleich natürlich nicht nötig. Daher startet die equals()-Methode mit dem Abgleich
if (this == obj). Für den inhaltlichen Vergleich muss man auf die Attribute des Objekts zugreifen
können. Die Signatur gibt allerdings vor, dass als Parameter irgendein Objekt einer beliebigen Klasse übergeben werden
kann. Daher muss zuvor mittels instanceof-Operator überprüft werden, ob es sich tatsächlich um ein Objekt der Klasse
Kunde handelt. Ist dies sichergestellt, kann das Objekt in ein Kunden-Objekt gecastet werden. Dadurch kann der Vergleich anhand
des Attributes kundennummer durchgefüht werden.
Es kann vorkommen, dass die relevanten Attribute ebenfalls komplexe Datentypen sind und auch mit equals() verglichen werden
müssen. Würde im vorliegenden Beispiel die Kundennummer fehlen, müsste man die Objekte anhand von Vornamen,
Nachnamen und Geburtsdatum unterscheiden:

Den Abschluss der Methode bildet stets die Anweisung return super.equals(obj);.
Handelt es sich nicht um ein Kunden-Objekt, wird die equals()-Methode der Oberklasse aufgerufen mit der Hoffnung, dass irgendeine
allgemeinere Oberklasse die Objekte vergleichen kann. Kann keine Oberklasse die Objekte unterscheiden, so wird in letzter Instanz
die Implementierung der equals()-Methode in der Klasse Object aufgerufen. Sie unterscheidet die Objekte nur noch anhand der
Referenz.
Eine eigene Implementierung der equals()-Methode sollte bestimmten Anforderungen genügen, damit darauf aufbauende Algorithmen nicht fehlerhaft arbeiten:
x.equals(x) == true.x.equals(y)
muss das gleiche Ergebnis liefern wie y.equals(x).x.equals(y) und
x.equals(z), dann auch y.equals(z).x.equals(null) == false.Der "=="-Operator kann Objekte zwar sehr schnell, aber nur anhand der Referenz vergleichen. Die equals()-Methode kann Objekte anhand ihres Inhalts vergleichen, ist dabei aber wegen der komplizierten Attribut-Zugriffe vergleichsweise langsam. Beide Möglichkeiten decken daher eine Reihe von Anwendungsfällen ab, Objekte miteinander zu vergleichen. Doch es gibt auch Situationen, in denen der "=="-Operator einerseits nicht genau genug und die equals()-Methode andererseits zu aufwendig ist. Dies ist zum Beispiel der Fall, wenn überprüft werden soll, ob ein gegebenes Objekt in einer sehr großen Liste enthalten ist.
Für solche Anwendungsfälle kann die hashCode()-Methode der Klasse Object überschrieben werden. Ihre Aufgabe ist es, einen möglichst eindeutigen Schlüssel, einen Hash-Code, für ein Objekt zu erzeugen. Dieser Vorgang nennt sich "Hashing". Anhand des Schlüssels können die Objekte dann unterschieden werden. Weil der Schlüssel eine Ganzzahl ist, kann die Unterscheidung sehr schnell vom Prozessor durchgeführt werden.
In Java gibt es für wichtige Datentypen bereits Implementierungen solcher hashCode()-Funktionen. Die Klasse String zum Beispiel überschreibt die hashCode()-Methode anhand einer Formel, die in der Java-Dokumentation nachzulesen ist. Darin wird jeder Buchstabe mit einer stets unterschiedlich potenzierten Primzahl multipliziert. Die Summe dieser Produkte ergibt den Hash-Code. Primzahlen eignen sich aufgrund ihrer besonderen mathematischen Eigenschaften sehr gut für die Berechnung möglichst eindeutiger Schlüssel. Die Primzahl 31 spielt dabei eine besondere Rolle, da sie nahe an der 2er-Potenz liegt und somit sehr schnell durch einen Prozessor berechnet werden kann (2⁵ - 1).
Solche Implementierungen können für eine eigene Lösung weiterverwendet werden. Wenn zum Beispiel die für
die Identifizierung relevanten Attribute vom Typ String sind, können sie in einen großen String zusammengefasst werden.
Anschließend kann dann die in der Klasse String überschriebene hashCode()-Implementierung verwendet werden.

Wenn primitive Attribute in einer Hash-Methode eingesetzt werden sollen (wie zum Beispiel das Alter des Kunden), kann nicht auf bestehenden Implementierungen zurückgegriffen werden. Dann bietet es sich an, die Zahlen mit einer Primzahl zu verrechnen. Eigene hashCode()-Implementierungen sollten dabei stets die folgenden Anforderungen erfüllen:
| Tipp |
| Manche Entwicklungsumgebungen, wie zum Beispiel Eclipse, können die equals()- und hashCode()-Methoden automatisch generieren. |
Der Einsatz von hashCode()-Methoden spielt eine entscheidende Rolle im Zusammenhang mit speziellen Datenstrukturen, die Hashing bei der Speicherung anwenden (z.B. die HashMap).
Neben der Prüfung auf Gleichheit/Ungleichheit von Objekten durch Überschreiben der equals()-Methode ist oft auch die Ordnung von Objekten interessant. Das heißt, zu einem beliebigen Objekt soll untersucht werden, inwiefern es im Vergleich zu einem gegebenen Objekt größer, kleiner, älter, jünger usw. ist. Bei primitiven Datentypen wie Zahlen und Fließkomma-Werten kann man die Vergleichsoperatoren "<" und ">" verwenden, um eine solche Ordnung feststellen zu können. Leider funktionieren diese Operatoren nicht für komplexe Datentypen.
Um dieses Problem zu lösen, haben die Entwickler von Java das Interface Comparable und dessen Methode
compareTo() vorgesehen.

Es kann von beliebigen Klassen implementiert werden, um eine Ordnung zwischen Objekten der Klasse zu definieren. Dabei kann man
ähnlich wie bei der equals()-Methode vorgehen: Entweder man entwickelt eine eigene Implementierung, oder man greift - soweit
verfügbar - auf bestehende Implementierungen zurück. Die folgende Abbildung zeigt eine beispielhafte Implementierung
für die Klasse "Kunde". Auf Basis der Kundennummer wird eine Ordnung hergestellt: Die Differenz ist negativ, wenn die
übergebene Kundennummer größer ist. Ist hingegen die Kundennummer des aktuellen Objekts größer, ist die
Differenz positiv. Handelt es sich um denselben Kunden, ergibt die Differenz null.
| Tipp |
| Zu beachten ist, dass die Methodensignatur nur eine Ganzzahl zurückliefern kann. Es ist nicht möglich, einen Fehlercode zu definieren und zurückzugeben, falls die zu vergleichenden Objekte nicht vom gleichen Typ sind. Für solche Zwecke sollten allgemein Exceptions eingesetzt werden. Am Beispiel der obigen compareTo()-Methode wird bereits in der ersten Anweisung zur Laufzeit eine ClassCastException erzeugt, falls die Typen nicht zueinander passen. Es liegt in der Verantwortung des aufrufenden Programms, diese Exception abzufangen und zu behandeln. |
| Exkurs: Generics und compareTo() |
Dieses Vorgehen ist allerdings seit Einführung der Generics ab Java-Version 5 überflüssig, da man Klassen
und Interfaces mit Typen parametrisieren kann. Bezogen auf das obige Beispiel bedeutet das, dass man am Interface Comparable
angeben kann, von welchem Typ die zu vergleichenden Objekte sind. Als Folge verändert sich die Signatur der
compareTo()-Methode.
|
Alternativ zu einer eigenen Implementierung kann auch auf bestehende compareTo()-Methoden zurückgegriffen werden. Zum Beispiel
wird das Interface Comparable auch von den sogenannten "Wrapper"-Klassen (diese kapseln primitive Datentypen in Objekte,
sodass sie wie Objekte behandelt werden können, z.B. Integer, Float und Double) implementiert. Da es sich bei der
Kundennummer um eine Ganzzahl handelt, kann daher die compareTo()-Methode der Klasse Integer wiederverwendet werden:

Neben der Sortierung eines Kunden hinsichtlich der Kundennummer kann man sich ebenso gut eine lexikografische Ordnung
anhand des Vor- und Nachnamens vorstellen. Das heißt, die Kunden sollen, ähnlich wie im Telefonbuch oder anderen
Verzeichnissen, alphabetisch nach Namen und dann nach Vornamen sortiert werden. Da es sich dabei aber um mehr als ein
Vergleichsattribut handelt, wird die Methode etwas komplexer. Zusätzlich muss man sich im Zusammenhang mit Zeichenketten
die Frage stellen, ob Groß- und Kleinschreibung bei der Ordnung eine Rolle spielen. Das folgenden Beispiel zeigt einen
lexikografischen Vergleich, bei dem auf die Methode compareToIgnoreCase()-Methode der Klasse String zurückgegriffen wird.

Wenn in Java beim Aufruf einer Methode primitive Datentypen als Parameter verwendet werden, so wird eine Kopie des Wertes übergeben. man spricht daher von "Call-by-Value". Änderungen an den übergebenen Werten haben lediglich lokale Auswirkungen innerhalb der Methode. Wird hingegen ein Objekt erwartet, so wird die Referenz auf das Objekt übergeben, keine Kopie des Objekts (man spricht in diesem Fall von "Call-by-Reference"). Das hat zur Folge, dass das aufrufende Programm und die aufgerufene Methode mit demselben Objekt arbeiten. Die Veränderungen am Objekt innerhalb der Methode wirken sich also auch im aufrufenden Programm aus. Falls das nicht gewünscht ist, sollte ein Klon des ursprünglichen Objekts übergeben werden.
Das Klonen von Objekten ist allerdings mit hohem Aufwand verbunden. Es sollte daher nur wenn unbedingt erforderlich implementiert werden und nicht als grundsätzliche Vorsichtsmaßnahme.
Objekte können auf zwei Arten geklont werden. Die erste Möglichkeit ist die Definition eines sogenannten "Copy-Kontruktors". Die zweite Möglichkeit ist das Überschreiben der Methode clone() der Klasse Object. Wenngleich diese Technik kontrovers diskutiert wird, soll sie der Vollständigkeit halber an dieser Stelle kurz vorgesellt werden.
Beim Überschreiben der clone-Methode sind zwei Sachverhalte zu beachten: Die Methode in der Klasse Object wurde mit dem
Sichtbarkeitsmodifikator protected definiert. Das heißt, die Methode ist nur innerhalb einer Unterklasse sichtbar und es kann
nicht von außen auf sie zugegriffen werden. Das Problem kann schnell gelöst werden, indem man die Methode mit der
Sichtbarkeit public überschreibt. Doch das alleine reicht noch nicht aus, um clone() aufrufen zu können. Denn der
Aufruf von clone() der Klasse Object ist nur dann erfolgreich, wenn die zu klonende Klasse das Interface Cloneable implementiert.
Andernfalls wird von clone() eine CloneNotSupportedException geworfen.

Der Aufruf der abgebildeten Implementierung von clone() sorgt dafür, dass alle Attribute des zu klonenden Objekts in ein neues Objekt kopiert werden und dieses Objekt zurückgegeben wird. Es sei angemerkt, dass die Methode in der Oberklasse Object so implementiert ist, dass sie hierfür erstaunlicherweise nicht explizit über Anzahl und Beschaffenheit der Attribute in der Unterklasse informiert werden muss.
Eine wichtige Fragestellung beim Klonen von Objekten ist der Umgang mit Referenzdatentypen: Werden lediglich deren Referenzen kopiert ("flache Kopie"), oder wird auch von allen referenzierenden Objekten eine Kopie erzeugt ("tiefe Kopie"). Dabei wird von den Referenzdatentypen z.B. durch Aufruf des Copy-Konstruktors ebenfalls eine Kopie erstellt. Zu beachten ist, dass die Implementierung von clone() in der Klasse Object nur eine flache Kopie erzeugt. Das bedeutet, dass - falls erwünscht - eine tiefe Kopie explizit in der überschriebenen Methode programmiert werden muss.