Porównania typów prostych oraz obiektó

Na początek…

W tym wpisie postaram Ci się wytłumaczyć odpowiedź na często spotykane pytanie rekrutacyjne “Jakie są różnicę między equals, a operatorem ==?”. Z tego co słyszę oraz czytam w internecie często to pytanie pojawia się na rozmowach rekrutacyjnych o pierwszą pracę jako developer i zdarza się, że osoby rekrutowane nie znają na nie odpowiedzi.

Świadomość

Wydaje mi się, że osoby, które przesiadają się z innych języków na jave (np. z C++) mogą zapominać o znaczeniu metody equals i z rozpędu użyć operatora ==. Kod w końcu nadal się kompiluje, ale aplikacja nie działa poprawnie. Pamiętam, nawet jak na początku swojej przygody z tym językiem pisałem większą aplikację i okazało się, że coś do końca nie działa po mojej myśli…

Okazało się, że zapomniałem zdefiniować metody equals dla swojej klasy co wiązało się z brakiem poprawnego działania aplikacji. Jeżeli nie wiesz jeszcze o czym mówię to się nie martw – w tym wpisię postaram Ci się bardzo dobrze przybliżyć temat stosowania equals oraz operatora == na przykładach i super rysunkach.

Operator ==

Rozpocznijmy część techniczną tego wpisu od omówienia operator == – od samych banałów, które możliwie, że poznałeś na jednej z pierwszych lekcji podstawowego kursu java.

Porównywanie typów prostych


Porównywanie podstawowych typów

Tak jak obiecywałem, zaczniemy od banału. Operator == z pewnością wykorzystywałeś do porównywania typów prostych. Prostych – to znaczy najbardziej podstawowych wbudowanych w jave, są to np.:

  • int
  • bool
  • double
  • float
  • short
  • byte

Dzięki temu operatorowi z łatwością możemy sprawdzać jakie liczby otrzymujemy od użytkownika naszej aplikacji lub odpowiednio reagować na liczby jakie zwraca nam jakaś użyta funkcja. Wydaję mi się, że szkoda więcej tłumaczyć to zagadnienie, a najlepiej będzie przejść do prostego przykładu użycia tego operatora.

Zasymulujmy sobie, że dane będą wpisywane przez użytkownika – z klawiatury. Do tego przyda nam się np. Scanner.

Scanner scanner = new Scanner(System.in);

I nie zapominamy o imporcie klasy.

import java.util.Scanner;

Teraz możemy dodać zmienną, do której będą wpisywane wartości z klawiatury – przyjmijmy, że będą to liczby całkowite.

int number = scanner.nextInt();

Przypuśćmy, że chcemy teraz odpowiedzieć użytkownikowi czy podana liczba jest parzysta czy nieparzysta. W tym celu możemy użyć operatora modulo w ten sposób:

if(number%2==0){
    //parzysta
}else{
    //nieparzysta
}

Ewentualnie możemy też użyć operacji na bitach, które będzie szybsze od modulo ze względu, że procesor nie musi wykonywać dzielenia.

if((number & 0x1) == 0){
    //parzysta
}else{
    //nieparzysta
}

Dla osób, które nie wiedzą co się dzieje krótkie wytłumaczenie. Na liczbie, którą otrzymujemy od użytkownika wykonujemy operator bitowy AND (&) z liczbą 0x1 – co oznacza jedynkę w systemie hexa (jak i dziesiętnym). Dzięki takiej operacji, wszystkie bity oprócz najmłodszego są wyzerowane, a my sprawdzamy czy otrzymana liczba jest 0 lub 1. Jeżeli jest 1 to oznacza, że liczba podana przez użytkownika jest nieparzysta, ponieważ najmłodszy bit w systemie dwójkowym symbolizuje 1, więc otrzymujemy zawsze liczbę nieparzystą.

Dodajmy jeszcze println, aby wyświetlić informację użytkownikowi na ekran konsoli. Nasz cały kod wygląda teraz:

package pl.maniaq;

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int number = scanner.nextInt();

        if((number & 0x1) == 0){
            System.out.println("Podana liczba jest parzysta.");
        }else{
            System.out.println("Podana liczba jest nieparzysta.");
        }

    }
}

I w taki oto sposób przypomnieliśmy porównywanie typów prostych za pomocą operatora == na przykładzie liczb całkowitych. Przejdźmy teraz do kolejnego zastosowania operatora ==.

Porównywanie obiektów

Porównywanie referencji

Operator == możemy również zastosować do porównywania obiektów. Stwórzmy, więc sobie na początek prostą klasę o nazwie Person, która będzie przechowywała imię i nazwisko osoby.

package pl.maniaq;

public class Person {

    private String name;
    private String lastname;

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

    public String getName(){
        return name;
    }

    public String getLastname(){
        return lastname;
    }

}

Mamy dwa pola na imię i nazwisko, konstruktor parametrowy i dwa gety – to nam wystarczy. W klasie main stwórzmy sobie kilka obiektów typu Person.

Person pawel = new Person("Pablo", "Escop");
Person johny = new Person("Johny", "Bravo");
Person rambo = new Person("John", "Rambo");
Person pablo = new Person("Pablo", "Escop");

Stworzyliśmy sobie cztery obiekty, dwa mają takie same wartości pola – no to chyba są równe? Użyjmy teraz kilku operatorów warunkowych do sprawdzenia czy obiekty są sobie równe. Nasza klasa main wygląda tak:

package pl.maniaq;

import pl.maniaq.Person;

public class Main {

    public static void main(String[] args) {
        Person pawel = new Person("Pablo", "Escop");
        Person johny = new Person("Johny", "Bravo");
        Person rambo = new Person("John", "Rambo");
        Person pablo = new Person("Pablo", "Escop");

        if(pablo == johny){
            System.out.println("Pablo i johny to to samo.");
        }

        if(rambo == johny){
            System.out.println("Rambo i johny to to samo");
        }

        if(pablo == pawel){
            System.out.println("Pawel i pablo to to samo");
        }

    }
}

Po uruchomieniu programu nasza konsola jest pusta, czemu się tak dzieje? Na to pytanie odpowiem za chwilę, stwórzmy sobie teraz małą zmianę w kodzie, nasz main wygląda teraz tak:

package pl.maniaq;

import pl.maniaq.Person;

public class Main {

    public static void main(String[] args) {
        Person pawel = new Person("Pablo", "Escop");
        Person johny = new Person("Johny", "Bravo");
        Person rambo = new Person("John", "Rambo");
        Person pablo = pawel;

        if(pablo == johny){
            System.out.println("Pablo i johny to to samo.");
        }

        if(rambo == johny){
            System.out.println("Rambo i johny to to samo");
        }

        if(pablo == pawel){
            System.out.println("Pawel i pablo to to samo");
        }

    }
}

Po uruchomieniu programu nasza konsola tym razem pokazuje coś takiego:

Pawel i pablo to to samo

Nasuwają się już wnioski? Jeżeli tak to super, jeżeli nie to i tak pędze z wyjaśnieniami.

Operator == podczas porównywania obiektów, nie porównuje tak naprawdę obiektów, tylko ich referencję. Co możemy rozumieć przez referencję? Jest to po prostu adres w pamięci, w drugim przykładzie obiekt pablo i pawel wskazują na ten sam obszar w pamięci. I to właśnie sprawdza operator == dając mu dwa obiekty.

Gdzie to się przydaje…

Są przypadki, w których faktycznie porównywanie referencji się przydaje. Stosuje się je w metodach, które faktycznie porównują zawartość obiektów. Po prostu na początku takiej funkcji warto sprawdzić czy obiekt przekazany do funkcji nie wskazuje na ten sam obszar pamięci co obiekt, do którego będziemy go porównywać – zaoszczędza nam to po prostu złożoność obliczeniową aplikacji.

public boolean isEqual(Object obj){
    if(this==obj){
        return true;
    }
    
    //comparing
    
    return false;
}

Proste działanie, dwa obiekty wskazują na ten sam obszar pamięci? No to w takim razie muszą być równe!

Wnioski

Wnioski są proste: stosuj operator == do porównywania typów prostych oraz sprawdzania czy dwa obiekty wskazują na ten sam obszar pamięci, lecz nie wykorzystuj go do sprawdzania czy dwa obiekty są sobie równe (ich pola są odpowiednio równe)!

equals

Porównywanie obiektów przy pomocy equals

Przejdźmy teraz od wyjaśnienia do czego służy metoda equals – możliwe, że po kilku powyższych przykładach się już domyślasz. Często w kodzie jest wykorzystywane porównywanie obiektów –  np. wstawiając obiekt do kolekcji – jeżeli nie wiesz jeszcze czym są kolekcje lub czujesz, że nie do końca wszystko rozumiesz to zapraszam Cię do wpisu o kolekcjach.

Wiemy, że użycie operator == nie jest wystarczające, gdy chcemy brać pod uwagę stany pól obiektów – w takim przypadku przychodzimy nam z pomocą metoda equals. Przejdźmy do implementacji metody equals w naszej klasie Person. Jej sygnatura wygląda tak:

@Override
public boolean equals(Object o) {
  //code here
}

Dzięki adnotacji Override mamy pewność, że będziemy przysłaniać metodę z obiektu Object – nawet jeżeli pomylimy się np. w typie argumenty to kompilator nam o tym przypomni. Przy poniższym kodzie kompilator nie poinformuje nas o błędzie:

public boolean equals(Person o) {
  //code here
}

Zaś w tym przypadku już tak:

@Override
public boolean equals(Person o) {
  //code here
}

Warto, więc pamiętać o stosowaniu adnotacji, aby nie wystąpiły trudne do znalezienia błędy w kodzie.

Skoro wiemy już jak wygląda sygnatury metody equals to przejdźmy do jej implementacji. Na początek warto będzie sprawdzić czy nie porównujemy tych samych obiektów przy pomocy operatora ==.

if(this == o) return true;

Kolejno musimy sprawdzić czy przypadkiem jako argument nie został podany null.

if(o==null) return false;

Musimy się też upewnić czy, aby na pewno podany obiekt jest instancją klasy Person, dodajmy więc kolejny warunek do poprzedniej instrukcji warunkowej if.

if (o == null || getClass() != o.getClass()) return false;

Dzięki temu zabezpieczeniu możemy z czystym sumieniem zrobić rzutowanie z Object na Person.

Person person = (Person) o;

I już zostaje nam tylko porównywanie pól klasy – wszystkich tych, na których nam zależy. W naszym przypadku będzie sprawdzać wszystkie pola – imię oraz nazwisko.

return name.equals(person.name) && lastname.equals(person.getLastname());

Jeżeli, więc te dwa pola będą sobie równe to według naszej logiki te dwa obiekty będą sobie równe. Cały kod metody equals wygląda tak:

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

Oczywiście taki kod można również wygenerować w IDE – czy to będzie IntelliJ czy Eclipse to bez różnicy. Jednak warto wiedzieć co się dzieje w tej metodzie, do czego służy oraz jak można edytować na własne potrzeby.

Mając już zdefiniowanie poprawnie metodą equals przejdźmy do krótkiego przykładu wykorzystania klasy Person.

package pl.maniaq;

public class Main {

    public static void main(String[] args) {
        Person pawel = new Person("Pablo", "Escop");
        Person johny = new Person("Johny", "Bravo");
        Person rambo = new Person("John", "Rambo");
        Person pablo = new Person("Pablo", "Escop");

        if(pablo.equals(johny)){
            System.out.println("Pablo i johny to to samo.");
        }

        if(rambo.equals(johny)){
            System.out.println("Rambo i johny to to samo");
        }

        if(pablo.equals(pawel)){
            System.out.println("Pawel i pablo to to samo");
        }


    }

}

A rezultat działania takiego programu wygląda tak:

Pawel i pablo to to samo

Jak widać metoda equals działa poprawnie.

hashCode

Czas na poruszenie tematu metody hashCode. Obowiązkowo trzeba poruszyć ten temat mówiąc o equals – nie bez powodu się mówi o kontrakcie między hashCode oraz equals.

Na czym ten kontrakt polega…

Jak wiadomo wszyscy chcemy, aby nasze aplikacje, algorytmy działały jak najszybciej to możliwe – tak samo chcą i twórcy javy, dlatego podczas porównywania obiektów wprowadzili pewien myk – są to hashe.

Czym jest hash?

Hash jest ukazywany jako liczba całkowita, która powstaje po ustalonych obliczeniach na polach klasy. Funkcji mieszających (hashujących) jest wiele – z pewnością najpopularniejszą jest funkcja modulo, lecz w przypadku hashCode nie będziemy z niej korzystać.

Co daje nam hash?

Odpowiedź jest prosta – porównywanie hashy( liczb całkowitych) jest po prostu dużo szybsze. Dla komputera jest łatwiej porównać dwie liczby całkowite – co zrobi w jednym takcie zegara – aniżeli porównywać wszystkie pola.

Jednak jest jedna wada hashu – hashe nie są unikalne. Oznacza to, że równość dwóch hashy niekoniecznie oznacza równość dwóch obiektów – wtedy musimy sprawdzać równości danych pól.

Trzy zasady kontraktu…

  1. Metoda hashCode dla tych samych wartości pól zawsze powinna zwracać tą samą wartość.
  2. Jeżeli dwa obiekty są sobie równe to ich hashe powinny być takie same.
  3. Dwa różne obiekty mogą mieć ten sam hash.

Jak jest tworzony hash?

Cykl tworzenia hashu jest dosyć prosty:

  1. Weź pewną liczbę – w książce Effective Java jest to narzucona liczba 17.
  2. Pomnóż aktualny hash przez 31
  3. Dodaj do hashu wartość/hash pola w klasie
  4. Krok drugi i trzeci wykonaj dla każdego pola
  5. Zwróć hash

W Effective Java została również narzucona liczba 31 nazwana odd prime – czyli dziwną liczbą pierwszą (taki urok liczb pierwszych).

Przekujmy teraz nasz pseudokod tworzenia hashu dla naszej klasy, zacznijmy od sygnatury metody nie zapominając o adnotacji Override.

@Override
public int hashCode() {
  //calculate hash here
}

Na początku wybierzmy pewną liczbę:

int result = 17;

I wykonajmy drugi i trzeci krok dla wszystkich pól w klasie:

result = 31 * result + name.hashCode();
result = 31 * result + lastname.hashCode();

Na koniec oczywiście zwracamy nasz utworzony hash:

return result;

I tak teraz wygląda nasza metoda hashCode:

@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + name.hashCode();
    result = 31 * result + lastname.hashCode();
    return result;
}

Jeżeli korzystamy z Javy 7+  możemy skorzystać z gotowej metody hash w klasie Objects, która za nas obliczy hash dla naszych pól:

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

Robi ona to w ten sam sposób jak my to zrobiliśmy powyżej:

public static int hashCode(Object a[]) {
    if (a == null)
        return 0;

    int result = 1;

    for (Object element : a)
        result = 31 * result + (element == null ? 0 : element.hashCode());

    return result;
}

Czy jest to istotne?

Jest to bardzo istotne w sytuacji, gdy korzystamy z kolekcji z hashami – czyli dosyć często. W tych kolekcjach wszystkie elementy są grupowane dzięki hashom, więc jeżeli elementy w kolekcjach nie będą pogrupowane to złożoność obliczeniowa podstawowych operacji może drastycznie wzrosnąć – a tego oczywiście nie chcemy.

Podsumowanie

Przedstawiłem Ci możliwe użycia operatora == oraz metody equals do porównywania obiektu w javie – możliwe, że jakieś zagadnienie pominąłem – jeżeli tak się stało to czekam na przypomnienie o tym w komentarzu. Mam nadzieję, że temat jest dla Ciebie zrozumiały – a wszystko z pewnością się okażę kiedy rozwiążesz zadania, które przygotowałem dla Ciebie.

  1. Napisz klasę Student, w której będą takie pola jak ID, name oraz lastname. Do klasy dopisz metody hashCode oraz equals.
  2. Napisz klasę University, w które będzie pole typu HashSet, a w tym zbiorze będą przechowywani studenci. Dodaj metodę umożliwiającą dodawanie oraz usuwanie studentów tego zbioru studentów. Podczas dodawania studentów spróbuj dodać dwa osobne obiekty o tych samych polach wartości – sprawdź później czy dodanie studenta się powiodło.

W przypadku problemów z kolekcją Set zachęcam do lektury na temat kolekcji w javie, gdzie wszystko zostało wyjaśnione.

Kamil Klimek

Od 2016 jestem programistą Java. Przez pierwsze 4 lata pracowałem jako Full Stack Java Developer. Później postanowiłem postawić nacisk na Javę, żeby jeszcze lepiej ją poznać.

Subscribe
Powiadom o
guest
5 komentarzy
najstarszy
najnowszy oceniany
Inline Feedbacks
View all comments
Łukasz Strzałkowski
Łukasz Strzałkowski
5 lat temu

Cześć, rozwiniesz proszę ten sposób żerowania ostatniego bitu przy użyciu Hex 1?

Kamil Klimek
Kamil Klimek
5 lat temu

Cześć, jasne – chociaż ten sposób polega na zerowaniu wszystkich bitów oprócz pierwszego.

1. Liczba 7 zapisana binarnie to: 0111
2. Operacja AND (iloczyn) wyrzuca prawdę tylko wtedy, gdy A i B są prawdą
3. Aby wyzerować wszystkie bity oprócz pierwszego można właśnie użyć operator AND i jedynki
4. Można to zapisać w ten sposób w postaci dziesiętnej: 1 & 7 – lub w postaci binarnej: 0111 & 0001
5. Wykonując AND na tych liczbach otrzymujemy 0001 = 1 w postaci dziesiętnej

Wniosek jest taki, że liczba nieparzysta ma zawsze najmłodszy bit równy 1 – dlatego chcemy wyzerować wszystkie bity oprócz pierwszego i zobaczyć czy się (nie)równa jedynce.

Mam nadzieję, że wszystko jest jasne. Sprawdź sobie kilka liczb parzystych i nieparzystych, a wszystko będzie jasne. 😉

Łukasz Strzałkowski
Łukasz Strzałkowski
5 lat temu
Reply to  Kamil Klimek

Dziękuję Kamil, bardzo jasno to wytłumaczyłeś. Ciekawy sposób zamiast modulo. Podrążę jeszcze jeśli pozwolisz :). Jak realnie szybsza jest ta operacja dla komputera? Dlaczego operacja na bitach jest szybsza od dzielenia?Czy obydwie nie są wykonywane w jednym takcie procesora i nie maja O(1)? Czy w tym miejscu nie powinieneś napisać nieparzysta? ‘(…)Jeżeli jest 1 to oznacza, że liczba podana przez użytkownika jest parzysta(…)’. Trzymam kciuki za projekt. Bardzo przystępnie i ciekawie piszesz!

Łukasz Strzałkowski
Łukasz Strzałkowski
5 lat temu

A jeszcze jedno, bo zatrzymałem się na chwilę w tym miejscu wczoraj: int hash = 7; Czy tutaj zmienną nie powinna się nazywać result?

Kamil Klimek
Kamil Klimek
5 lat temu

Zgadza się, jeżeli najmłodszy bit jest jedynką to liczba jest nieparzysta. Wystąpił mały konflkt – w kodzie jest wszystko ok, zaś w tekście namieszałem.

Tak zgadza się, powinno być int result = 7, zamiast hash.

Co do dzielenia, dzielenie jest wykonywane dłużej, ponieważ jest wykonywane na podobnej zasadzie tak jak my ludzie wykonujemy to działanie na kartce papieru. Jest to na tyle skomplikowane działanie bo procesor musi dobierać odpowiednie liczby podczas działania algorytmu – tak jak my dobieramy liczby, aby się “mieściły” w drugiej liczbie.

Operacja na bitach jest szybsza, ponieważ procesor nie musi dopasowywać liczb, po prostu wykonuje takie działanie w jednym takcie procesora. Jeżeli procesor jest 32 bitowy, ale działanie jest wykonywane na 64 bitowej zmiennej to taka akcja zajmie mu wtedy dwa takty procesora – choć teraz może to nie jest tak istotne, gdy rzadko spotyka się 32 bitowe procesory to jeszcze kilka/kilkanaście lat temu miałe spore znaczenie w szybkości działania aplikacji.

Dzięki wielkie za wychwycenie błędów oraz za miłe słowa. 😉

5
0
Would love your thoughts, please comment.x