Diskussion zu equals

Meniskusschaden

Top Contributor
Weil der Programmierer es unnötig komplex machen wollte. Wenn du dir den Code von Object.equals ansiehst:
Java:
public boolean equals(Object obj) {
return (this == obj);
}
Dann wäre ein return false; an der Stelle auch vollkommen ausreichend.
Wenn man dem doch noch etwas abgewinnen will, könnte man vielleicht argumentieren, dass man die Gleichheitsentscheidung bei ungleichen Klassen nicht der Klasse Kunde überlassen will, sondern der nächsten Oberklasse. Das ist zwar jetzt noch Object, aber das muss ja nicht so bleiben. Ist vielleicht etwas konstruiert, aber prinzipiell kann es so etwas ja schon mal geben, wie folgendes Beispiel zeigt:
Java:
    public static void main(String[] args) {
        ArrayList<Integer> l1 = new ArrayList<>(List.of(1, 2, 3));
        LinkedList<Integer> l2 = new LinkedList<>(List.of(1, 2, 3));
        System.out.println(l1.equals(l2)); // liefert true
    }
 

mrBrown

Super-Moderator
Mitarbeiter
Wenn man dem doch noch etwas abgewinnen will, könnte man vielleicht argumentieren, dass man die Gleichheitsentscheidung bei ungleichen Klassen nicht der Klasse Kunde überlassen will, sondern der nächsten Oberklasse. Das ist zwar jetzt noch Object, aber das muss ja nicht so bleiben.
Das dürfte allerdings in nahezu jedem Fall die geforderte Transitivität brechen.


Konstruiertes Beispiel, 3D-Punkt "extends" 2D-Punkt:

Code:
(1,1,1) == (1,1) //delegiert an 2D-Punkt, daher gleich
(1,1,2) == (1,1) //delegiert an 2D-Punkt, daher gleich
(1,1,1) != (1,1,2) //delegiert nicht an 2D-Punkt, daher ungleich, was equals bricht
 

Meniskusschaden

Top Contributor
Das dürfte allerdings in nahezu jedem Fall die geforderte Transitivität brechen.
Ja. Ich bin auch kein Verfechter davon. Aber ist es "in nahezu jedem Fall" oder "in jedem Fall" so? "Nahezu jeder Fall" genügt ja nicht, um das Konstrukt als prinzipiell schlecht anzusehen. Mir fällt aber kein wirklich gutes Beispiel ein. Vielleicht gibt es auch keins. Angenommen man hat ein Kunde-Objekt und ein Mitarbeiter-Objekt, die sich auf dieselbe Person beziehen. Vielleicht lässt sich daraus ein sinnvoller Anwendungsfall konstruieren. Etwa ob die beiden Briefe, die an dem Tag verschickt werden sollen, von der Kuvertiermaschine in denselben Umschlag gesteckt werden dürfen. ;)
 

mrBrown

Super-Moderator
Mitarbeiter
Ja. Ich bin auch kein Verfechter davon. Aber ist es "in nahezu jedem Fall" oder "in jedem Fall" so? "Nahezu jeder Fall" genügt ja nicht, um das Konstrukt als prinzipiell schlecht anzusehen. Mir fällt aber kein wirklich gutes Beispiel ein. Vielleicht gibt es auch keins.
Mir fällt zumindest kein Fall ein, in dem das problemlos funktioniert, nur genügend Fälle, in denen es schief geht (und die lassen sich eigentlich immer auf das "Punkt"-Beispiel reduzieren). Für mich wäre das genug, um das prinzipiell als Schlecht anzusehen, Ausnahmen bestätigt die Regel und so.

Und grad in diesem Fall: Solange jemand nicht den Contract für equals aus dem FF runterbeten kann und solche Fälle direkt als problematisch erkennt und erklären kann, sollte man besser nichts anders als das "Standard-Bloch-Template" zu sehen bekommen.

Angenommen man hat ein Kunde-Objekt und ein Mitarbeiter-Objekt, die sich auf dieselbe Person beziehen. Vielleicht lässt sich daraus ein sinnvoller Anwendungsfall konstruieren. Etwa ob die beiden Briefe, die an dem Tag verschickt werden sollen, von der Kuvertiermaschine in denselben Umschlag gesteckt werden dürfen. ;)
Das funktioniert dann aber nur solange, wie Kunde und Mitarbeiter equals generell immer an die Oberklasse delegieren, und überschreiben somit eh überflüssig war?

Aber was Java manchmal fehlt ist ein ComparEqualator-Interface :p
 
B

BestGoalkeeper

Gast
Meinst du mit delegiert/delegieren nur "super. ..." oder auch "instanceof"? (was ja auch die sub class miteinbezieht).

Allgemein: super- und instanceof-Aufrufe sind in equals() oft toxisch. D.h., zwar nicht immer, aber in der überwiegenden Anzahl der Fälle, denke ich.
 

temi

Top Contributor
Man könnte die Typgleichheit auch mit getClass() prüfen. Allerdings wird dadurch dann das Substitutionsprinzip verletzt.
 
Zuletzt bearbeitet:
B

BestGoalkeeper

Gast
Das instanceof braucht man in equals eigentlich immer
Weder das instanceof noch das super.equals braucht man, wenn nur genau dieselben Klassen gleich sein sollen:
Java:
public class Mensch {
    private String vorname, nachname;

    public Mensch(String vorname, String nachname) {
        super();
        this.vorname = vorname;
        this.nachname = nachname;
    }

    @Override
    public int hashCode() {
        return Objects.hash(nachname, vorname);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Mensch other = (Mensch) obj;
        return Objects.equals(nachname, other.nachname) && Objects.equals(vorname, other.vorname);
    }
}
mrBrowns Beispiel mit dem Punkt habe ich aber immer noch nicht verstanden. Dazu müsste equals() doch in unterschiedlichen Klassen unterschiedlich implementiert werden (also in 2D anders wie in 3D).
 
K

kneitzel

Gast
Ich denke, dass mrBrown das noch nicht wirklich durchdacht hat, denn die Problematik mit dem instance of ist ebenso kritisch wie die Prüfung mit dem super.equals Aufruf.

Dein Vergleich der Klassen ist notwendig und wichtig aus meiner Sicht, denn das Beispiel von mrBrown hat nur eine Seite beleuchtet: Die Problematik bei dem super Aufruf:
Der 3D Punkt kann ja vom 2D Punkt erben. Der Vergleich eines 2D Punktes mit eine, 3D Punkt kann dann falsche Ergebnisse liefern, weil der Vergleich nur x und y Koordinate vergleicht und nicht die z Koordinate. Das scheint mrBrown aber nur bei dem super.equals Aufruf gesehen zu haben, aber das trifft ja auch beim 2dPoint.equals(3dPoint) ebenso direkt zu, denn der 3d Punkt ist halt auch ein instanceOf 2d Punkt und schwups: Wird da der Cast gemacht, x und y Verglichen und das Ergebnis zurück gegeben.

Also schön die Klasse selbst in equals prüfen wie Du es gezeigt hast. Das ist das Pattern, das ich auch kenne.

Eine andere Variante findet sich bei Lombok - die bauen zusätzlich noch weitere protected Methoden auf (canEqual) und dann wird das mit geprüft.
Das führt also zu potenziellen Problemen:
Ich baue eine Klasse 2DPoint mit Lombok und jemand anderes leitet 3DPoint davon ab ohne canEqual zu überschreiben. Das kann dann auch zu dieser Problematik führen, dass ein 2D Punkt gleich einem 3D Punkt ist ...
Dazu kann man sich die Equals Implementierung einmal ansehen: https://projectlombok.org/features/EqualsAndHashCode
 
K

kneitzel

Gast
Kleiner Verbesserungsvorschlag:
Java:
public final class Mensch {
Das behebt zwar auch das Problem, aber das ist ähnlich wie: "Entwickle gar keine Software". Du hast diese Probleme dann auch nicht mehr, aber du hast diese nur vermieden statt diese zu lösen.

Das final behebt zwar alle Probleme, die durch das Ableiten einer Klasse von Mensch erzeugen könnten, aber ich würde dazu tendieren, einfach equals und co korrekt zu schreiben, so dass diese Probleme nicht auftreten können.
 

mrBrown

Super-Moderator
Mitarbeiter
Weder das instanceof noch das super.equals braucht man, wenn nur genau dieselben Klassen gleich sein sollen:
Dann mach Mensch aber auch final, überschreibbar ist es dann nämlich nicht.

mrBrowns Beispiel mit dem Punkt habe ich aber immer noch nicht verstanden. Dazu müsste equals() doch in unterschiedlichen Klassen unterschiedlich implementiert werden (also in 2D anders wie in 3D).

Das bezieht sich auf das "Antipattern" aus dem Originalcode, wo an die Oberklasse delegiert wurde, wenn der instanceof-Check fehl schlägt. Code dazu etwa:

[CODE lang="java" highlight="36-41"]static class Point2D {
int x, y;

public Point2D(final int x, final int y) {
this.x = x;
this.y = y;
}

public boolean equals(final Object other) {
if (this == other) {
return true;
}
if (!(other instanceof Point2D)) {
return false;
}
final Point2D point2D = (Point2D) other;
return x == point2D.x
&& y == point2D.y;
}

}

static class Point3D extends Point2D {
int z;

public Point3D(final int x, final int y, final int z) {
super(x, y);
this.z = z;
}

public boolean equals(final Object other) {
if (this == other) {
return true;
}

if (other instanceof Point3D) {
Point3D that = (Point3D) other;
return this.x == that.x
&& this.y==that.y
&& this.z == that.z;
}

return super.equals(other);
}
}

public static void main(String[] args) {
final Point2D origin = new Point2D(0, 0);
final Point3D one = new Point3D(0,0,1);
final Point3D two = new Point3D(0,0,2);

System.out.println(origin.equals(one));
System.out.println(origin.equals(two));
System.out.println(one.equals(two));
}[/CODE]
 

mrBrown

Super-Moderator
Mitarbeiter
Ich denke, dass mrBrown das noch nicht wirklich durchdacht hat, denn die Problematik mit dem instance of ist ebenso kritisch wie die Prüfung mit dem super.equals Aufruf.

instanceof erlaubt zumindest generell Überschreiben, der Vergleich der Klassen verhindert jedes Überschreiben, daher würd ich das nur mit finalen Klassen nutzen (aber dann wäre auch instanceof problemlos möglich).

equals überschreiben führt natürlich ganz generell eigentlich immer zu Problemen, aber das ist ja noch was anderes :)
 
K

kneitzel

Gast
Und genau das macht doch Probleme:
Java:
        public boolean equals(final Object other) {
            if (this == other) {
                return true;
            }
            if (!(other instanceof Point2D)) {
                return false;
            }
            final Point2D point2D = (Point2D) other;
            return x == point2D.x
                   && y == point2D.y;
        }

Wenn Du also ein Point2D mit einem Point3D vergleichst, dann ist other instanceof Point2D true, denn Point3D erbt von Point2D. Also wird im Anschluss nur x und y verglichen.

==> Daher statt instanceof die getClass() vergleichen.
 
K

kneitzel

Gast
Das Problem ist aber doch, dass gelten sollte:
a.equals(b) == b.equals(a)

Und das ist bei Deinem Beispiel nicht der Fall.

Und ansonsten haben wir hier jetzt sehr schön herausgearbeitet, wieso Lombok macht, was es macht denke ich mal.
 

mrBrown

Super-Moderator
Mitarbeiter
Das Problem ist aber doch, dass gelten sollte:
a.equals(b) == b.equals(a)

Und das ist bei Deinem Beispiel nicht der Fall.
Ne, Symmetrie ist in dem Fall sogar gegeben, nur die Transitivität ist kaputt:

Java:
    public static void main(String[] args) {
        final Point2D a = new Point2D(0, 0);
        final Point3D b = new Point3D(0, 0, 1);
        final Point3D c = new Point3D(0, 0, 2);

        System.out.println("reflexive");
        System.out.println("a==a: " + Objects.equals(a, a));
        System.out.println("symmetric");
        System.out.println("a==b: " + Objects.equals(a, b));
        System.out.println("b==a: " + Objects.equals(b, a));
        System.out.println("transitive");
        System.out.println("a==b: " + Objects.equals(a, b));
        System.out.println("a==c: " + Objects.equals(a, c));
        System.out.println("b==c: " + Objects.equals(b, c));
    }
 
K

kneitzel

Gast
Ach so - durch das super.equals(other) wird ja die Prüfung an den 2D Punkt gegeben (wenn es kein 3D Punkt ist).

Aber das ist ein Verhalten, das ich zumindest als irritierend empfinde - wenn ein 2D Punkt gleich sein soll zu einem 3D Punkt. Denn die sind ja nun einmal nicht wirklich gleich :)
 
B

BestGoalkeeper

Gast
Kleiner Verbesserungsvorschlag:
Java:
public final class Mensch {
Verstehe ich das jetzt richtig, wenn equals() reflexiv, antisymmetrisch und transitiv (Halbordnung) sein soll, sollte man weder extends noch instanceof noch super.equals verwenden?

Bearbeitung: Meine, wenn equals() eine Äquivalenzrelation garantieren soll.
 
K

kneitzel

Gast
@JustNobody Bitte woanders weiterdiskutieren - diese Gedanken sollte sich auch wahrscheinlich der TE machen/gemacht haben.
Wenn sich das etwa überschneidet, dann ist es so. Soll ich das jetzt ein zweites Mal schreiben? Dann sollte doch eher der Mod so Posts ebenfalls verschieben. Und eine Wiederholung der Aussage vom Moderator bringt nichts, den mit dem Posten hat mein Browser das ja auch aktualisiert und ich dann den Beitrag spätestens auch gesehen ...
 
K

kneitzel

Gast
Ich frage mich gerade, ob das Point2D/3D-Beispiel nicht ohnehin ein Verstoß gegen "Komposition vor Vererbung" ist. So eine richtig saubere ist-ein-Beziehung ist das doch nicht.🤔
Also das war teilweise auch schon ein Gedanke von mir, wobei ich da auch keine Komposition haben würde, sondern schlicht zwei Klassen, die voneinander unabhängig sind. Denn ein 3D Punkt ist kein 2D Punkt und verhält sich auch nicht so. Und er hat drei (x,y,z) Koordinaten und nicht einen 2D Punkt mit einer Z Koordinate.

Der Thread ist aber sehr interessant, wenn er zeigt mir, dass man im Betrieb schnell eine Art Betriebsblindheit entwickelt. Man hat halt seine Arbeitsweise und z.B. bei equals / hashcode hat man eine bestimmte Arbeitsweise, wie es gemacht werden soll.

Und da kommt ein Punkt hinzu, der wichtig ist: Was ist die genaue Erwartungshaltung an equals? Das muss man ja auch einmal definieren. Wann sind zwei Instanzen gleich?

Nur um ein Beispiel zu nennen: In Bezug auf Lombok mit Hibernate wurde mir neulich in einem Thread (habe ich jetzt nicht auf Anhieb wieder gefunden) ein Link gegeben, in dem für hashcode ein konstanter Wert zurück gegeben werden sollte und für equals sollte der Primary Key geprüft werden: Wenn kein primary key gesetzt ist, dann gibt equals immer false aus, ansonsten wird der primary Key verglichen...
 

mrBrown

Super-Moderator
Mitarbeiter
Vererbung ist da ein AntiPattern und Unsinn, aber ...

Also das war teilweise auch schon ein Gedanke von mir, wobei ich da auch keine Komposition haben würde, sondern schlicht zwei Klassen, die voneinander unabhängig sind. Denn ein 3D Punkt ist kein 2D Punkt und verhält sich auch nicht so. Und er hat drei (x,y,z) Koordinaten und nicht einen 2D Punkt mit einer Z Koordinate.
... ein 3D-Punkt kann ja schon alles, was auch ein 2D-Punkt kann, da kommen viele darauf, dass man da Vererbung nutzten sollte.
Komposition bieten sich da auch an (als Implementierungsdetail), das macht schon manches einfacher und bringt keine Probleme mit sich.



Nur um ein Beispiel zu nennen: In Bezug auf Lombok mit Hibernate wurde mir neulich in einem Thread (habe ich jetzt nicht auf Anhieb wieder gefunden) ein Link gegeben, in dem für hashcode ein konstanter Wert zurück gegeben werden sollte und für equals sollte der Primary Key geprüft werden: Wenn kein primary key gesetzt ist, dann gibt equals immer false aus, ansonsten wird der primary Key verglichen...
Hat ganz einfache Gründe: auf Datenbank-Seite entscheidet nur der Primärschlüssel über Gleichheit, also muss man das auf Java-Seite auch machen – daher der Vergleich mit Primärschlüssel.
Der Primärschlüssel ist allerdings nicht konstant, ohne dass das Objekt gespeichert wurde – also für hashCode nicht nutzbar, da dafür über die gesamte Lebensdauer konstante Werte vorausgesetzt werden, also einen konstante Wert nutzen. Und für equals kann das beutetet, dass zwei ungespeicherte Objekte beide den Primärschlüssel null haben, also muss man den Fall explizit als "nicht gleich" behandeln.
 

LimDul

Top Contributor
Übrigens sehr interessante Diskussion. Ich stelle fest, dass ich auch falsch lag, als ich sagte macht braucht instanceof und/oder super.

Das ist tatsächlich gefährlich, weil sonst ein Vergleich von superclassObject.equals(classObject) true liefert, während classObject.equals(superClassObject) false liefert. Was natürlich nicht sein darf.

Das bedeutet aber eigentlich im Umkehrschluss das ich entweder so ein Konstrukt wie lombok baut, brauche oder ich in jeder abegeleiteten Klasse die equals Methode voll ausimplementiere inkl. des Vergleichs aller Attribute der Superklasse.

So sehr hab ich bisher gar nicht über equals nachgedacht.
 

mrBrown

Super-Moderator
Mitarbeiter
Alle drei Varianten haben ihre Tücken:
  • mit instanceof darf man equals nicht überschreiben, sonst gehts kaputt
  • Mit getClass muss man equals überschreiben, verstößt damit aber gegen das liskovsche Substitutionsprinzip
  • Mit canEqual muss man nicht überschreiben, darf aber, man muss dabei aber auf canEqual achten, und verstößt dann auch gegen das liskovsche Substitutionsprinzip
Daher würde ich meisten entweder:
  • instanceof und equals final oder Klasse final
  • getClass und Klasse final
  • oder die Dritte Variante nutzen, und den Bruch in Kauf nehmen
Eine wirklich perfekte Lösung gibt’s da nicht.

Und wenn man mit Hibernate und ähnlichem arbeitet muss man natürlich noch deren Eigenheiten beachten ..
 
Ähnliche Java Themen
  Titel Forum Antworten Datum
U [Diskussion] Java vs. C# Softwareentwicklung 208
M Diskussion: MVC Softwareentwicklung 5

Ähnliche Java Themen

Neue Themen


Oben