Po co zastępować metody równości i hashcode w Javie?
Źródło:
Medium W tym artykule skupiono się na dwóch blisko powiązanych metodach: quals() i hashcode() . Dowiesz się, jak współdziałają ze sobą i jak prawidłowo je zastąpić.
Dlaczego zastępujemy metodę równości()?
W Javie nie możemy przeciążać zachowania operatorów takich jak
== ,
+= ,
-+ . Działają według zadanego procesu. Rozważmy na przykład operację operatora
== .
Jak działa operator ==?
Sprawdza, czy dwa porównywane odniesienia wskazują na tę samą instancję w pamięci. Operator
== zwróci wartość true tylko wtedy, gdy dwa odniesienia reprezentują tę samą instancję w pamięci. Rzućmy okiem na przykładowy kod:
public class Person {
private Integer age;
private String name;
..getters, setters, constructors
}
Załóżmy, że w swoim programie utworzyłeś dwa obiekty
Person w różnych miejscach i chcesz je porównać.
Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println( person1 == person2 ); --> will print false!
Z biznesowego punktu widzenia oba wyglądają tak samo, prawda? Ale w przypadku JVM nie są one takie same. Ponieważ oba są tworzone przy użyciu słowa kluczowego
new , te instancje znajdują się w różnych segmentach pamięci. Dlatego operator
== zwróci
false . Ale jeśli nie możemy zastąpić operatora
== , to jak powiedzieć maszynie JVM, że chcemy, aby te dwa obiekty były traktowane tak samo? W tym miejscu do gry wchodzi metoda
.equals() . Możesz zastąpić funkcję
równości() , aby sprawdzić, czy niektóre obiekty mają te same wartości dla niektórych pól, aby uznać je za równe. Możesz wybrać, które pola chcesz porównać. Jeśli powiemy, że dwa obiekty
Person będą takie same tylko wtedy, gdy będą miały ten sam wiek i tę samą nazwę, wówczas IDE wygeneruje coś takiego , aby automatycznie utworzyć
równa() :
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
name.equals(person.name);
}
Wróćmy do naszego poprzedniego przykładu.
Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println ( person1 == person2 ); --> will print false!
System.out.println ( person1.equals(person2) ); --> will print true!
Tak, nie możemy przeciążać operatora
== , aby porównywać obiekty tak, jak chcemy, ale Java daje nam inny sposób - metodę
równości() , którą możemy zastąpić według własnego uznania.
Należy pamiętać, że jeśli nie udostępnimy w naszej klasie niestandardowej wersji funkcji .equals() (znanej również jako przesłonięcie), wówczas predefiniowana funkcja .equals() z klasy Object i operator == będą zachowywać się tak samo. Domyślna metoda
równości() , odziedziczona z
Object , sprawdza, czy obie porównywane instancje są takie same w pamięci!
Dlaczego zastępujemy metodę hashCode()?
Niektóre struktury danych w Javie, takie jak
HashSet i
HashMap , przechowują swoje elementy w oparciu o funkcję skrótu zastosowaną do tych elementów. Funkcja skrótu to
hashCode() . Jeśli mamy wybór nadpisania metody
.equals() , powinniśmy mieć także możliwość nadpisania metody
hashCode() . Jest ku temu powód. W końcu domyślna implementacja
funkcji hashCode() , odziedziczona z
Object , traktuje wszystkie obiekty w pamięci jako unikalne! Wróćmy jednak do tych struktur danych skrótu. Istnieje reguła dotycząca tych struktur danych.
HashSet nie może zawierać zduplikowanych wartości, a HashMap nie może zawierać zduplikowanych kluczy. HashSet jest implementowany przy użyciu
HashMap w taki sposób, że każda wartość
HashSet jest przechowywana jako klucz w
HashMap . Jak działa
HashMap ?
HashMap to natywna tablica z wieloma segmentami. Każdy segment ma połączoną listę (
linkedList ). Ta połączona lista przechowuje nasze klucze.
HashMap znajduje poprawną listę połączoną dla każdego klucza za pomocą metody
hashCode() , a następnie przechodzi przez wszystkie elementy tej
listy połączonej i stosuje metodę
równości() do każdego z tych elementów, aby sprawdzić, czy element ten się tam zawiera.
Duplikaty kluczy są niedozwolone. Kiedy umieścimy coś w
HashMap , klucz jest przechowywany na jednej z połączonych list. Na której połączonej liście będzie przechowywany ten klucz, pokaże wynik metody
hashCode() dla tego klucza. Oznacza to, że jeśli
funkcja key1.hashCode() daje w wyniku 4, wówczas ten
klucz1 zostanie zapisany w czwartym segmencie tablicy na istniejącej tam
liście LinkedList . Domyślnie metoda
hashCode() zwraca różne wyniki dla każdej instancji. Jeśli mamy domyślną funkcję
równości() , która zachowuje się jak
== i traktuje wszystkie instancje w pamięci jako różne obiekty, nie będzie problemu. Jak być może pamiętasz, w naszym poprzednim przykładzie powiedzieliśmy, że chcemy, aby instancje
Person były uznawane za równe, jeśli ich wiek i imiona są takie same.
Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println ( person1.equals(person2) ); --> will print true!
Utwórzmy teraz mapę do przechowywania tych instancji jako kluczy z określonym ciągiem jako parą wartości.
Map<Person, String> map = new HashMap();
map.put(person1, "1");
map.put(person2, "2");
W klasie
Person nie zastąpiliśmy metody
hashCode , ale mamy nadpisaną metodę
równości . Ponieważ domyślny
hashCode daje różne wyniki dla różnych instancji Java
person1.hashCode() i
person2.hashCode() , istnieje duże prawdopodobieństwo uzyskania różnych wyników. Nasza mapa może kończyć się różnymi
osobami na różnych połączonych listach.
Jest to sprzeczne z logiką
HashMap .
W końcu HashMap nie może mieć kilku identycznych kluczy! Rzecz w tym, że domyślny
hashCode() odziedziczony z klasy
Object nie wystarczy. Nawet po zastąpieniu metody
równości() klasy
Person . Dlatego musimy zastąpić metodę
hashCode() po zastąpieniu metody
równości . Teraz naprawmy to. Musimy zastąpić naszą metodę
hashCode() tak, aby uwzględniała te same pola co
równa się() , a mianowicie
wiek i
imię .
public class Person {
private Integer age;
private String name;
..getters, setters, constructors
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
name.equals(person.name);
}
@Override
public int hashCode() {
int prime = 31;
return prime*Objects.hash(name, age);
}
W metodzie
hashCode() zastosowaliśmy prostą wartość (można zastosować dowolne inne wartości). Sugeruje się jednak używanie liczb pierwszych, aby stworzyć mniej problemów. Spróbujmy ponownie zapisać te klucze w naszej
HashMap :
Map<Person, String> map = new HashMap();
map.put(person1, "1");
map.put(person2, "2");
person1.hashCode() i
person2.hashCode() będą takie same. Powiedzmy, że wynoszą 0.
HashMap przejdzie do segmentu 0, a w nim
LinkedList zapisze
osobę1 jako klucz o wartości „1”. W drugim przypadku, gdy
HashMap ponownie przejdzie do segmentu 0, aby zapisać klucz
osoba2 o wartości „2”, zobaczy, że inny klucz mu równy już tam istnieje. W ten sposób nadpisze poprzedni klucz. I tylko kluczowa
osoba2 będzie istnieć w naszej
HashMap . W ten sposób dowiedzieliśmy się, jak działa reguła
HashMap , która mówi, że nie można używać wielu identycznych kluczy!
Należy jednak pamiętać, że nierówne instancje mogą mieć ten sam hashcode, a równe instancje muszą zwracać ten sam hashcode.
GO TO FULL VERSION