|
|||||||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||||||
|
Implementing the equals() Method - Part 1
|
||||||||||||||||||||||||||||||||
VorbemerkungMit diesem Artikel beginnen wir unter dem Titel "Effective Java" eine Kolumne, die sich mit der Programmiersprache Java auseinandersetzen wird. Wir haben dabei bewusst den Titel "Effective Java" gewählt, um an die Tradition von Scott Meyers anzuknüpfen, der den Begriff "Effective..." durch seine "Effective C++"-Bücher populär gemacht hat. Betrachtungen unter dem Motto "Effective" wenden sich typischerweise der Tücke des Objekts zu und so werden wir uns in dieser Kolumne den mehr oder weniger offensichtlichen Fallstricken der Programmiersprache Java widmen.
Nun werden unter dem Begriff "Java" unzählige Aspekte subsummiert,
von Realtime-Programmierung unter speziellen virtuellen Maschinen über
GUI-Programmierung bis hin zur Applikationsentwicklung auf Basis von EJB
und JSP. Es wäre vermessen, über "Java" in dieser Gesamtheit
schreiben zu wollen. Wir beschränken uns daher bewusst auf den
Kern von Java: die Programmiersprache selbst, wesentliche Aspekte
der virtuellen Maschine und einige grundlegende APIs aus den Bibliotheken
der Java 2 Standard Edition (J2SE). Unser Ziel ist es, über
genau den Teil von Java zu schreiben, der jeden Java-Programmierer angeht,
ganz egal in welcher Domain er oder sie programmiert. Dabei wollen
wir uns die weniger offensichtlichen und bisweilen überraschenden
Effekte in Java ansehen.
Objekt-Infrastruktur in Java
Alle Klassen in Java sind implizit von der Klasse Object abgeleitet und erben daher alle Methoden aus Object. Zu diesen geerbten Methoden gehören die public Methoden equals() und hashCode(). equals() vergleicht zwei Objekte miteinander, während hashCode() einen integralen Wert (den sogenannten Hashcode) berechnet. Mit den Details dieser Methoden werden wir uns in diesem und den folgenden Artikeln noch eingehend beschäftigen. An dieser Stelle nur soviel: beide Methoden werden u.a. gebraucht, um Java-Objekte in hash-basierten Containern wie beispielsweise HashSet ablegen zu können. Wegen der automatischen Ableitung von der Superklasse Object sind equals() und hashCode() Teil der Schnittstelle einer jeder Java-Klasse, d.h. man kann auf allen Objekten in Java equals() und hashCode()aufrufen. Es gibt auch immer eine Implementierung dieser Methoden, nämlich entweder die aus Object geerbte Default-Implementierung oder eine klassenspezifische Implementierung, wenn die betreffende Klasse die geerbte Methode überschrieben hat. Methoden wie equals() und hashCode() stellen Basisfunktionalität zur Verfügung, die man von allen Objekten in Java erwartet. Die Menge der Basisfunktionalitäten bezeichnet man bisweilen als "Infrastruktur" eines Objekts. Zur Infrastruktur gehören nicht nur equals() und hashCode(), sondern auch Funktionalität für Initialisierung und Aufräumen von Objekten sowie für Kopieren und Vergleichen von Objekten. Initialisierung geschieht üblicherweise mittels Konstruktoren, Aufräumen mittels finalize()-Methode, Kopieren mittels clone()-Methode, Vergleichen mittels equals() und compareTo()-Methode. Die Liste erhebt keinen Anspruch auf Vollständigkeit. Zur Infrastruktur gehören in gewissem Sinne auch die Methoden für die Serialisierung von Objekten, nämlich readObject() und writeObject(), weil sie ebenfalls so etwas wie Konstruieren und Kopieren von Objekten definieren. Die von einer Klasse geforderte Infrastruktur kann also variieren abhängig vom Kontext, in dem die Klasse verwendet werden soll. Wie wir bereits gesehen haben, werden equals() und hashCode() von der Superklasse Object geerbt. Beides sind public Methoden in Object, d.h. equals() und hashCode() gehören immer zur Schnittstelle einer Klasse. Das ist anders bei clone() und finalize(). Diese beiden Methoden sind ebenfalls in der Superklasse Object definiert, aber sie sind dort als protected deklariert. Damit werden sie zwar geerbt, sind aber nicht automatisch Bestandteil der Schnittstelle der Subklasse. Nur wenn die Subklasse Funktionalität für das Kopieren oder Aufräumen unterstützen will, dann wird sie diese geerbten Methoden aus Object überschreiben und als eigene public Methoden zur Verfügung stellen. (Im Falle von clone() kommt noch hinzu, dass die Subklasse zusätzlich das Cloneable-Interface implementieren muss, damit die clone()-Methode funktioniert.) Andere Teile der Infrastruktur haben gar nichts mit der Superklasse Object zu tun, sondern man implementiert gewisse Interfaces, um die entsprechende Infrastruktur zur Verfügung zu stellen. In diese Kategorie fallen die Methoden compareTo() aus dem Comparable-Interface und readObject() und writeObject(), aus dem Serializable-Interface. Diese Teile der Infrastruktur wird eine Klasse nur dann zur Verfügung stellen, wenn das sinnvoll erscheint, was allerdings häufig der Fall ist: wenn Objekte in baum-basierten Containern wie TreeSet abgelegt werden sollen, dann macht es sehr viel Sinn, dass die Klasse eine compareTo()-Methode bekommt. Analog, wenn Objekte serialisiert werden sollen, dann müssen readObject() und writeObject() implementiert werden. Damit haben wir nun eine Liste von Basisfunktionalität, die jede Java-Klasse zur Verfügung stellen kann. Beim Design einer neuen Klasse muss entschieden werden, welche Teile der Infrastruktur unterstützt werden sollen. Gewisse Methoden, nämlich equals() und hashCode(), können gar nicht vermieden werden. Wenn eine Klasse diese Methoden nicht überschreibt, dann steht automatisch die Default-Funktionalität aus der Superklasse Object zur Verfügung. Für diese Methoden ist die entscheidende Frage nicht "Unterstützen? Ja oder Nein?", sondern man muss entscheiden: "Ist das Default-Verhalten korrekt? Ja oder Nein?". Die Entscheidungen, die der Autor einer Klasse an dieser Stelle trifft, haben weitreichende Auswirkungen für die Benutzung und Benutzbarkeit der Klasse. Das gilt ganz besonders, wenn die Klasse eine potentielle Superklasse ist, und jede Klasse in Java, die nicht als final erklärt ist, ist eine potentielle Superklasse. In dieser und den nachfolgenden Ausgaben der Kolumne wollen wir uns einige Teile dieser Infrastruktur näher ansehen. Dabei wird sich herausstellen, dass korrekte Implementierungen der Infrastruktur keineswegs immer trivial sind. Was theoretisch so harmlos aussieht, kann in der Praxis tückisch sein. Landläufig herrscht die Meinung: "Es ist doch kein Problem, clone() oder equals() zu implementieren. Da muss man doch nur alle Felder kopieren bzw. miteinander vergleichen und das war's dann schon. Oder nicht?" Oder doch? Wir werden sehen!
Schauen wir uns diesmal den Objektvergleich mittels equals() an.
Objektvergleich in Java
Beispiel:
int x = 100;
Hier werden zwei int-Variablen miteinander verglichen. Für den Vergleich gibt es nur den == Operator, weil der Typ int keine equals()-Methode hat. Generell unterscheidet man in Java zwischen Variablen vom primitivem Typ und Referenz-Variablen. Primitive Typen sind in der Sprache vordefinierte Typen wie int, long, boolean, etc.. Für Variablen vom primitivem Typ gibt es nur den Vergleich über den == Operator und der liefert true, wenn beide Variablen den gleichen Wert enthalten, wie das in obigem Beispiel der Fall ist. Nicht-primitiven Typen sind Klassen und Interfaces. Alle Variablen diesen Typs sind in Java Referenzvariablen. Sie verweisen lediglich auf Objekte, enthalten diese Objekte aber nicht. Beispiel:
String s1 = new String("Hello World !");
Hier werden zwei String-Variablen verglichen. String ist eine Klasse
und deshalb sind die beiden Variablen s1 und s2 Referenzvariablen. Für
Referenzvariablen gibt es neben dem Vergleich per == Operator den Vergleich
mit Hilfe der equals()-Methode. Die beiden Vergleiche haben nicht
nur unterschiedliche Syntax, sondern auch unterschiedliche Semantik.
Damit haben wir nun ein erstes intuitives Verständnis von equals(): es prüft auf inhaltliche Gleichheit im Gegensatz zum == Operator, der auf Identität prüft (equality vs. identity). Leider ist es nicht immer so, dass equals() und der == Operator diese unterschiedlichen Eigenschaften haben. Man findet schon in den Java-Bibliotheksklassen Beispiele für abweichendes Verhalten. Beispiel: String init = "Hello World !";
StringBuffer sb1 = new StringBuffer(init);
Offenbar sind StringBuffer-Objekte selbst bei gleichem Inhalt nicht gleich; jedenfalls ist dies das Ergebnis des Vergleichs mittels equals(). Wie kann das sein? Nun, das liegt daran, dass jede Klasse die equals()-Methode von der Superklasse Object erbt. Eine Klasse wie StringBuffer, die die geerbte equals()-Methode nicht überschreibt, stellt damit automatisch die Default-Implementierung von equals()aus Object zur Verfügung. Die Default-Implementierung ist aber identisch mit dem Verhalten des == Operators: es wird auf Identität der referenzierten Objekte geprüft. Dieses Defaultverhalten von equals()aus Object erklärt sich dadurch, dass in der Klasse Object über die Struktur und den Inhalt von Subklassen nichts bekannt ist. Eine universelle Implementierung von equals(), die für jede beliebige Subklasse "das Richtige" tut, nämlich den Inhalt vergleichen, wäre zwar machbar gewesen (mit Hilfe von dynamischer Typinformation), aber aufwendig. Die Designer der Klasse Object haben sich für eine einfachere Lösung entschieden und deshalb wird in Object.equals() nur auf Identität und nicht auf inhaltliche Gleichheit geprüft. Dieses Default-Verhalten von Object.equals() und die Tatsache, dass die Klasse StringBuffer die geerbte equals()-Methode nicht überschreibt, erklären, warum in obigem Beispiel in beiden Vergleichen false als Ergebnis geliefert wird: die StringBuffer-Objekte haben zwar gleichen Inhalt, sind aber nicht identisch.
Ob das Ergebnis des Vergleichs von StringBuffer-Objekten mittels equals()
das ist, was man erwartet, kann man sicher kontrovers diskutieren.
Zumindest wirft es Fragen auf ... wann muss eine Klasse die Default-Implementierung
von equals() überschreiben, und wann nicht? Und wenn ja, wie?
Damit wollen wir uns im Folgenden beschäftigen.
Value vs. Entity-TypesTypen lassen sich in zwei Kategorien einteilen: man unterscheidet zwischen sogenannten Value- und Entity-Typen.
Was bedeutet die Unterscheidung zwischen Value- und Entity-Typen für die Implementierung von equals()? Entity-Typen überschreiben selten die equals()-Methode. Da sie keine Werte darstellen, ist der Vergleich des Inhalts praktisch bedeutungslos und aus diesem Grunde ist es völlig in Ordnung, wenn zwei Entity-Objekte genau dann "gleich" sind, wenn sie identisch sind. Das ist bei Value-Typen ganz anders. Der Inhalt ist das Wesentliche des Objekte und deshalb sind zwei Value-Objekte genau dann gleich, wenn sie den gleichen Inhalt haben. In solchen Fällen muss equals() überschrieben werden, denn die Default-Implementierung ist unbrauchbar für solche Value-Typen.
Was schließen wir daraus? Eine der ersten Entscheidungen,
die beim Design einer neuen Klasse gefällt werden muss, ist
die Entscheidung, ob die Klasse Value- oder Entity-Objekte beschreiben
soll. Im Falle von Entity-Verhalten kann man sich die Arbeit mit
equals() sparen; im Falle von Value-Verhalten muss man es implementieren.
In der PraxisWie ist das nun in der Praxis?"Habe ich was falsch gemacht, wenn ich eine Klasse ohne equals() geschrieben habe?" Das kommt darauf an. Wenn es ein Entity-Typ ist, also eine reine Service-Klasse ist oder einen Verweis auf irgendwas darstellt, dann nicht. Wenn es aber ein Value-Typ ist, dann ist die geerbte equals()-Methode normalerweise inkorrekt. "Aber ich weiß genau, dass equals() überhaupt nicht aufgerufen, nirgendwo in der gesamten Applikation. Wozu soll ich mir all die ganze Arbeit machen, wenn das sowieso keiner braucht?" Das ist eine überzeugendes Argument! Aber ... wer kann schon mit Bestimmtheit sagen, dass eine Methode, die heute nicht gebraucht wird, morgen ebenfalls nicht gebraucht werden wird? Das Gefährliche an equals() ist, dass es immer definiert ist, weil es bereits in der Superklasse Object implementiert ist. Wenn morgen jemand MyClass.equals() ruft, dann lässt sich das klaglos übersetzen und es läuft ... aber leider falsch. Die dann einsetzende Fehlersuche erinnert fatal an die Suche nach Pointer-Problemen in C oder C++ - und das glaubte man doch in Java hinter sich gelassen zu haben. Sobald man sich halbwegs darüber klar geworden ist, dass man mit seiner Klasse einen Value-Typen implementiert, dann sollte man auf jeden Fall equals() korrekt implementieren. Alles andere ist fahrlässig. Erschwerend kommt hinzu, dass equals() nicht immer sichtbar benutzt wird, sondern bereits implizit von gewissen JDK-Klassen verwendet wird. Der wichtigste Vertreter dieser equals()-benutzenden JDK-Klassen sind die hash-basierten Container wie Hashtable, HashMap und HashSet. Aber auch andere Klassen benutzen equals(). Häufig ist dies nicht einmal explizit in der JavaDoc ausgewiesen; eine korrekte equals()-Implementierung wird deshalb von jeder Klasse erwartet. Man kann also gar nicht mit Gewissheit sagen, dass equals() nicht gebraucht wird, weil es nicht benutzt wird.
Das heißt, der Autor einer Klasse muss in jedem Fall entscheiden,
welche Semantik (Entity- oder Value-Typ) die Klasse haben soll. Daraus
ergibt
sich dann die Semantik für die equals()-Methode der neuen Klasse.
Anders als bei anderen Teilen der Objekt-Infrastruktur kann man sich bei
equals() um die Entscheidung nicht drücken. Wenn man sich nicht
entscheidet, ist die Klasse mit ihrer geerbten Default-Implementierung
von equals() u.U. inkorrekt.
Der sogenannte equals()-ContractWenn man nun equals() implementieren will, was muss man tun? Was wird von equals() erwartet? Intuitiv ist klar, dass es den Inhalt zweier Objekte vergleichen soll. Aber was bedeutet das genau?Der Vergleich zweier Objekte sollte gewissen Regeln folgen, die man mehr oder weniger intuitiv von einem Vergleich erwartet. Diese zusätzlichen Eigenschaften einer Implementierung von equals() sind formal beschrieben im sogenannten "equals()-Contract". Den equals()-Contract findet man in der JDK JavaDoc unter Object.equals(). Hier ist die Originalbeschreibung aus der API Spezifikation der JavaTM 2 Platform, Standard Edition: public boolean equals(Object obj) Indicates whether some other object is "equal to" this one.
Anleitung zum Implementieren von equals()Im Folgenden werden wir equals() Zeile für Zeile implementieren.SignaturEine Implementierung von equals() beginnt damit, dass man sich die Signatur (d.h. Anzahl und Type der Argumente) der equals()-Methode überlegen muss. Üblicherweise will man die Version von equals(), die in der Klasse Object definiert ist, überschreiben. Aus diesem Grunde ist es klar, dass die equals()-Methode der eigenen Klasse exakt dieselbe Signatur haben muss, wie Object.equals(), nämlichpublic boolean equals(Object other)
Es gibt alternative Ansätze, bei denen Überschreiben und Überladen
kombiniert wird, aber diese Technik ist ungewöhnlich und wir wollen
sie deshalb zunächst nicht betrachten.
Alias-PrüfungMan kann die Implementierung von equals() sofort beenden, wenn die beiden zu vergleichenden Objekte identisch sind. In diesem Falle müssen sie den gleichen Inhalt haben und man kann sich den gesamten Vergleich des Inhalts sparen. Aus Optimierungsgründen kann man daher als erstes auf Identität von this und other prüfen.
public boolean equals(Object other) {
Aufgabenteilung in einer KlassenhierarchieDie nächsten Schritte der Implementierung sind abhängig davon, ob die Klasse, deren equals()-Methode wir implementieren wollen, eine direkte Subklasse von Object ist, oder ob es sich um eine in der Klassenhierarchie weiter unten liegende Subklasse handelt. Wir unterscheiden daher im Folgenden zwischen direkten Subklassen von Object und indirekten Subklassen von Object. (Eigentlich muss man den Begriff "direkte Subklasse von Object" noch etwas präziser fassen. Als "direkte Subklasse von Object" betrachten wir hier die erste Subklasse, die equals() implementiert. Es kann also durchaus vorkommen, dass Object eine Subklasse hat, die aber kein equals() implementiert, beispielsweise weil sie keine Felder hat. Wenn es eine Sub-Subklasse gibt, die equals() implementiert, dann ist diese Sub-Subklasse die "direkte Subklasse von Object" im Sinne unserer Definition.) Die Unterscheidung zwischen direkten und indirekten Subklassen ist bedeutsam, weil gewisse Aufgaben in einer Klassenhierarchie nur einmal erledigt werden müssen. Und diese Aufgaben werden von direkten Subklassen übernommen.Generell ist es so, dass eine Subklasse den Vergleich ihrer Felder durchführt und den Vergleich der von den Superklassen geerbten Felder an ihre direkte Superklasse delegiert. Ähnlich wie bei Konstruktoren delegiert dabei jede Klasse an ihre direkte Superklasse, so dass rekursiv die equals()-Methoden der gesamten Hierarchie aufgerufen werden und damit das gesamte Objekt verglichen wird. Die Rekursion endet bei der Klasse in der Hierarchie, die direkt von Object abgeleitet ist. Sie ruft die equals()-Methode ihrer Superklasse, nämlich Object.equals(), nicht auf. Statt dessen übernimmt sie gewisse Sonderaufgaben.
Dazu gehört die Prüfung auf Vergleichbarkeit, d.h. die Prüfung,
ob der Inhalt von this überhaupt mit dem Inhalt von other verglichen
werden kann.
[1]
Der Vergleich ist beispielsweise
nicht möglich, wenn other eine null-Referenz ist, also keinen Inhalt
hat. Der Vergleich ist auch dann nicht möglich, wenn other auf ein
Objekt verweist, dass von einem gänzlich inkompatiblen Typ ist.
Schließlich lassen sich "Äpfel und Birnen" nicht miteinander
vergleichen. Hingegen ist der Vergleich immer dann möglich,
wenn this und other von genau dem gleichen Typ sind.
Indirekte Subklassen von ObjectSehen wir uns zunächst die Klassen an, die nicht direkt von Object abgeleitet werden. In der equals()-Implementierung solcher Klassen wird nach dem bereits gezeigten Alias-Check an die Superklasse delegiert und super.equals() gerufen.
boolean equals(Object other) {
Direkte Subklassen von ObjectIn einer Klasse, die direkt von Object abgeleitet ist, wird nicht super.equals() aufgerufen. Statt dessen wird der Fall einer null-Referenz behandelt und auf Vergleichbarkeit geprüft.Der Aufruf von super.equals() ist nicht nur überflüssig, sondern wäre ernsthaft falsch; man sollte ihn also nicht etwa versehentlich machen. super.equals() ist im Falle einer direkten Subklasse von Object genau Object.equals(). Die Implementierung von Object.equals()liefert aber auch false, wenn this und other zwar denselben Inhalt haben, aber als Duplikate im Speicher an verschiedenen Stellen angelegt sind, also nicht identisch sind. Die Information, die Object.equals()liefert ist daher unbrauchbar für die Implementierung der equals()-Methode einer Subklasse. Zu den Sonderaufgaben einer direkten Subklasse von Object: Test auf nullNach dem Alias-Check wird geprüft, ob other eine null-Referenz ist.
public boolean equals(Object other) {
Diesen Test kann man im Prinzip auch in jeder indirekten Subklasse machen, aber es genügt, ihn genau einmal in der obersten Klasse durchzuführen. Wenn man sich an das Koch-Rezept hält, d.h. wenn jede Subklasse nach dem Alias-Check als erstes an die Superklasse delegiert und die oberste Klasse als erstes auf null abprüft, dann erfolgt die Prüfung garantiert, bevor irgendwelche Zugriffe auf Felder von other erfolgen. Damit ist sichergestellt, dass es nicht zu einer NullPointerException kommt, denn diese würde den equals()-Contract verletzen. Der equals()-Contract verlangt, dass der Vergleich mit null-Referenzen das Ergebnis false liefert; das Werfen einer NullPointerException ist daher kein konformes Verhalten. Ganz allgemein sollte man es vermeiden, equals() mit einer NullPointerException zu beenden. Als Ergebnis von equals() wird true oder false erwartet. Wenn der Vergleich aus irgendwelchen Gründen nicht gemacht werden kann, dann sollte keine Exception geworfen werden, sonders es sollte false als Ergebnis geliefert werden. Test auf VergleichbarkeitNachdem getestet ist, dass other keine null-Referenz ist, wird auf Vergleichbarkeit geprüft. Interessanterweise ist das das kontroversesten Themen im Zusammenhang mit equals() überhaupt. Wir werden den Test auf Vergleichbarkeit in der nächsten Kolumne noch näher beleuchten. Hier nur ein erster Einblick in die Problematik.Der Vergleichbarkeitstest ist nötig, weil equals() ein Argument vom Typ "Referenz auf Object" akzeptiert. Eine solche Referenz kann daher auf jede Art von Objekt zeigen und es ist keineswegs sicher gestellt, dass other auf ein Objekt desselben Typs wie this zeigt oder dass die referenzierten Objekte wenigstens in irgendeiner Form vergleichbar sind. Die Vergleichbarkeit muss daher durch einen expliziten Test feststellen. Das heißt, man muss sich vor der Implementierung von equals(), genau genommen schon beim Design der Klasse, überlegen, mit welcher Art von Objekten ein Vergleich überhaupt möglich und sinnvoll ist. Im einfachsten Fall ist der Vergleich nur zwischen Objekten gleichen Typs erlaubt. Das sieht dann wie folgt aus:
public boolean equals(Object other) {
Daneben gibt es zahlreiche Techniken, bei denen versucht wird, den Vergleich
zwischen Sub- und Superobjekten zu erlauben. Immerhin haben Sub-
und Superobjekte einen gemeinsamen Superklassenanteil, den man miteinander
vergleichen kann. Solche Implementierungen sind aber meistens inkorrekt,
weil sie nicht transitiv und oft nicht einmal symmetrisch sind, und damit
nicht den Anforderungen aus dem equals()-Contract entsprechen. Diese
häufig fragwürdigen Implementierungen sind leider so populär,
dass wir ihnen die nächste Ausgabe der Kolumne widmen werden.
Vergleich der FelderNach dem Delegieren an die Superklasse bzw. den Tests auf null und auf Vergleichbarkeit folgt in direkten wie indirekten Subklassen der eigentliche Vergleich der Felder.Im Prinzip müssen alle Felder von this mit den korrespondierenden Feldern von other verglichen werden. Wie das im einzelnen geschieht, hängt vom Typ der Felder ab. Man unterscheidet zwischen
Transiente FelderTransiente Felder werden ignoriert. Sie tragen nichts zum Zustand des Objekts bei und gehören logisch nicht zum Inhalt des Objekts. Daher werden sie beim Vergleich nicht berücksichtigt.Primitive TypenFelder von primitivem Typ werden mit Hilfe des == Operators verglichen. Beispiel:
class MyClass {
Bei primitiven Typen vergleicht der == Operator den Inhalt und das ist genau das, was wir hier brauchen. Referenz-TypenFelder, die Referenzen sind, werden verglichen, indem die equals()-Methoden der referenzierten Objekte gerufen werden. Beispiel:
class MyClass {
Mögliche null-ReferenzenObige Lösung ist natürlich nur korrekt, wenn von der Logik der Klasse her sichergestellt ist, dass das betreffende Feld keine null-Referenz sein kann. Wenn null ein möglicher Wert ist, dann muss zuvor auf null abgeprüft werden, um eine NullPointerException zu verhindern. Beispiel:
class MyClass {
Typen ohne korrektes equals()Der Aufruf der equals()-Methode des referenzierten Objekts macht nur Sinn, wenn diese equals()-Methode korrekt, d.h. konform zu den Regeln des equals()-Contracts, implementiert ist. Andernfalls muss man Sonderlösungen und Umgehungen finden.Ein Beispiel für einen solchen Typ, der eine Sonderbehandlung braucht, ist StringBuffer. Man kann StringBuffer-Objekte zwar per StringBuffer.equals() miteinander vergleichen, aber es wird auf Identität und nicht auf inhaltliche Gleichheit geprüft. Das ist nicht das, was hier gebraucht wird; wir wollen den Inhalt der StringBuffer-Objekte vergleichen und brauchen daher eine Umgehungslösung, die man mit Hilfe der Klasse String bauen kann. Für einen korrekten Vergleich des Inhalts von zwei StringBuffer-Objekten kann man beide Objekte in String-Objekte konvertieren (per toString()-Methode) und dann die String-Objekte per String.equals() miteinander vergleichten Ein anderes Beispiel für Felder, die eine Sonderbehandlung brauchen, sind Arrays. Zum Vergleich von zwei Arrays muss der Inhalt der Arrays elementweise verglichen werden, indem man für jedes Array-Element die equals()-Methode aufruft (oder bei Elementen von primitivem Typ den == Operator). Zur Arbeitserleichterung gibt bereits eine Hilfsklasse, nämlich java.util.Arrays. Diese Klasse hat eine statische Methode equals(), die genau das oben beschriebene tut.
Am Ende, wenn alle Prüfungen und Vergleiche erfolgreich durchgeführt
worden sind, wird true zurückgegeben.
RollenspieleKlassen ohne korrekte Implementierung von equals() machen ihren Benutzern reichlich Probleme. Die Probleme sind insbesondere dann gravierend, wenn es sich bei der inkorrekten Klasse um eine Superklasse handelt. Die Subklasse hat dann kaum noch eine Chance, ihrerseits eine korrekte Implementierung von equals() zur Verfügung zu stellen, weil Zugriff auf die privaten Daten der Superklasse gar nicht möglich ist und u.U. die Kette von rekursiven Delegationen an super.equals() unterbrochen ist. Es ist daher bei der Implementierung von potentiellen Superklassen, d.h. von Klassen, die nicht als final deklariert sind, besonders wichtig, dass sie equals() korrekt implementieren und nach Möglichkeit dem vorgeschlagenen Rezept folgend ihre Sonderaufgaben übernehmen. [2]
Hier nochmals in der Übersicht die Verantwortlichkeiten in einer
Klassenhierarchie, sowie Beispielimplementierungen im Source-Code:
Implementierung von equals() in einer direkten Subklasse von Object
if (!(s.equals(((MyClass)other).s)))
Implementierung von equals() in einer indirekten Subklasse von Objectclass MySubclass extends MyClass {private String t; ... public boolean equals(Object other) { if (this == other) return true; if (!super.equals(other)) return false;
if (!(t.equals(((MySubClass)other).t)))
Haben wir damit den equals()-Contract erfüllt? Sehen wir uns die 5 Anforderungen noch einmal an.
Das ist erfüllt, weil wir als erstes den Alias-Check ausführen. Wir vergleichen nur Objekte gleichen Typs miteinander; deshalb ist unsere Implementierung von equals() symmetrisch. Verletzungen dieser Forderung können auftreten, wenn der Vergleich zwischen Objekten unterschiedlichen Typs erlaubt wird. Dann findet man bisweilen Implementierungen, die asymmetrisch sind, weil A.equals() den Vergleich mit Objekten des Typs B erlaubt, aber B.equals() den Vergleich mit A-Objekten nicht zulässt. Dazu mehr im nächsten Artikel. Wir vergleichen nur Objekte gleichen Typs miteinander; deshalb ist unsere Implementierung von equals() transitiv. Verletzungen dieser Forderung können auftreten, wenn der Vergleich zwischen Objekten unterschiedlichen Typs erlaubt wird. Dann findet man bisweilen Implementierungen, die intransitiv sind, weil der Vergleich zwischen einem A-Objekt und einem B-Objekt true liefern kann und genauso der Vergleich zwischen dem B-Objekt und einem anderen A-Objekt; aber das heißt noch nicht, dass deshalb die beiden A-Objekte gleich sind, obwohl das laut equals()-Contract so sein sollte. Dazu mehr im nächsten Artikel. In unserer Implementierung von equals() werden die Felder miteinander verglichen. Es geht keine weitere Information in die Produktion des Booleschen Ergebnisses ein. Deshalb ist das Ergebnis immer dasselbe, sofern sich die Objekte nicht ändern. Fehler können nur auftreten, wenn beispielsweise statische Daten in die Ermittlung des Ergebnisses eingehen würden, was aber ganz ungewöhnlich wäre. Das haben wir durch die Prüfung auf null in der direkten Basisklasse von Object erreicht und dadurch, dass wir NullPointerExceptions sorgfältig vermieden haben. Zusammenfassung und AusblickJede Klasse in Java muss ein Minimum an Objekt-Infrastruktur implementieren, damit sie sinnvoll verwendbar ist. Zu diesen grundlegenden Methoden gehört u.a. auch die equals()-Methode. Dabei ist man nicht frei in der Wahl der Semantik, die man der equals()-Methode gibt. Jede Implementierung von equals()sollte die Regeln des sogenannten equals()-Contracts befolgen. Dafür ist es nötig, dass Klassen, die Value-Typen repräsentieren, die Defaultimplementierung aus Object.equals() überschreiben, weil diese auf Identität und nicht auf inhaltliche Gleichheit prüft, was für Value-Typen semantisch falsch ist. Wir haben das Prinzip solcher Implementierungen diskutiert, wobei eine Reihe von Details offen geblieben sind. Insbesondere ist offen, welche Arten von Objekten als "miteinander vergleichbar "gelten sollen. Wir haben nur den Vergleich von Objekten gleichen Typs erlaubt. Dazu gibt es aber Alternativen, die wir im nächsten Artikel diskutieren werden.Fußnoten[1] Dieser Vergleich ist nicht zu verwechseln mit dem Alias-Check, bei dem geprüft wird, ob this und other identisch sind.
[2] Andere Vorgehensweisen sind denkbar, aber
in jedem Falle sollten alle Klassen in einem Projekt oder zumindest in
einer Klassenhierarchie demselben Konzept folgen. Alternative Implementierungen
von equals() und deren Vor- und Nachteile besprechen wir im nächsten
Artikel.
NachtragViele Jahre später - dieser Zusatz wurde im Juli 2009 geschrieben, also 7 Jahre nach der Veröffentlichung des obigen Artikels - wird das Thema equals() noch immer diskutiert. Mittlerweile hat sich augenscheinlich herumgesprochen, dass die Implementierung von equals() vielleicht doch nicht ganz so simpel ist, wie sie in Joshua Bloch's "Effective Java" auf den ersten Blick erscheinen mag. Diesmal wird das Thema aus dem Blickwinkel von Scala aufgerollt, rückübersetzt nach Java. Dem interessierten Leser sei daher folgender Beitrag als Ergänzung empfohlen: "How to Write an Equality Method in Java" von Martin Odersky, Lex Spoon und Bill Venners vom 1. Juni 2009 (/ OSV /) .Literaturverweise
|
|||||||||||||||||||||||||||||||||
© Copyright 1995-2012 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/01.Equals-Part1/01.Equals1.html> last update: 1 Nov 2012 |