Könnte eine Instanz einer anderen Instanz eines spezifischeren Typs entsprechen?


25

Ich habe diesen Artikel gelesen: So schreiben Sie eine Gleichheitsmethode in Java .

Grundsätzlich bietet es eine Lösung für eine equals () -Methode, die Vererbung unterstützt:

Point2D twoD   = new Point2D(10, 20);
Point3D threeD = new Point3D(10, 20, 50);
twoD.equals(threeD); // true
threeD.equals(twoD); // true

Aber ist es eine gute Idee? Diese beiden Instanzen scheinen gleich zu sein, können jedoch zwei unterschiedliche Hash-Codes haben. Ist das nicht ein bisschen falsch?

Ich glaube, dass dies besser erreicht werden würde, wenn man stattdessen die Operanden wirft.


1
Das im Link angegebene Beispiel mit farbigen Punkten ist für mich sinnvoller. Ich würde annehmen, dass ein 2D-Punkt (x, y) als 3D-Punkt mit einer Null-Z-Komponente (x, y, 0) betrachtet werden kann, und ich möchte, dass die Gleichheit in Ihrem Fall false zurückgibt. Tatsächlich wird in diesem Artikel ausdrücklich gesagt, dass sich ein ColoredPoint von einem Point unterscheidet und immer false zurückgibt.
Coredump

10
Nichts schlimmeres als Tutorials, die gängige Konventionen brechen ... Es dauert Jahre, um diese Gewohnheiten von Programmierern zu brechen.
Corsika

3
@coredump Das Behandeln eines 2D-Punkts als Nullkoordinate zkann für einige Anwendungen eine nützliche Konvention sein. Aber es ist eine willkürliche Konvention. Ebenen in Räumen mit 3 oder mehr Dimensionen können beliebige Ausrichtungen haben ... das macht interessante Probleme interessant.
Ben Rudgers

Antworten:


71

Dies sollte nicht gleich sein, weil es die Transitivität bricht . Betrachten Sie diese beiden Ausdrücke:

new Point3D(10, 20, 50).equals(new Point2D(10, 20)) // true
new Point2D(10, 20).equals(new Point3D(10, 20, 60)) // true

Da Gleichheit transitiv ist, sollte dies bedeuten, dass der folgende Ausdruck auch wahr ist:

new Point3D(10, 20, 50).equals(new Point3D(10, 20, 60))

Aber natürlich nicht.

Ihre Vorstellung vom Casting ist also richtig - erwarten Sie, dass Casting in Java einfach das Casting des Referenztyps bedeutet. Was Sie hier wirklich wollen, ist eine Konvertierungsmethode, die Point2Daus einem Point3DObjekt ein neues Objekt erstellt. Dies würde auch den Ausdruck aussagekräftiger machen:

twoD.equals(threeD.projectXY())

1
Der Artikel beschreibt Implementierungen, die die Transitivität unterbrechen, und bietet eine Reihe von Umgehungsmöglichkeiten. In einem Bereich, in dem wir 2D-Punkte zulassen, haben wir bereits entschieden, dass die dritte Dimension keine Rolle spielt. und so ist (10, 20, 50)gleich (10, 20, 60)gut. Wir kümmern uns nur um 10und 20.
Ben Rudgers

1
Sollte Point2Deine projectXYZ()Methode vorhanden sein, um eine Point3DRepräsentation von sich selbst bereitzustellen ? Mit anderen Worten, sollten sich Implementierungen gegenseitig kennen?
hjk

4
@hjk Das Entfernen Point2Dscheint einfacher zu sein, da für die Projektion von 2D-Punkten zunächst die Ebene im 3D-Raum definiert werden muss. Wenn der 2D-Punkt seine Ebene kennt, ist er bereits ein 3D-Punkt. Wenn nicht, kann es nicht projizieren. Ich erinnere mich an Abbotts Flatland .
Ben Rudgers

@benrudgers Sie können jedoch ein Plane3DObjekt definieren , das eine Ebene im 3D-Raum definiert. liftDiese Ebene kann eine Methode (2D-> 3D wird angehoben, nicht projiziert) haben, die ein Point2Dund eine Zahl für die "dritte Achse" akzeptiert "- Abstand vom Flugzeug entlang der Flächennormalen. Zur Vereinfachung der Verwendung können Sie die gemeinsamen Ebenen als statische Konstanten definieren, sodass Sie beispielsweise Folgendes tun können:Plane3D.XY.lift(new Point2D(10, 20), 50).equals(new Point3D(10, 20, 50))
Idan Arye

@IdanArye Ich habe den Vorschlag kommentiert, dass 2D-Punkte eine Projektionsmethode haben sollten. Für Flugzeuge mit Auftriebsmethoden sind meines Erachtens zwei Argumente erforderlich: ein 2D-Punkt und die Ebene, auf der er sich befinden soll, dh es muss wirklich eine Projektion sein, wenn ihm der Punkt nicht gehört ... und wenn es den Punkt besitzt, warum nicht einfach einen 3D-Punkt besitzen und einen problematischen Datentyp und den Geruch einer verschwommenen Methode beseitigen? YMMV.
Ben Rudgers

10

Ich entferne mich vom Artikel und denke über die Weisheit von Alan J. Perlis nach:

Epigramm 9. Es ist besser, 100 Funktionen für eine Datenstruktur auszuführen, als 10 Funktionen für 10 Datenstrukturen.

Die Tatsache, dass die richtige "Gleichheit" ein Problem ist, das den Erfinder von Scala , Martin Ordersky, nachts wach hält, sollte darüber nachdenken, ob das Überschreiben equalsin einem Vererbungsbaum eine gute Idee ist.

Was passiert, wenn wir Pech haben, eine zu erhalten, ColoredPointist, dass unsere Geometrie fehlschlägt, weil wir die Vererbung verwendet haben, um Datentypen zu vermehren, anstatt einen guten Datentyp zu erstellen. Dies, obwohl Sie zurückgehen und den Stammknoten des Vererbungsbaums ändern müssen, damit er equalsfunktioniert. Warum nicht nur hinzufügen , zund eine colorzu Point?

Der gute Grund wäre, dass Pointund ColoredPointin verschiedenen Domänen zu betreiben ... zumindest, wenn diese Domänen nie vermischt werden. Wenn dies der Fall ist, müssen wir es nicht überschreiben equals. Vergleiche ColoredPointund PointGleichberechtigung sind nur in einem dritten Bereich sinnvoll, in dem sie sich vermischen dürfen. In diesem Fall ist es wahrscheinlich besser, die "Gleichheit" auf diese dritte Domäne zugeschnitten zu haben, als zu versuchen, Gleichheitssemantik von der einen oder der anderen oder beiden nicht vermischten Domänen anzuwenden. Mit anderen Worten, "Gleichheit" sollte lokal an dem Ort definiert werden, an dem Schlamm von beiden Seiten einströmt, weil wir nicht ColoredPoint.equals(pt)gegen Instanzen scheitern wollen , Pointselbst wenn der Verfasser der ColoredPointMeinung war, dass dies vor sechs Monaten um 2 Uhr morgens eine gute Idee war .


6

Als die alten Programmiergötter das objektorientierte Programmieren mit Klassen erfanden, entschieden sie sich, wenn es um Komposition und Vererbung ging, zwei Beziehungen für ein Objekt zu haben: "ist ein" und "hat ein".
Dies löste teilweise das Problem, dass Unterklassen sich von übergeordneten Klassen unterschieden, sie jedoch verwendbar machten, ohne Code zu brechen. Da eine Unterklasseninstanz "ein" Oberklassenobjekt ist und direkt durch ein Objekt ersetzt werden kann, obwohl die Unterklasse mehr Elementfunktionen oder Datenelemente enthält, garantiert "a", dass alle Funktionen des übergeordneten Objekts ausgeführt werden und alle Funktionen vorhanden sind Mitglieder. Man könnte also sagen, ein Point3D ist ein Point, und ein Point2D ist ein Point, wenn beide von Point erben. Zusätzlich kann ein Point3D eine Unterklasse von Point2D sein.

Die Klassengleichheit ist jedoch problemdomänenspezifisch, und das obige Beispiel ist nicht eindeutig, was der Programmierer benötigt, damit das Programm ordnungsgemäß funktioniert. Im Allgemeinen werden Regeln für mathematische Domänen befolgt, und Datenwerte führen zu Gleichheit, wenn Sie den Umfang des Vergleichs auf nur zwei Dimensionen beschränken, aber nicht, wenn Sie alle Datenelemente vergleichen.

So erhalten Sie eine Tabelle mit einschränkenden Gleichungen:

Both objects have same values, limited to subset of shared members

Child classes can be equal to parent classes if parent and childs
data members are the same.

Both objects entire data members are the same.

Objects must have all same values and be similar classes. 

Objects must have all same values and be the same class type. 

Equality is determined by specific logical conditions in the domain.

Only Objects that both point to same instance are equal. 

Sie wählen im Allgemeinen die strengsten Regeln, die Sie können, um dennoch alle erforderlichen Funktionen in Ihrer Problemdomäne auszuführen. Die eingebauten Gleichheitstests für Zahlen sind so restriktiv wie möglich für mathematische Zwecke, aber der Programmierer hat viele Möglichkeiten, um dies zu umgehen, wenn dies nicht das Ziel ist, einschließlich Auf- / Abrunden, Abschneiden, gt, lt usw . Objekte mit Zeitstempeln werden häufig nach ihrer Generierungszeit verglichen. Daher muss jede Instanz eindeutig sein, damit Vergleiche sehr spezifisch werden.

In diesem Fall besteht der Entwurfsfaktor darin, effiziente Methoden zum Vergleichen von Objekten zu bestimmen. Manchmal ist ein rekursiver Vergleich aller Objektdatenelemente erforderlich, und dies kann sehr teuer werden, wenn Sie viele, viele Objekte mit vielen Datenelementen haben. Alternativ können Sie nur relevante Datenwerte vergleichen oder das Objekt einen Hash-Wert der betroffenen Datenelemente generieren lassen, um einen schnellen Vergleich mit anderen ähnlichen Objekten zu ermöglichen. Sie können Sammlungen sortieren und bereinigen, um Vergleiche zu beschleunigen und weniger CPU-intensiv zu machen, und möglicherweise Objekte zulassen, die dies zulassen Daten identisch sind, die gekeult werden sollen, und ein doppelter Zeiger auf ein einzelnes Objekt an seine Stelle gesetzt wird.


2

Die Regel ist, wann immer Sie überschreiben hashcode(), Sie überschreiben equals()und umgekehrt. Ob dies eine gute Idee ist oder nicht, hängt von der beabsichtigten Verwendung ab. Persönlich würde ich mit einer anderen ( isLike()oder ähnlichen) Methode den gleichen Effekt erzielen.


1
Es kann in Ordnung sein, hashCode zu überschreiben, ohne gleich zu setzen. Zum Beispiel würde man das tun, um einen anderen Hashalgorithmus für die gleiche Gleichheitsbedingung zu testen.
Patricia Shanahan

1

Es ist oft nützlich , wenn nicht öffentlich zugängliche Klassen über eine Methode zum Testen der Äquivalenz verfügen, mit der Objekte unterschiedlicher Typen einander als "gleich" betrachten können, wenn sie die gleichen Informationen darstellen, weil Java jedoch keine Mittel zulässt, mit denen sich Klassen als solche ausgeben können Zum anderen ist es oft gut, einen einzelnen öffentlich zugänglichen Wrapper-Typ zu haben, wenn es möglich sein könnte, äquivalente Objekte mit unterschiedlichen Darstellungen zu haben.

Angenommen, eine Klasse kapselt eine unveränderliche 2D-Wertematrix double. Wenn eine externe Methode nach einer Identitätsmatrix der Größe 1000 fragt, fragt eine zweite nach einer Diagonalmatrix und übergibt ein Array mit 1000 Einsen, und eine dritte fragt nach einer 2D-Matrix und übergibt ein 1000x1000-Array, bei dem alle Elemente auf der primären Diagonale 1,0 sind und alle anderen sind Null, die Objekte, die an alle drei Klassen übergeben werden, können intern unterschiedliche Hintergrundspeicher verwenden [der erste hat ein einzelnes Feld für die Größe, der zweite hat ein Tausend-Elemente-Array und der dritte hat ein Tausend-1000-Elemente-Array], aber sollten sich gegenseitig als äquivalent melden [da alle drei eine unveränderliche 1000x1000-Matrix mit Einsen auf der Diagonale und Nullen überall sonst einkapseln].

Abgesehen von der Tatsache, dass das Vorhandensein unterschiedlicher Backing-Store-Typen verborgen ist, ist der Wrapper auch nützlich, um Vergleiche zu erleichtern, da das Überprüfen von Elementen auf Gleichwertigkeit im Allgemeinen ein mehrstufiger Prozess ist. Fragen Sie den ersten Punkt, ob er weiß, ob er dem zweiten entspricht. Wenn es nicht weiß, fragen Sie das zweite, ob es weiß, ob es gleich dem ersten ist. Wenn keines der beiden Objekte dies weiß, fragen Sie jedes Array nach dem Inhalt seiner einzelnen Elemente [möglicherweise werden weitere Überprüfungen hinzugefügt, bevor Sie sich für den langwierigen Vergleich einzelner Elemente entscheiden].

Beachten Sie, dass die Äquivalenztestmethode für jedes Objekt in diesem Szenario einen Wert mit drei Zuständen zurückgeben muss ("Ja, ich bin äquivalent", "Nein, ich bin nicht äquivalent" oder "Ich weiß nicht"). daher wäre die normale "gleich" -Methode nicht geeignet. Während jedes Objekt einfach "Ich weiß nicht" antworten könnte, wenn es nach einem anderen Objekt gefragt wird, würde das Hinzufügen von Logik zu z. B. einer Diagonalmatrix, die keine Identitätsmatrix oder Diagonalmatrix nach Elementen außerhalb der Hauptdiagonale befragt, Vergleiche zwischen solchen Elementen erheblich beschleunigen Typen.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.