Dzisiejszy wpis jest bardzo ważny, z punktu widzenia programowania obiektowego w Javie, oraz przyszłych wpisów. Dobra znajomość metod equals, hashCode oraz zrozumienie kontraktu pomiędzy tymi metodami pozwoli nam uniknąć poważnych problemów w przyszłości 😊 Wpis jest częścią kursu programowania w Javie. Zapraszam!

01. Equals

Z definicji, metoda equals  zwraca czy przekazany jako argument do tej metody obiekt, jest „taki sam” jak obiekt do którego jest porównywany. Prosty przykład:

BigDecimal value = new BigDecimal(1);
BigDecimal value2 = new BigDecimal(1);
System.out.println(value.equals(value2));

Jak się zapewne domyślacie, na konsolę zostanie wypisana wartość true. Co jeśli porównamy obiekty przy pomocy operatora == ?

System.out.println(value == value2);

Na konsoli zostanie wypisane false. Dzieje się tak ponieważ operator ==  sprawdza czy porównywane obiekty to dokładnie te same obiekty. Użycie operatora new tworzy nam zupełnie nowe obiekty, zatem nie możemy używać tego operatora do sprawdzenia „czy obiekty są takie same”. Do tego typu porównania zawsze używamy metody equals!

Wszystko jest proste prawda? Zatem utwórzmy prostą klasę:

public class Person {

    private String name;
    private String surname;

    public Person(String name, String surname) {
        this.name = name;
        this.surname = surname;
    }
}

Stwórzmy dwie „takie same” instancje tej klasy, oraz sprawdźmy je za pomocą equals:

Person person1 = new Person("Arek", "Fajdek");
Person person2 = new Person("Arek", "Fajdek");
System.out.println(person1.equals(person2));

Zgodnie z tym co napisałem powyżej powinniśmy dostać true prawda? Porównujemy przecież „takie same” instancje obiektów metodą equals. Jednak na konsoli dostajemy false. Dlaczego? Metoda equals jest metodą z klasy java.lang.Object (klasa ta jest rodzicem niemal wszystkiego co znajduje się w Javie) oraz wygląda następująco:

public boolean equals(Object obj) {
        return (this == obj);
}

Jak widzicie, podstawowa implementacja metody equals wykorzystuje tylko i wyłącznie operator porównania ==, zatem tak naprawdę sprawdza czy porównywane obiekty to dokładnie te same obiekty.

Naszym zadaniem jest teraz nadpisanie tej metody! Każdy doświadczony programista odradzi wam pisanie własnej implementacji metody equals oraz hashCode, i będzie mieć w 100% rację! Użyj swojego IDE lub innej pomocy (np. biblioteczki Lombok). W InetlliJ możemy wcisnąć ALT + Insert oraz z menu kontekstowego wybrać „equals oraz hashCode”. Implementacja może wyglądać następująco:

public class Person {

    private String name;
    private String surname;

    public Person(String name, String surname) {
        this.name = name;
        this.surname = surname;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return Objects.equals(name, person.name) &&
                Objects.equals(surname, person.surname);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, surname);
    }
}

Po implementacji metody equals, ponownie przeprowadźmy wcześniejszy test:

Person person1 = new Person("Arek", "Fajdek");
Person person2 = new Person("Arek", "Fajdek");
System.out.println(person1.equals(person2));

Teraz zgodnie z naszymi oczekiwaniami, wartość która pojawiła się na konsoli ma wartość true!

02. HashCode

Metoda hashCode, tak samo jak metoda equals jest metodą która znajduje się w klasie java.lang.Object. Metoda ta zwraca int i jest tak jak by skrótową reprezentacją instancji danego obiektu. Już tłumaczę o co tutaj chodzi.

Autorzy Javy doszli do wniosku że można określić czy obiekty są różne w optymalniejszy sposób niż wykonywanie metody equals. Policzenie wartości hashCode powinno być szybkie, a jego wartość dla danej instancji obiektu powinna być zawsze taka sama (niezależnie od tego ile razy wywołamy metodę hashCode dla danej instancji obiektu, dostaniemy zawsze tą samą wartość! No chyba że zmienimy w międzyczasie wartości pól na których bazuje hashCode 😉 )

Jednak należy pamiętać że hashCode zwraca int. Wartość int jest ograniczona, zatem może się zdarzyć że dwa zupełnie różne obiekty mają ten sam hashCode.

Sytuacja ta jest wykorzystywana przez wiele wewnętrznych mechanizmów Javy (szczególnie w kolekcjach, o których będę mówić w kolejnej lekcji!), oraz jest ona podstawą kontraktu pomiędzy metodą equals oraz hashCode.

03. Kontrakt equals oraz hashCode

Kontrakt mówi o tym, że jeżeli dwa obiekty „są takie same” (object1.equals(object2) ma wartość true) to ich wartości hashCode są takie same. Jednak jeśli spojrzymy od strony hashCode wygląda to inaczej, dwa obiekty o takim samym hashCode nie muszą być takie same! (ich wartość equal może, lecz nie musi mieć wartość true).

Prosty przykład! Każdy lubi proste przykłady! Mamy już nam dobrze znaną klasę Person, teraz tworzymy kolejną, z takimi samymi polami:

public class Hero {

    private String name;
    private String surname;

    public Hero(String name, String surname) {
        this.name = name;
        this.surname = surname;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Hero hero = (Hero) o;
        return Objects.equals(name, hero.name) &&
                Objects.equals(surname, hero.surname);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, surname);
    }
}

A teraz utworzymy sobie instancje tych klas oraz sprawdzimy ich hashCode oraz equals:

Hero hero = new Hero("Arek", "Fajdek");
Person person1 = new Person("Arek", "Fajdek");
System.out.println(person1.hashCode());
System.out.println(hero.hashCode());
System.out.println(person1.equals(hero));

Jak widzisz, dwa zupełnie inne obiekty pomimo iż mają ten sam hashCode, nie są equals.

04. Trochę problemów...

Musimy pamiętać że w momencie kiedy chcemy nadpisać equals lub hashCode, musimy nadpisać obie te metody aby kontrakt został zachowany! Złamanie kontraktu może spowodować nieprzyjemne w skutku działanie programu, w szczególności jeśli będziemy pracować z kolekcjami.

Pamiętaj że najłatwiej (oraz najskuteczniej!) jest skorzystać z rozwiązań które zostały już wymyślone! Pozwól aby twoje IDE wykonało za Ciebie robotę podczas generowania metody equals oraz hashCode!

/*Temat związany z equals oraz hashCodem jest dość złożony, i jestem pewien że jeszcze nie raz zaskoczy jakiegoś programistę*/