Logo Wissenstransfer Gerhard at CichnaDotCom

>> Wissensdatenbank / Objektorientiertes Programmieren

Arbeiten mit Objekten

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.
Methoden der Klasse Object

String-Darstellung von Objekten

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:
Überschreiben der Methode toString()

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:
Impliziter Aufruf der toString()-Methode

Vergleich mit ==

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:
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.
Darstellung komplexer Datentypen im Hauptspeicher
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).
Objektvergleich mit ==

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.

Vergleichen mit equals()

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:
Equals-Methode überschreiben

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:
Equals-Methode überschreiben für komplexe Datentypen

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:

Vergleichen per hashCode()

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.
HashCode-Methode überschreiben

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).

compareTo()

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.
Das Interface Comparable
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.
Implementierung eines Interfaces Comparable

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.

Interface Comparable mit Generic

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:
Interface Comparable mit Wrapper-compareTo()

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.
compareTo()-Methode mit lexikographischem Vergleich

Klonen von Objekten

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.
Implementierung clone()-Methode

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.