Kurs Java Podstawy - rozszerzony

Metoda hashCode()

Metoda hashCode() służy w Javie do zwrócenia unikalnej wartości liczbowej (typu int) dla każdego unikalnego obiektu. Istnieje kontrakt mówiący o tym, że dwa obiekty, których porównanie przy pomocy metody equals() zwraca true, to metoda hashCode() powinna zwracać dla tych obiektów taką samą wartość.

Choć brzmi to bardzo prosto, to w praktyce zwrócenie unikalnej wartości dla każdego obiektu nie jest tak oczywiste.

  1. Domyślna metoda hashCode()
  2. Własna implementacja
  3. Konsekwencje błędnej implementacji

Domyślna implementacja

Metoda hashCode jest dziedziczona z klasy Object i domyślnie powinna zwrócić unikalną wartość dla każdego obiektu. Jeśli jej nie nadpiszemy, to zwróci ona różne wartości nawet dla obiektów, które pod względem przechowywanych wartości są równe. Spójrzmy na przykład.

public class Product {
    private String name;
    private double price;
    
    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }
}

Po utworzeniu dwóch identycznych obiektów ani ich porównanie przez metodę equals, ani wartości zwracane przez metody hashCode nie będą równe.

public class HashExample {
    public static void main(String[] args) {
        Product prod1 = new Product("Czekolada", 2.99);
        Product prod2 = new Product("Czekolada", 2.99);
        System.out.println(prod1.equals(prod2));
        System.out.println(prod1.hashCode()); //2018699554
        System.out.println(prod2.hashCode()); //1311053135
    }
}

equals hashcode example

Domyślna metoda hashCode wykorzystuje do wyliczenia zwracanej wartości adres obiektu w pamięci. Ta sama wartość jest wykorzystywana w domyślnej metodzie toString, ale w postaci szesnastkowej.

Własna implementacja

Przy nadpisywaniu metody hashCode powinniśmy uwzględnić wszystkie pola klasy. Jeśli są to wartości prostych typów liczbowych możemy je wykorzystać bezpośrednio. W przypadku typów obiektowych powinniśmy wywołać ich metodę hashCode i posłużyć się zwracaną przez nią wartością (deep hashCode). Dla naszej poprzedniej klasy metoda taka może wyglądać tak jak poniżej:

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((name == null) ? 0 : name.hashCode());
    long temp;
    temp = Double.doubleToLongBits(price);
    result = prime * result + (int) (temp ^ (temp >>> 32));
    return result;
}

Do wyliczenia zwracanej wartości wykorzystujemy zarówno wartość name i price. Istnieją różne algorytmy pozwalające wyliczyć hashCode, wszystkie łączy to, że powinniśmy wykorzystać w nich liczbę pierwszą (powyżej 31), następnie wykonać przesunięcia bitowe. Powyższa metoda zostałą wygenerowana przy pomocy eclipse (Source > generate hashCode i equals), nieco inaczej wyglądałoby to w IntelliJ IDEA, czy NetBeansie.

Konsekwencje błędnej implementacji

Pierwszym pytaniem jakie musimy sobie zadać, to gdzie właściwie metoda hashCode jest wykorzystywana? Otóż przede wszystkim we wszystkich strukturach danych, które opierają się na tablicach mieszających (hashtable), np. zbiorach (HashSet), czy mapach (HashMap). W przypadku błędnej implementacji, czyli sytuacji, gdzie dla dwóch różnych obiektów otrzymalibyśmy tę samą wartość zwróconą przez metodę hashCode, wymienione struktury danych nie będą działały poprawnie, np. w przypadku zbiorów nie będzie można dodać obiektu, bo zostanie on potraktowany jako duplikat.

 

Komentarze