Avatar von valenterry
  • valenterry

mehr als 1000 Beiträge seit 14.11.2006

Konzeptionell falsch

Der Artikel beschreibt eine konzeptionell grundlegend falsche Umsetzung und dann eine Verschlimmbesserung dieser Umsetzung.

Das Problem beginnt bereits damit, dass jedes Objekt mit jedem anderen Objekt verglichen werden können soll, d.h. das java.lang.Object eine equals Methode hat. Das ist leider kaum mehr zu ändern und führt zu diversen Problemen.

Dabei ist es gar nicht nötig. Warum sollte man Äpfel mit Autos vergleichen wollen? Oder "VisitorProxy" mit "CustomerDTO". Das gibt keinen Sinn und sollte vom Compiler komplett unterbunden werden - zur Compilezeit.

Wenn Object aber kein equals hat, wie sollte das Thema des Vergleichens dann gelöst werden?

Dafür sollte man sich zunächst klarmachen, dass oft zwei verschiedene Dinge miteinander verwechselt werden: nennen wir sie Äquivalenz und Gleichheit.

Äquivalenz ist stets kontextspezifisch.So können 1€ und 100 Cent (in der Tasche) im Kontext der Bezahlung einer Restaurant-Rechnungsposition äquivalent sein. Auch bei der Frage, welches von mehreren Unternehmen die größten finanziellen Mittel hat, spielt es keine Rolle, ob in Cent oder Euro gemessen wird. Möchte man aber Geld auf's Konto einzahlen, wird die Bank möglicherweise eine zu große Anzahl an Münzen ablehnen. Auch bei der Restaurant-Rechnung kann es einen Unterschied machen, ob dort 1€ oder 100 Cent stehen - zwar nicht bei der Frage, ob der Kunde noch genug Geld hat, die Rechnung zu bezahlen, aber hinsichtlich der ästhetischen Darstellung - sonst muss man erstmal im Kopf umrechnen.
Gleiches gilt auch für die technischere Ebene: zwei ArrayListen mit den gleichen enthaltenen Elementen mögen äquivalent sein, wenn man das erste Element abfragt und damit etwas tun möchte. Sie sind aber z.B. hinsichtlich ihrer Performance möglicherweise nicht mehr äquivalent, wenn man ein neues Element hinzufügt (nämlich wenn die eine ArrayList intern ihre Struktur erweitert, um wieder mehr Platz für zukünftige Elemente zu haben, die anderen ArrayList es aber noch nicht tun muss). Auch aus Sicht eines internen Umsortierungs-Algorithmus sind die beiden ArrayListen unterschiedlich.

Gleichheit hingegen bedeutet, dass sich beim Umgang mit zwei Objekten in keinem Fall ein Unterschied beobachten lässt. Und nicht nur das: es darf sich auch zukünftig niemals ein Unterschied beobachten lassen. Wenn zwei Objekte gleich sind, sind sie in jedem Kontext äquivalent.

Javas equals Methode beschreibt also Gleichheit _solange sie nicht überschrieben wird_. Wird sie überschrieben, dann erhält man einen wilden, unzuverlässigen und unsicheren Mix. Probleme wie ein unpassendes hashcode kommen dazu. Fazit: equals niemals überschreiben und nur für die Prüfung der Objektidentität nutzen (was man allerdings eigentlich niemals brauchen sollte...).

Oftmals möchte man aber Äquivalenzrelationen definieren. Leider macht eine Sprache wie Java das nicht besonders leicht, aber hier ein beispielhafter Ansatz (in halbem Pseudocode):

interface Equivalence<Typ> { Bool isEquiv(a: Typ, b: Typ) } //Money in our Pocket. Can be something like 42€ and 140 Cent final class Money(euro: Int, cent: Int) { Int value() { 100*euro + cent } //mentally add getter here... } Equivalence<Money> moneyValueEquivalence = new Equivalence<Money> { override Bool isEquiv(a: Money, b: Money) { a.value() == b.value() } } Equivalence<Money> moneyValueRoundedEquivalence = new Equivalence<Money> //abgerundet override Bool isEquiv(a: Money, b: Money) { floor(a.value()) == floor(b.value()) } } Equivalence<Money> moneyFormatEquivalence = new Equivalence<Money> override Bool isEquiv(a: Money, b: Money) { a.euro == b.euro && a.cent == b.cent } }

Nehmen wir an, zu einem guten Zweck spendet jemand jeden Tag die Cent-Beträge seiner Verdienste an einen Verein. Sein Kollege macht das gleiche. Die beiden wollen nun vergleichen, an wievielen Tagen sie im letzten Monat gleichviel verdient haben - einmal vor Abzug der Spenden und einmal dahingehend, wieviel sie am Ende wirklich in der Tasche hatten.

int countSames<Typ>(as: List<Typ>, bs: List<Typ>, equiv: Equivalence<Typ>) { int numberOfFoundEquiv = 0; for(int i=0; i < as.length; i++ { if( equiv.isEquiv(as.get(i), bs.get(i)) ) numberOfFoundEquiv++; } return numberOfFoundEquiv } List<Money> tagesverdiensteVonA = ... List<Money> tagesverdiensteVonB = ... int gleicheTagesverdiensteNachSpenden = countSames<Money>(verdiensteVonA, verdiensteVonB, moneyValueRoundedEquivalence) int gleicheTagesverdiensteVorSpenden = countSames<Money>(verdiensteVonA, verdiensteVonB, moneyValueEquivalence)

Man verzeihe mir diverse Hässlichkeiten im Code, aber ich hoffe das Prinzip wird klar.
Besonders hervorzuheben ist die Flexibilität und die Abstraktionsmöglichkeiten, die sich durch diesen Ansatz ergeben. Die Funktion countSames ist komplett generisch und lässt sich nun in verschiedenen Kontexten aufrufen. Man muss ihr nur sagen, was man unter äquivalent versteht. Adapter-schreiben bei 3rd Party-Klassen und umständliches hin- und her Konvertieren fällt weg.
In Java leider nur mit recht viel Overhead umsetzbar, was den Einsatz manchmal fraglich werden lässt, aber trotzdem ein sehr flexible Lösung, die noch weit mehr kann als hier gezeigt.

Weitere Details lassen sich mit den Stichworten Equivalence, Equality, Typeclass ergoogeln.

Das Posting wurde vom Benutzer editiert (19.05.2017 21:18).

Bewerten
- +
Anzeige