Was passiert, wenn man die Wertklasse Datum aus Teil 4
mittels Unterklasse DatumMitZeit um die Uhrzeit erweitert?
Main-Klasse
equals.EqualsTest
Anders als Entity-Objekte, die immer nur mit sich selbst gleich sind,
sind Wert-Objekte gleich, wenn ihre Instanzvariablen die gleichen Werte haben.
Um das sicherzustellen, müssen Wertklassen die Instanzmethoden
equals(Object) und hashCode() aus java.lang.Object
überschreiben. Dabei muss equals laut
API-Spezifikation
eine Äquivalenrelation sein, d.h. equals muss
reflexiv, symmetrisch und transitiv sein.
Außerdem muss hashCode laut
API-Spezifikation
für zwei Objekte denselben Hash-Code liefern,
wenn sie laut equals gleich sind.
Die Main-Klasse EqualsTest prüft die verlangten Eigenschaften von
equals und hashCode mit einigen Beispiel-Objekten der Oberklasse
Datum und Unterklasse DatumMitZeit.
Für die Klassen Datum und DatumMitZeit
gibt es vier Implementierungsvarianten, jede in einem eigenen Unterpaket.
In EqualsTest wird per import-Anweisung festgelegt,
welche Implementierungsvariante getestet werden soll.
Paket equals.variante1 mit Oberklasse
Datum
und Unterklasse
DatumMitZeit
Die erste Implementierungsvariante funktioniert gut für Testfälle, die entweder
nur Datum-Objekte oder nur DatumMitZeit-Objekte enthalten.
Das Substitutionsprinzip der Klassenvererbung verlangt nun aber, dass Objekte einer
Oberklasse jederzeit durch Objekte ihrer Unterklassen ersetzt werden können,
d.h. es müssen auch gemischte Testfälle mit Datum- und
DatumMitZeit-Objekten richtig funktionieren.
Beim gemischten Testfall in EqualsTest wird jedoch die Symmetrie
von equals verletzt, weil bei a.equals(b) und b.equals(a)
verschiedene Methoden aufgerufen werden, einmal die aus Datum
(sie liefert true) und einmal die aus DatumMitZeit
(sie liefert false).
Implementierungsvariante 1 ist also wegen Verletzung der Symmetrie falsch.
Paket equals.variante2 mit Oberklasse
Datum
und Unterklasse
DatumMitZeit
Die zweite Implementierungsvariante versucht die erste zu reparieren,
indem die equals-Methode der Unterklasse die equals-Methode
der Oberklasse aufruft. Die Symmetrie ist damit auch im gemischten Testfall gesichert,
dafür verletzt hashCode die Spezifikation und die Transistivität
von equals ist verletzt.
Implementierungsvariante 2 ist also auch falsch.
Paket equals.variante3 mit Oberklasse
Datum
und Unterklasse
DatumMitZeit
equals-Methode der Oberklasse Objekte der Unterklasse prinzipiell
nicht mehr als gleich zu Objekten der Oberklasse betrachtet
(Verwendung von getClass() statt instanceof).
Implementierungsvariante 3 ist im Sinne der API-Spezifikation richtig,
aber weil sie das Substitutionsprinzip der Klassenvererbung verletzt,
ist sie schlecht.
Paket equals.variante4 mit Objektkomposition statt Vererbung mit
Datum,
Zeit
und
DatumMitZeit
Die vierte Implementierungsvariante verhält sich exakt wie die dritte,
verletzt aber das Substitutionsprinzip nicht, weil sie ohne Klassenvererbung auskommt.
Diese Lösung ohne Klassenvererbung wird auch im Paket
java.time
der Klassenbibliothek verwendet (Klassen LocalDate, LocalTime
und LocalDateTime).
Fazit: Klassenvererbung und Wertklassen passen nicht zusammen.
Wertklassen müssen final sein und dürfen abgesehen von
java.lang.Object höchstens abstrakte Oberklassen haben.