Freitag, 9. August 2019

Objkete null-sicher vergleichen

Wenn wir Objekte auf Gleichheit vergleichen möchten, benötigen wir natürlich die equals()-Methode. Die implementieren wir in der jeweiligen Klasse und programmieren sie so, dass sie zwei Objekte miteinander vergleichen kann. Schnappen wir uns mal eine einfache Personen-Klasse:

public class Person {
    private String firstName;
    private String lastName;
 }



Jetzt führen wir einmal die folgenden Vergleiche durch. Sie werden alle wunderbar funktionieren.

Person person1 = new Person("Phil", "Harmonie");
Person person2 = new Person("Elle", "Fant");
Person person3 = new Person("Phil", "Harmonie");

System.out.println("Person1 equals Person2: " + person1.equals(person2));
System.out.println("Person2 equals Person3: " + person2.equals(person3));
System.out.println("Person3 equals Person2: " + person3.equals(person3));
System.out.println("Person1 equals null: " + person1.equals(null));



Sie werden einwandfrei funktionieren, unter der Voraussetzung, dass wir daran gedacht haben, in der equals()-Methode einen null-Wert abzufragen. Haben wir das vergessen, wird uns der letzte Vergleich mit einer NullPointerException um die Ohren fliegen.

Daher gibt es die Hilfsmethode Objects.equals(). Sie erhält zwei Übergabeparameter und tut nichts weiter als deren equals()-Methode aufzurufen. Sie verspricht dabei null-sicher zu sein, d.h. sie kann mit null-Werten umgehen. Wir brauchen also die Überprüfung auf null in die equals()-Methode nicht mehr einzubauen:

System.out.println("Person1 equals Person2: " + Objects.equals(person1, person2));
System.out.println("Person2 equals Person3: " + Objects.equals(person2, person3));
System.out.println("Person3 equals Person1: " + Objects.equals(person3, person1));
System.out.println("null equals Person1: " + Objects.equals(null, person1));
System.out.println("null equals Person1: " + Objects.equals(person1, null));
System.out.println("Person1 equals null: " + Objects.equals(null, null)); 



Wenn einer der beiden Parameter null ist, gibt Objects.equals() false aus. Sind beide Parameter gleich, wird true ausgegeben. Wenn beide Parameter null sind, also der letzte Fall im Beispiel, sind beide Parameter gleich und somit ist die Rückgabe konsequenterweise true.

Nun ist mir beim Experimentieren ein Problem aufgefallen. Sie müssen unbedingt in die equals()-Methode einen Test auf die richtige Klasse mit instanceof einbauen, denn sonst gibt es auf jeden Fall eine NullPointerException, wenn die zweite Übergabe null ist. Warum das so ist, zeige ich gleich. Erst einmal ein bisschen Code, der zeigt, was ich meine:

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Person)) {
            return false;
        }
        Person other = (Person) obj;
        if (firstName == null) {
            if (other.firstName != null)
                return false;
        } else if (!firstName.equals(other.firstName))
            return false;
        if (lastName == null) {
            if (other.lastName != null)
                return false;
        } else if (!lastName.equals(other.lastName))
            return false;
        return true;
    }



Den Test auf null habe ich weggelassen. Aber die erste if-Anweisung, die das instanceof enthält, muss unbedingt vorhanden sein. Das liegt an der - meiner Meinung nach nicht ganz glücklichen Implementierung der Methode Objects.equals(). In einer guten IDE können wir uns die Implementierung ansehen:

public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
}


Sie sehen, dass eigentlich nur der erste Parameter auf null überprüft wird. Und dann wird die equals()-Methode mit dem zweiten Parameter aufgerufen. Ist der zweite Parameter also null und wird dann in equals() eine Methode aufgerufen wie z.B. getClass(), kracht es auch hier mit einer NullPointerException. Es hat ein paar Minuten gedauert, bis ich das mit einer Standardimplementierung der equals()-Methode von Eclipse herausgefunden habe. An der Implementierung sehen Sie allerdings auch, dass Sie sich ebenfalls den Referenzvergleich mit == schenken können.

Also so ganz hält die Methode ihr Versprechen nicht. Aber ein instanceof müssen Sie in der equals()-Methode sowieso verwenden, bevor Sie beide Parameter von Object in den jeweiligen Objekttyp casten. Dann schreiben Sie instanceof einfach direkt an den Anfang und arbeiten nicht mit der getClass()-Methode.

Natürlich können Sie Ihre equals()-Methode nur derart kürzen, wenn Sie und Ihr Team konsequent Objects.equals() für Objektvergleiche einsetzen.

Das oben beschriebene Problem kann auch noch anders umgangen werden. Objects bietet auch noch die Methode deepEquals() an, mit der man auch bspw. Arrays miteinander vergleichen kann. Betrachten wir mal deren Implementierung:

    public static boolean deepEquals(Object a, Object b) {
        if (a == b)
            return true;
        else if (a == null || b == null)
            return false;
        else
            return Arrays.deepEquals0(a, b);
    }



Und hier ist zu sehen, dass tatsächlich beide Parameter auf null geprüft werden. Mit dieser Methode wäre man also entsprechend noch sicherer unterwegs bei den folgenden Vergleichen.

System.out.println("null equals Person1: " + Objects.deepEquals(null, person1));
System.out.println("null equals Person1: " + Objects.deepEquals(person1, null));
System.out.println("Person1 equals null: " + Objects.deepEquals(null, null));

Keine Kommentare:

Kommentar veröffentlichen