To jest jeden z artykułów w ramach darmowego kursu programowania w Javie. Proszę zapoznaj się z pozostałymi częściami, mogą one być pomocne w zrozumieniu materiału z tego artykułu.

Porównywanie typów prostych

Do sprawdzenia “równości” typów prostych służą operatory == oraz !=. Dzięki nim możemy porównać ze sobą każdą zmienną typu prostego. Wynikiem takiego porównania jest wartość typu booleantrue jeśli porównywane obiekty są równe i false w przeciwnym wypadku. Proszę spójrz na przykład poniżej:

System.out.println("10 == 10: " + (10 == 10));
System.out.println("10 != 10: " + (10 != 10));
System.out.println("true == true: " + (true == true));
System.out.println("true != true: " + (true != true));
System.out.println("'a' == 'a': " + ('a' == 'a'));
System.out.println("'a' != 'a': " + ('a' != 'a'));
System.out.println("500L == 500L: " + (500L == 500L));
System.out.println("500L != 500L: " + (500L != 500L));

Kolejne linijki porównują odpowiednio:

  • liczby typu int,
  • zmienne typu boolean,
  • znaki typu char,
  • liczby typu long.

Priorytety operatorów

Drobna dygresja dotycząca priorytetów operatorów. W języku Java wszystkie operatory mają tak zwany priorytet. Oznacza to tyle, że priorytet operatorów określa kolejność wykonywania działań. Proszę spójrz na przykład niżej

10 == 4 + 6
4 + 6 == 10
3 * 5 + 2

Operator + ma wyższy priorytet niż operator ==. W związku z tym operacja dodawania wykonana zostanie jako pierwsza i porównanie zwróci true. Podobnie * ma wyższy priorytet niż +. Zatem na początku wykonana zostanie operacja mnożenia a na końcu dodawanie.

Czasami jednak domyślny priorytet operatorów nie jest odpowiedni, chcielibyśmy wykonać operacje w innej kolejności. Z pomocą przychodzą nawiasy, które pozwalają na modyfikację zachowania programu. Przykład poniżej pomaga zrozumieć jak to działa:

3 * (5 + 2)

Mimo tego, że operator * ma wyższy priorytet niż + operacja mnożenia zostanie wykonana jako druga. Pierwsze zostanie wykonane dodawanie ponieważ zostało otoczone parą nawiasów.

Po tym wstępie mogę wyjaśnić dlaczego w przykładzie poniżej potrzebujemy nawiasów:

"10 == 10: " + 10 == 10 // compilation error!
"10 == 10: " + (10 == 10)

Bez dodatkowej pary nawiasów pierwszeństwo miałby operator +. "10 == 10: " + 10 == 10 inaczej możemy zapisać jako "10 == 10: 10"  == 10 a taki zapis nie jest poprawny, ponieważ operatorem == nie możemy porównać instancji typu String i wartości typu int.

Porównywanie zmiennoprzecinkowych typów prostych

O ile sprawdzanie równości wartości liczb całkowitych nie jest trudne to ta sama operacja dla typów zmiennoprzecinkowych jest trochę bardziej skomplikowana. W związku ze sposobem reprezentacji liczb zmiennoprzecinkowych typu float i double w pamięci komputera nie jest możliwe ich dokładne porównywanie. Operacja taka jest dopuszczalna ale może prowadzić do dziwnych rezultatów (na przykład, liczby, które teoretycznie powinny być równe według komputera nie są).

W związku z tym liczby zmiennoprzecinkowe powinno się porównywać z pewną dokładnością.

System.out.println("0.3 == 0.1 + 0.2: " + (0.3 == 0.1 + 0.2)); // !!!
System.out.println("0.3 == 0.1 + 0.2: " + (Math.abs(0.3 - (0.1 + 0.2)) < 0.000001));

W przykładzie powyżej została użyta metoda Math.abs(). Metoda ta zwraca wartość bezwzględną danej liczby. Następnie wartość tą porównujemy z bardzo małą liczbą. Liczba ta reprezentuje dokładność porównania. Jeśli różnica liczb jest mniejsza niż nasza założona dokładność uznajemy, że porównywane liczby są równe.

Porównanie obiektów

Używając operatora == do porównywania obiektów uzyskamy błędne rezultaty. Do porównania tego typu powinniśmy używać metody equals.

System.out.println("test == test: " + (new String("test") == new String("test")));
System.out.println("test equals test: " + new String("test").equals(new String("test")));

Dlaczego tak się dzieje? Otóż w przypadku obiektów operator == porównuje referencje obiektów (adresy na stercie). Mając dwie różne instancje obiektów mają one dwa różne adresy w pamięci w związku z tym zawsze ich adresy są różne. Innymi słowy w przypadku obiektów przy pomocy operatora == możemy sprawdzić czy dwie referencje wskazują na ten sam obiekt.

String reference1 = new String("something");
String reference2 = reference1;
System.out.println("reference1 == reference2: " + (reference1 == reference2));

Metoda equals

Metoda equals jest jedną z metod dostępnych w klasie Object. W związku z tym, że każdy obiekt w języku Java ma tą klasę w swojej hierarchii dziedziczenia możemy tą metodą wywołać na każdym obiekcie.

W większości przypadków domyślna implementacja metody equals nie jest odpowiednia1 w związku z tym programista tworzący nowy obiekt musi tą metodę zaimplementować jeśli chce sprawdzać czy instancje tej klasy są równe.

Istnieje zestaw wytycznych, które metoda equals powinna spełniać aby była poprawnie zaimplementowana. Opiszę je po kolei:

Metoda equals powinna być zwrotna

Oznacza to tyle, że dla każdego obiektu operacja object.equals(object) powinna zwrócić true.

Metoda equals powinna być symetryczna

Oznacza to tyle, że dla każdej pary obiektów X i Y powinna zachodzić właściwość jeśli X.equals(Y) == true wówczas także Y.equals(X) == true.

Metoda equals powinna być przechodnia

Jeśli mamy trzy obiekty X, Y i Z oraz jeśli X.equals(Y) == true i Y.equals(Z) == true to także X.equals(Z) jest prawdą.

Metoda equals powinna być spójna

Innymi słowy kilkukrotne wywołanie metody equals na tych samych obiektach zawsze powinno zwrócić ten sam wynik (zakładając, że obiekty nie były modyfikowane pomiędzy wywołaniami).

Metoda equals powinna zwrócić false przy porówaniu z null

Dla każdego obiektu X, który nie jest null porównanie typu X.equals(null) powinno zwrócić false.

Załóżmy, że mamy klasę Chair. Możemy powiedzieć, że krzesła są “równe” jeśli zostały wyprodukowane w tym samym roku, przez tego samego producenta oraz są tego samego modelu. Założenia te zostały zaimplementowane poniżej.

public class Chair {
    private String model;
    private String manufacturer;
    private int productionYear;
 
    public Chair(String model, String manufacturer, int productionYear) {
        this.model = model;
        this.manufacturer = manufacturer;
        this.productionYear = productionYear;
    }   
 
    public String getModel() {
        return model;
    }   
 
    public String getManufacturer() {
        return manufacturer;
    }   
 
    public int getProductionYear() {
        return productionYear;
    }
 
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (obj instanceof Chair) {
            Chair otherChair = (Chair) obj;
            return model.equals(otherChair.model) &&
                    manufacturer.equals(otherChair.manufacturer) &&
                    productionYear == otherChair.productionYear;
        }
        return false;
    }
 
    @Override
    public int hashCode() {
        return 17 * model.hashCode() + 31 * manufacturer.hashCode() + 7 * productionYear;
    }
}
 
Chair chair1 = new Chair("Adde", "IKEA", 2016); 
Chair chair2 = new Chair("Janinge", "IKEA", 2016); 
Chair chair3 = new Chair("Adde", "IKEA", 2015); 
Chair chair4 = new Chair("Adde", "IKEA", 2016);
 
System.out.println("chair1.equals(chair2): " + chair1.equals(chair2)); 
System.out.println("chair1.equals(chair3): " + chair1.equals(chair3)); 
System.out.println("chair1.equals(chair4): " + chair1.equals(chair4)); 
System.out.println("chair1.equals(null): " + chair1.equals(null));

Zauważ, że w naszej implementacji metody equals używamy także metody equals z typu String aby sprawdzić czy model i producent są równi.

Nowy może być także operator instanceof, służy on do sprawdzenia czy dana instancja jest typu Chair. Po tym sprawdzeniu możemy bezpiecznie rzutować obiekt obj i mamy pewność, że nie zostanie rzucony wyjątek ClassCastException.

Metoda hashCode

Podobnie jak w przypadku equals hashCode jest zaimplementowane w klasie Object. Zawsze kiedy programista implementuję metodę hashCode powinien też zaimplementować metodęequals.

Metoda ta zwraca liczbę typu int, która służy do przyporządkowania danego obiektu do grupy. Dzięki metodzie hashCode jesteśmy w stanie podzielić wszystkie możliwe instancje danej klasy na rozdzielne grupy. Każda z tych grup reprezentowana jest przez liczbę zwracaną przez metodę hashCode.

hashCode zasada działania.

Obrazowe przyporządkowanie obiektów do grup zostało przedstawione na diagramie powyżej. Koła i trójkąt zostały przyporządkowane do tej samej grupy, rąb i pięciokąt do grupy Hash#2 natomiast trapez został przyporządkowany do grupy Hash#3.

Metoda hashCode wykorzystywana jest przez niektóre kolekcje (tablice na sterydach), o których przeczytasz w jednym z kolejnych artykułów. Implementacja metody hashCode sprowadza się do zwrócenia odpowiedniej liczby, tak zwanego hasha. Przyporządkuje on dany obiekt do grupy używanej w niektórych kolekcjach. Najczęściej metodę hashCode implementuje się w oparciu o hashe atrybutów danej instancji. Hashe atrybutów zazwyczaj mnoży się przez liczby pierwsze i sumuje ze sobą. Użycie liczb pierwszych pomaga w uzyskaniu “dobrych hashy”. Dobra implementacja hashCode pozwala na uzyskanie jak największej liczby grup (hashy), do których przyporządkowujemy obiekty.

Posłużę sie tu klasą Chair wspomianą wyżej. Zakładając, że nasza klasa ma trzy atrybuty i żaden z nich nie może mieć wartości null przykładowa implemetacja może wyglądać następująco:

@Override
public int hashCode() {
    return 17 * model.hashCode() + 31 * manufacturer.hashCode() + 7 * productionYear;
}

W większości przypadków użycie metody Objects.hash przy implementacji metody hashCode jest dobrym pomysłem.

Kontrakt między metodami equals i hashCode

Metody hashCode i equals są ze sobą powiązane i ich implementacja powinna być spójna. Tą zależność określa się kontraktem między metodami hashCode i equals.

  • Jeśli X.equals(Y) == true wówczas wymagane jest aby X.hashCode() == Y.hashCode(),
  • Kilkukrotne wywołanie metody hashCode na tym samym obiekcie, który nie był modyfikowany pomiędzy wywołaniami musi zwrócić tą samą wartość,
  • Jeśli X.hashCode() == Y.hashCode() to nie jest wymagane aby X.equals(Y) == true.

Trzeci przypadek jest ilustrowany na obrazku powyżej gdzie koła i trójkąt mają ten sam hashCode jednak koło i trójkąt nie są równe.

Generatory metod hashCode i equals

Implementacja tych metod w większości przypadków jest dość prosta. W większości z nich także nie jest to kod zbyt skomplikowany. Jednak za każdym razem pisanie tych metod jest uciążliwe. Z pomocą przychodzi IDE. Polecam generowanie tych metod przy jego pomocy. W przypadku InteliJ IDEA pomocny może okazać się skrót klawiaturowy Alt+Insert. Po jego naciśnięciu pokaże się menu kontekstowe pozwalające na wygenerowanie tych metod.

Dodatkowo warto przyjrzeć się klasie Objects i bibliotekom Guava czy Apache commons-lang. Zawierają one metody pomocnicze użyteczne podczas implementacji metod hashCode i equals.

Zadanie

Na koniec krótkie zadanie dla Ciebie. Napisz klasę reprezentującą człowieka, zaimplementuj metody hashCode i equals. Zastanów się czy to, że ktoś ma to samo imię i nazwisko sprawia, że jest to ta sama osoba? Jaki atrybut może posłużyć do sprawdzenia czy dwie instancje klasy Human reprezentują tę samą osobę?

Jeśli będziesz miał problem z rozwiązaniem zadania przykładowe rozwiązanie umieściłem na githubie. Jak zwykle zachęcam do samodzielnych prób, wtedy nauczysz się najwięcej.

Materiały dodatkowe

Kod źródłowy wszystkich przykładów użytych w artykule znajduje się na githubie. Jeśli chcesz poczytać więcej na temat metod equals i hashCode zapraszam do materiałów dodatkowych:

Podsumowanie

Bardzo się cieszę, że dotarłeś do końca artykułu. Mam nadzieję, że był on dla Ciebie ciekawy i przydatny. Na koniec mam do Ciebie prośbę, podziel się artykułem ze swoimi znajomymi, zależy mi na dotarciu do jak największej liczby czytelników. Jeśli nie chcesz przegapić nowych artykułów polub nas na facebooku :) W przypadku jakichkolwiek pytań proszę zdaj je w komentarzach, postaram się odpowiedzieć.

Do następnego razu!

  1. Domyślna implementacja zachowuje się jak operator ==, porównuje adresy obiektów. 

Zostaw komentarz