|
|||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||
|
Effective Java - OOP 2002 Conference Proceedings
|
||||||||||||||||||
Effective Java Programming
OOP 2002 Conference Proceedings,
January 2002
VorbemerkungDieses Tutorial unter dem Titel "Effective Java" befasst sich mit der Programmiersprache Java, wobei wir den Titel "Effective Java" bewusst gewählt haben, 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 widmen wir uns in dem "Effective Java"-Tutorial den mehr oder weniger offensichtlichen und bisweilen überraschenden Effekten und "Features" der Programmiersprache Java.Im einzelnen werden im Tutorial die folgenden Themen betrachtet:
Objekt-Infrastruktur in JavaIn objekt-orientierten Programmiersprachen unterstützen alle Objekte ein Minimum an offensichtlich sinnvoller Basisfunktionalität. Dazu gehören scheinbare Trivialitäten wir das "Kopieren von Objekten" und "Vergleichen von Objekten". In Java sind das die Methoden clone(), equals() und einige andere, die zusammen so etwas wie die "Infrastruktur" eines Objekts ausmachen. Was genau meinen wir mit "Infrastruktur"?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. Im Tutorial sehen wir uns einige Teile dieser Infrastruktur näher an. 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 JavaIn Java gibt es zwei Möglichkeiten, Variablen zu vergleichen: die eine ist der Vergleich über den == Operator, die andere Möglichkeit ist der Vergleich mit Hilfe der equals()-Methode.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 !");String s2 = new String("Hello World !");...if (s1 == s2) ... // yields false...if (s1.equals(s2)) ... // yields true
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);StringBuffer sb2 = new StringBuffer(init);...if (sb1 == sb2) ... // yields false...if (sb1.equals(sb2)) ... // yields false (!!!)
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.Value-Typen. Alle primitiven Typen in Java sind Value-Typen. Sie enthalten einen Wert und dieser Wert ist das Wesentliche. Klassen können ebenfalls Value-Typen sein. Bei solchen Klassen ist der Inhalt der Objekte ganz wesentlich. Der Inhalt repräsentiert den Wert des Objekts und bestimmt das Verhalten der Objekte fast vollständig. Beispiele solcher Value-Klassen sind die Standard-Klassen BigDecimal, String, Date, Point, etc.
Entity-Typen.
Darunter versteht man Klassen, bei denen der Inhalt
nicht das Wesentliche ist. Sie werden nicht als "Werte" betrachten
und auch nicht als "Wert" herumgereicht. Das sind Typen, die hauptsächlich
Dienste anbieten, oder Typen, die Referenzen auf andere unterliegende Objekte
darstellen. Beispiele sind die Standardklassen Thread, Socket, oder FileOutputStream.
Betrachten wir zur Illustration ein Thread-Objekt und ein String-Objekt. Ein String-Objekt ist im wesentlichen durch seinen Inhalt, nämlich die enthaltene Zeichenkette, bestimmt. Davon kann man Kopien anlegen und man kann sie vergleichen. Das ist bei einem Thread-Objekt ganz anders. Natürlich hat auch ein Thread -Objekt Inhalt; ein Thread hat einen Namen und einen Zustand (runnable, blocked, dead, usw.) und eine Priorität und er verwendet ein Runnable-Objekt, dessen Code er ausführt. Aber all diese Eigenschaften ergeben in ihrer Gesamtheit keinen "Wert", den man vergleichen oder kopieren möchte. Wann sind zwei Threads gleich? Wenn sie denselben Namen haben? Oder denselben Code ausführen? Das macht logisch keinen Sinn. Was soll man sich unter der Kopie eines Threads vorstellen? Auch das macht nicht so recht Sinn. In solchen Fällen spricht man von Entity-Typen, wobei die Grenze zwischen Value- und Entity-Typen oftmals schwer zu ziehen ist. 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 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()-Contract
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)
The equals method implements an equivalence relation:
Anleitung zum Implementieren von equals()Im Tutorial wird equals() Zeile für Zeile implementiert. An dieser Stelle können wir leider nur eine Übersicht über die Aufgaben und Verantwortlichkeiten in einer Klassenhierarchie geben, sowie Beispielimplementierungen im Source-Code:
Implementierung von equals() in einer direkten Subklasse von Object
class MyClass {private String s;private int i;...public boolean equals(Object other) {if (this == other)return true;if (other == null)return false;if (other.getClass() != getClass())return false;if (!(s.equals(((MyClass)other).s)))return false;if (i != ((MyClass)other).i)return false;...return true;}}
Damit bleiben natürlich zahlreiche Fragen offen. Antworten finden Sie in den Büchern und Zeitschriften der nachfolgenden Referenzliste.class 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)))return false;...return true;}} Weiterführende Literatur
/1/ “The Java Programming Language 3rd Edition” , Ken Arnold and James Gosling, Addison-Wesley, 2000 Das ist das Standardwerk zu Java und erklärt fast alle Sprachmittel. Zu verzwickten Problemen wie den oben angedeuteten wird man nicht viel finden. Exotische Sprachmittel wir etwa Phantom-Referenzen sind ebenfalls nicht besprochen. Aber insgesamt ist es trotzdem Pflichtlektüre für jeden Java-Programmierer. /2/ “Effective Java” , Joshua Bloch, Addison-Wesley, 2001 Ein Buch im Stile von Scott Meyers's "Effective C++"-Büchern. Josh Bloch bespricht verschiedene Fallstricke der Sprache Java. Die vorgeschlagenen Lösungen sind aber bisweilen mit Vorsicht zu genießen (anders als bei Scott Meyers, der wirklich allgemein anerkannte Wahrheiten verkündet hat). Insgesamt aber durchaus lesens- und empfehlenswert. /3/ “Practical Java” , Peter Haggar, Addison-Wesley, 2000
Vorläufer von "Effective Java". Ebenfalls im Stile von Scott
Meyers's "Effective C++"-Büchern geschrieben. Peter Haggar kommt
bisweilen zu ganz anderen Schlüssen und Empfehlungen als Joshua Bloch.
Lesenswerte Zeitschriften in diesem Zusammenhang sind: /4/ "Java Report" Ein US-Magazin, das leider im November 2001 das Erscheinen eingestellt hat. Wer Zugriff auf alte Ausgaben hat, kann sich die Kolumne von Mark Davies ansehen, der interessante Aspekte der Sprache besprochen hat. /5/ "JavaSpektrum"
Ein deutsches Magazin. Themen aus dem Tutorial kann man in unserer
Kolumne "Effective Java" nachlesen.
Informationen über das weiterführende Seminar, dem das Material
des Tutorials entnommen ist, finden Sie unter:
|
|||||||||||||||||||
© Copyright 1995-2011 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/Papers/JavaGotchas/EffectiveJavaProceedings.htm> last update: 8 Jul 2011 |