Technischer Projektleiter
Wie man Objekte in Java richtig vergleicht
In einer objektorientierten Programmiersprache wie Java gehört der Vergleich von Objekten untereinander oder mit anderen primitiven Typen zu den häufig durchgeführten grundlegenden Operationen. Aus der Praxis wissen wir jedoch, dass vor allem Anfänger Schwierigkeiten haben, Daten korrekt miteinander zu vergleichen und wann sie die Operatoren Gleichheitszeichen (==) und Ungleichheitszeichen (!=) verwenden sollen und wann die Methoden equals() oder compareTo(). Dies ist für sie oft ziemlich verwirrend.
Das Vergleichen von Objekten ist der Prozess der Überprüfung, ob zwei Objekte gleich sind oder nicht, basierend auf ihren Daten oder Referenzen. In Java werden Objekte aus Klassen erstellt, und jedes Objekt hat seinen eigenen Satz von Daten und definiertem Verhalten. Wir können jedoch auch überprüfen, ob sich Objekte an derselben Stelle im Speicher befinden. Wenn dies der Fall ist, handelt es sich nicht um zwei verschiedene Objekte, sondern um ein und dasselbe.
In diesem Artikel wird anhand von praktischen Beispielen gezeigt, wie man Objekte in Java richtig vergleicht, entweder auf der Basis der Daten in den Objekten oder ihrer Referenzen. Du wirst auch erfahren, wie die Methode equals() implementiert ist und funktioniert. Zudem werden wir auf die Bedeutung der Implementierung der Methode hashCode() eingehen, um Objekte effizient mithilfe von Hash-Algorithmen zu vergleichen.
Vergleich von Objekten mit primitiven Typen
In Java sind primitive Typen wie int, double, boolean, usw. keine Objekte, sondern grundlegende Datentypen. Beim Abgleich von Objekten mit primitiven Typen konvertiert Java das entsprechende Wrapper-Objekt einer Klasse automatisch in seinen primitiven Typ unter Verwendung der erweiterbaren primitiven Konvertierung(§5.1.2). Für letztere sind die folgenden Regeln festgelegt:
- Wenn einer der Operanden vom Typ double ist, wird der andere in double umgewandelt.
- Andernfalls, wenn einer der Operanden vom Typ float ist, wird der andere in float umgewandelt.
- Andernfalls, wenn einer der Operanden vom Typ long ist, wird der andere in long konvertiert.
- Andernfalls werden beide Operanden in den Typ int konvertiert.
Beispiel:
Integer I = new Integer(5);
int i = 5;
System.out.println(I == i);
// => true
System.out.println(I.equals(i));
// => true
Soweit wir sehen können, ist der Vergleich eines Objekts mit seinem primitiven Typ, entweder über den Operator == oder gleich, gleichwertig und wenn die Werte gleich sind, ist das Ergebnis ein boolescher Wert von true.
Vergleich von Objekten anhand von Referenzen
Die Identität einer Variablen (auch Referenzgleichheit genannt) wird durch die Referenz (Verweis) definiert, die sie besitzt. Wenn zwei Variablen die gleiche Referenz haben, sind sie identisch. Dies wird mit dem == Operator überprüft. Es ist wichtig, sich daran zu erinnern, dass der == Operator in Java verwendet wird, um die Referenzen von zwei Objekten zu vergleichen, nicht deren Inhalte. Er prüft also, ob beide Referenzen auf genau dasselbe Objekt im Speicher (oder dessen Adresse) zeigen.
Beispiel:
String s1 = new String("Java");
String s2 = new String("Java");
System.out.println(s1 == s2);
// => false
In diesem Beispiel haben wir denselben Text in beiden Strings, aber da wir Referenzen (Speicherreferenzen auf zwei verschiedene Instanzen) vergleichen, ist das Ergebnis natürlich ein boolescher Wert von false.
Beispiel:
String s1 = new String("Java");
String s2 = s1;
System.out.println(s1 == s2);
// => true
Wenn wir jedoch anstelle der Erstellung einer neuen Instanz die Referenz des ersten Strings in den zweiten String kopieren, ist das Ergebnis beim Vergleich der Referenzen true.
Vergleich von Objekten anhand ihrer Werte
Die Gleichheit einer Variable wird durch den Wert definiert, auf den sie verweist. Wenn zwei Variablen auf denselben Wert verweisen, sind sie gleich. Dies wird in Java mit der Methode equals() überprüft. Sie ist Teil der Klasse Object, die die übergeordnete Klasse aller Java-Klassen ist. Das bedeutet, dass wir in Java die Methode equals verwenden können, um den Dateninhalt von zwei beliebigen Objekten zu vergleichen.
Beispiel:
String s1 = new String("Java");
String s2 = new String("Java");
System.out.println(s1.equals(s2));
// => true
Das Beispiel zeigt den Vergleich von 2 verschiedenen Instanzen des Typs String mit demselben Text. Das Ergebnis des Aufrufs equals() liefert den booleschen Wert true. Wenn wir im vorherigen Beispiel einen Verweis auf dasselbe Objekt kopiert hätten, wäre das Ergebnis des Vergleichs der Werte desselben Objekts ebenfalls wahr.
Beispiel:
String s1 = new String("Java");
String s2 = s1;
System.out.println(s1.equals(s2));
// => true
Bisher haben wir in den Beispielen einfache Objekte verwendet, die von den Java-Autoren in den Bibliotheken bereitgestellt wurden. Jetzt werden wir ein eigenes Objekt erstellen und die Methode equals() genauer unter die Lupe nehmen.
Beispiel:
Klasse Shape
package pack;
public class Shape {
String colour;
public Shape(String colour) {
this.colour = colour;
}
public String getColour() {
return colour;
}
public void setColour(String colour) {
this.colour = colour;
}
}
Klasse Circle
package pack;
public class Circle extends Shape {
double radius;
public Circle (String colour, double radius) {
super(colour);
this.radius = radius;
}
public double getRadius() {
return radius;
}
public void setRadius(double radius) {
this.radius = radius;
}
}
Wir haben eine Klasse Circle erstellt, die von der übergeordneten Klasse Shape erbt. Jetzt erstellen wir zwei Instanzen mit denselben Daten und vergleichen ihre Referenzen und ihre Werte mit der Methode equals.
Circle c1 = new Circle("green", 1);
Circle c2 = new Circle("green", 1);
System.out.println(c1 != c2);
// => true
System.out.println(c1.equals(c2));
// => false
Die Tatsache, dass die Verweise auf die beiden verschiedenen Instanzen unterschiedlich sind, ist zwar das, was wir erwartet haben. Die Überraschung besteht jedoch darin, dass die Methode equals false zurückgibt, obwohl die beiden Objekte dieselben Attributwerte enthalten und somit die Werte der beiden Objekte unterschiedlich sind. Das liegt daran, dass die Standardimplementierung von equals in der Klasse Object, die jedes Objekt erbt, die Speicheradressen der Objekte vergleicht, also genau wie der == Operator funktioniert. Daher müssen wir diese Methode außer Kraft setzen, um zu definieren, was Gleichheit für unsere Objekte bedeutet. Dazu müssen wir jedoch zunächst wissen, welche Regeln für die Methode equals gelten.
Regeln der Methode Equals()
Nehmen wir an, wir haben die Referenzen x, y, z definiert. Dann sollte eine korrekt implementierte equals-Methode die folgenden Eigenschaften haben. Reflexivität: Für jede Referenz x, die nicht Null ist, sollte x.equals(x) true zurückgeben. Symmetrie: für alle Referenzen x und y, die nicht Null sind, sollte x.equals(y) nur dann true zurückgeben, wenn y.equals(x) true zurückgibt. Transitivität: für alle Referenzwerte x, y und z, die nicht Null sind, sollte x.equals(y) true und y.equals(z) true zurückgeben, dann sollte x.equals(z) true zurückgeben. Konsistenz: Für jeden Referenzwert x und y, der nicht Null ist, werden mehrere Aufrufe von x.equals(y), vorausgesetzt, dass keine Informationen, die beim Gleichheitsvergleich der Objekte verwendet werden, geändert werden, konsistent nacheinander dieselben wahren oder falschen Werte zurückgeben. Für jeden Referenzwert, der nicht null ist, sollte x.equals(null) false zurückgeben.
Korrekte Implementierung der Methode equals()
Da wir nun die Regeln von equals kennen, können wir unsere eigene Version von equals für die Klasse Circle wie folgt implementieren.
@Override
public boolean equals(Object o) {
// self check
if (this == o) return true;
// Null check
if (o == null) return false;
// Type check and cast
if (getClass() != o.getClass())
return false;
Circle circle = (Circle) o;
// Field comparison
return Objects.equals(radius, circle.radius)
&& Objects.equals(colour, circle.colour);
}
Wir überprüfen, ob beide Instanzen die gleichen Werte enthalten und ob die Methode equals korrekt funktioniert und true zurückgibt.
Circle c1 = new Circle("green", 1);
Circle c2 = new Circle("green", 1);
System.out.println(c1.equals(c2));
// => true
In unserer Implementierung von equals überprüfen wir zunächst, ob wir das Objekt mit sich selbst vergleichen, und wenn ja, ist das Ergebnis true. Danach prüfen wir, ob die Referenz auf das zweite Objekt, mit dem wir das erste vergleichen, gültig ist (d. h. nicht null), und wenn sie null ist, geben wir false zurück. Keine Instanz sollte gleich null sein, und durch diese Zeile stellen wir sicher, dass wir später keine NullPointerException erhalten, wenn wir auf Attribute zugreifen. Vor dem eigentlichen Vergleich der Attribute testen wir außerdem, ob beide zu vergleichenden Objekte derselben Klasse angehören. Dies stellt sicher, dass selbst wenn die Klasse Circle von der Klasse Shape erbt, der Vergleich dieser Klassen immer false zurückgibt.
Methode hashCode()
In der Java-Praxis ist es üblich, dass wenn wir die ursprüngliche Implementierung von equals() überschreiben, wir auch die Methode hashCode() überschreiben sollten, da beide Methoden zusammenarbeiten, insbesondere wenn wir mit Objektkollektionen arbeiten. Der Zusammenhang zwischen den beiden Methoden besteht darin, dass, wenn die equals-Methode feststellt, dass zwei Objekte gleich sind, sie auch denselben Hashcode berechnen sollten. Ohne eine korrekte Implementierung des Hashcodes funktionieren Sammlungen, die davon abhängen, wie HashSet und HashMap, nicht richtig. Nun zeigen wir an unserer Klasse Circle, wie wir die Methode hashCode() korrekt überschreiben sollten.
@Override
public int hashCode() {
return Objects.hash(radius, colour);
}
Zusammenfassung
In Java ist der Vergleich zweier Objekte nicht so einfach wie der Vergleich zweier primitiver Datentypen. Es erfordert ein tieferes Verständnis der Objektstruktur sowie der Art und Weise, wie sie im Speicher abgelegt werden. Die Methode equals() ist die primäre Methode, die zum Vergleich zweier Objekte in Java verwendet wird. Wenn wir die Methode equals() überschreiben, müssen wir auch die Methode hashCode() überschreiben, um sicherzustellen, dass zwei gleiche Objekte denselben Hashcode haben. Mit den Methoden equals() und hashCode() können wir zwei Objekte effektiv auf Grundlage ihres inneren Zustands vergleichen, anstatt anhand ihres Speicherorts.
Wenn du ein Java Programmierer bist und nach Arbeit suchst, schau dir unsere Mitareiterbenefits an und reagiere auf die neuesten Stellenangebote.