Kurs Java od podstaw Tydzień 4

Porównywanie obieków – czyli metoda equals

Metoda equals

Możliwe, że w ostatnim tygodniu irytowało Cię to, że używałem metody equals – obiecywałem, że do niej wrócimy, a ty wiedziałeś tylko, że służy do porównywania obiektów. Choć jest to prawda, to temat warto rozwinać, aby nie zrobić przypadkiem jakiegoś głupstwa. Weźmy, więc na ruszt metodę equals.

Porównania

Z pierwszego tygodnia kursu wiesz, że do porównywania służy operator == – w takim razie po co nam w ogóle jakaś metoda equals?

W końcu coś takiego działa:

int x = 3;
int y = 5;
if (x == y) {
  //do sth
}

Działa, ponieważ int jest prymitywem!

Czyli mamy pierwsze wnioski: operator == służy do porównywania typów prostych – czyli prymitywnych.

Skoro int jest prymitywem, to w końcu Integer już jest typem obiektowym.

Spróbujmy czegoś takiego:

public class Main {

    public static void main(String[] args) {
      Integer x  = 4;
      Integer y = 4;

      if (x == y) {
          System.out.println("equals");
        }
    }
}

T0 po uruchomieniu otrzymamy napisac equals – to IntelliJ nawet podpowie nam, że coś jest nie tak:

Number Objects are comparing using ==, not equals.

Czyli IntelliJ podpowiada nam, abyśmy użyli metody equals zamiast == na obiektach.

Porównania obiektów

Typy obiektowe są typami złożonymi – czyli zazwyczaj składają się z wielu innych typów obiektowych i prymitywnych. Ich całkowity rozmiar w pamięci jest zmienny – czyli dodając kolejne zmienne maszyna wirtualna Javy zarezerwuje na ten obiekt jeszcze więcej pamięci.

Typ prymitywny jest zapisywany zawsze w tej samej formie zajmującej tyle samo miejsca, ale do czego dążę?

W obiektach wyróżniamy tzw. referencję, czyli adresy do obiektów.

Adresy są to liczby zapisane w systemie szestanstkowym, można to łatwo sprawdzić wywołując metodę toString() z klasy Objects. Podstawowa implementacja – czyli taka, której jeszcze nie przeciążymy (nie nadsłonimy) swoją wyświetla właśnie nazwę klasy i pakiet oraz adres obiektu.

Wpisując taką linijkę:

System.out.println(new Object().toString());

Otrzymamy taki rezultat:

java.lang.Object@7f31245a

Dodając kolejny obiekt taki:

java.lang.Object@6d6f6e28

Czyli każdy obiekt ma swój adres. Adres pierwszego to 7f31245a, a drugiego:6d6f6e28. Do czego, więc dąże?

Porównywanie referencji

Dąże do tego, że porównywanie obiektów operatorem == służy do sprawdzenia czy mają one ten sam adres.

Co wiąże się z tym, że mają ten sam adres? To, że wskazują na ten sam obszar w pamięci – czyli tak naprawdę są tym samym obiektem!

Działania przykładu Integer i String zostawiam na kolejne akapity – są to wyjątkowe przypadki w Javie.

User

Zdefiniujmy klasycznie sobie jakąś klasę pomocniczną – może to być User z trzema polami.

Klasa prosta jak zwykle:

package pl.maniaq;

public class User {
    private Long id;
    private String login;
    private String email;

    public User(Long id, String login, String email) {
        this.id = id;
        this.login = login;
        this.email = email;
    }


    public Long getId() {
        return id;
    }

    public String getLogin() {
        return login;
    }

    public String getEmail() {
        return email;
    }
}

Mając klasę możemy już stworzyć obiekt typu User.

Porównywanie referencji – praktyka

Wróćmy do porównywania obiektów metodą operatora ==.

Stwórzmy sobie trzy obiekty typu User – w tym dwa mające te same wartości.

User user = new User(1l, "Pablo", "admin@example.com");
User user1 =  new User(1l, "Pablo", "admin@example.com");
User user2 = new User(2l, "Admin", "admin@admin.com");

Nazwy obiektów są tylko w celach naukowych – nigdy tak nie rób! 😉

Porównajmy wszystkie te obiekty do siebie:

if (user == user1) {
   System.out.println("user==user1");
}

if (user == user2) {
   System.out.println("user==user2");
}

if (user2 == user1) {
   System.out.println("user2==user1");
}

Uruchommy program i co widzimy?

Nic – czyli żaden obiekt nie jest sobie równy – choć user user1 mają takie same wartości!

Sprawdźmy jeszcze moją tezę: przypiszmy jeden obiekt do drugiego. 😉

User newUser = user;

I porównajmy je:

if (newUser == user) {
  System.out.println("newUser == user");
}

Uruchommy:

newUser == user

I otrzymaliśmy równość? Ponieważ oba obiekty wskazują na ten sam obiekt.

Operator równa się przypisał nam adres obiektu user do newUser – później porównując je wyszło, że obiekty wskazują na ten sam obszar w pamięci!

Szybkie wnioski: operator == służy do porównywana typów prymitywnych oraz sprawdzania czy obiekty wskazują na ten sam obszar w pamięci!

Metoda equals

Zdefiniujmy sobie metodę equals – tak naprawdę musimy ją przysłonić swoją, ponieważ ta metoda zawiera się już w klasie Objects, po której dziedziczą wszystkie obiekty.

Tylko, że jej implementacja sprowadza się do operator ==:

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

Więc sprawdza nam tylko adresy…

Przejdźmy do klasy User i zdefniujmy metodę equals:

@Override
public boolean equals(Object o) {
}

Przysłonięcie będzie tylko poprawne, gdy typem argumentu będzie Object, a nie User!

Musimy teraz zaimplementować porównywanie obiektów User – czyli kiedy uważamy, że są równe.

Moim zdaniem tak będzie, gdy jego wszystkie pola będą sobie równe.

Na początku w ogóle musimy sprawdzić, czy porównywany obiekt nie jest tym samym obiektem:

if (this == o) return true;

Jeżeli wskazują na ten sam adres to zwracamy prawdę – czyli równe.

Dalej musimy sprawdzić czy obiekt nie jestem nullem (pusty) i czy jest typu User.

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

Jeżeli jest to null lub nie jest to klasa User to zwracamy fałsz – czyli nierówne.

Skoro mamy już pewność, że nasz obiekt jest typu User ( z warunku getClass() != o.getClass() ) to możemy go zrzutować na User – w końcu nadal jest on typem Object.

User user = (User) o;

Rzutowanie w dół jest zawsze dozwolone – czyli z User do Object – ponieważ każdy obiekt zawiera w sobie klasę Object.

Rzutowanie w górę musi być przeprowadzone ostrożnie, wszystko musi zostać sprawdzone – czyli z Object do User – ponieważ Object nie musimy być User.

Skoro mamy już przygotowany nasz obiekt i jest wszystko z nim w porządku to czas na porównanie wszystkich pól:

return Objects.equals(id, user.id) &&
        Objects.equals(login, user.login) &&
        Objects.equals(email, user.email);

Porównania pól są wykonane przy użyciu metody Objects.equals – która wymaga dwóch argumentów i zapewnia nam bezpieczne porównanie pól. Korzysta tak naprawdę ona z metody equals. 😉

No i mamy metodę equals gotową:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    User user = (User) o;
    return Objects.equals(id, user.id) &&
            Objects.equals(login, user.login) &&
            Objects.equals(email, user.email);
}

Przejdźmy do naszego maina:

package pl.maniaq;

public class Main {

    public static void main(String[] args) {
      User user = new User(1l, "Pablo", "admin@example.com");
      User user1 =  new User(1l, "Pablo", "admin@example.com");
      User user2 = new User(2l, "Admin", "admin@admin.com");

      if (user == user1) {
      	System.out.println("user==user1");
    }

    if (user == user2) {
      System.out.println("user==user2");
    }

    if (user2 == user1) {
      System.out.println("user2==user1");
    }

    User newUser = user;

      if (newUser == user) {
      	System.out.println("newUser == user");
    }

    }
}

I dodajmy jeszcze jedno porównanie:

if (user.equals(user1)) {
  System.out.println("user equals user 1");
}

I po uruchomieniu otrzymujemy:

user equals user 1
newUser == user

Wyciągnijmy wnioski z tego co zrobiliśmy.

Metoda equals definiuje schemat według, którego mają być porównywane dwa obiekty. Dzięki niej możemy bez problemu porównać dwa pola na podstawie wartości ich pól.

Auto boxing i unboxing

Na czym polega automatyczne pakowanie i rozpakowywanie?

Mówiłem, Ci że Integer jest szczególnym przypadkiem – nie tylko Integer – również: Long, Boolean, Double, Float. Czyli wszystkie typy prymitywne pisane z wielkiej litery są nazywane typami osłonowymi!

Czyli Integer jest osłoną na typ prymitywny int.

Czyli Long jest osłoną na typ prymitywny long.

Itd…

Skoro typy osłonowe są typami obiektowymi – czyli normalnymi obiektami to dlaczego to porównanie:

public class Main {

    public static void main(String[] args) {
      Integer x  = 4;
      Integer y = 4;

      if (x == y) {
          System.out.println("equals");
        }
    }
}

Działało?

No i przechodzimy do pakowania i rozpakowywania.

Wszelkie operację na typach osłonowych czyli Integer, Long, Boolean itd. są wykonywane tak naprawdę na typach prymitywnym.

Przed wykonaniem operacji np. porównaniem dwóch Integer – liczby są najpierw „rozpakowywane” z obiektu i porównywane są już typy prymitywne int.

To samo dzieje się w drugą stronę, gdy np. do zmiennej Integer przypisujemy liczbę:

Integer x = 5;

To choć liczba 5 jest intem – to Java wie o co nam chodzi i opakowuję tą 5 w Integer – czyli w pakuję w pudełko. 😉

Czemu o tym wspominam?

Myślę, że warto mieć pojęcie o automatycznym zachowaniu Javy, ponieważ w łatwiejszy sposób w przyszłości możesz optymalizować – czyli przyśpieszać – swoją aplikację.

Po prostu wykonywanie milion razy pakowania i rozpakowywania też nie jest dobre. 😉

Posłuchajmy, więc rady IntelliJ – do porównywania obiektów używajmy equals – nawet jeżeli jest to typ osłonowy.

Mając, więc w klasie zdefiniowane id o typie osłonowym to porównujmy go przy użyciu metody equals, zamiast operator ==. 😉

Co dają nam typy osłonowe?

Skoro już wspomnieliśmy o typach osłonowych to warto się zastanowić po cholere one są?

Przecież na pierwsz rzut oka Integer i int to to samo.

No nie do końca – różnica jest kolosalna.

Integer jest obiektem, a int nim nie jest.

Gdzie widzimy tą różnicę? Wykorzystując tzw. typy generyczne – o czym powiem, w tym tygodniu.

Do listy nie wstawimy typu prymitywnego np. int:

List<int> numbers = new ArrayList<int>();

To nie zadziała – ponieważ w nawiasach ostrych musi być obiekt.

W tym celu powstał Integer, który nam zapewnia to:

List<Integer> numbers = new ArrayList<Integer>();

Czyli są plusy jego używania. 😉

Podsumowanie

To tyle na temat porównywania obiektów, jeśli jesteś głodny wiedzy to możesz przeczytać jeszcze mój artykuł na temat porównywania obiektów, gdzie bardziej rozwinąłem temat metody equals i hashCode – i kontraktu między nimi.

Wspominałem, że szczególnym przypadkiem jest też String – dlatego o nim wspomnimy w następnej lekcji. 😉

Przydatną wskazówką dla Ciebie może być to, że metody equals (i hashCode) nie pisze się z palca. Te metody zazwyczaj wyglądają tak samo – chcemy sprawdzić wszystkie pola – dlatego są generowane automatycznie np. przez IntelliJ.

Użyj skrótu klawiszowego ALT+INSERT – i wybierz z listy wyboru metody equals i hashCode – zostaną one dla Ciebie wygenerowane automatycznie.

Po co w sumie ten temat? Musisz mieć świadomość jak działają te metody oraz po co w ogóle one istnieją! Musisz też potrafić modyfikować metodę equals dla własnych potrzeb co czasami może się zdarzać!

Zauważ, że używając ALT+INSERT możesz automatycznie wygenerować konstruktor, metodę toString() oraz gettery i settery.  Jeśli nie czujesz, że pisanie klas weszło Ci w krew to nie używaj tych funkcji IntelliJ. Jednak jeżeli czujesz, że wolisz poświęcić czas na pisanie ciekawszych serwisów niż prostszych modeli to używaj skrótu ALT+INSERT. W końcu dążymy do tego, aby kod był dobry, testowany i pisany jak najszybciej. 😉

Ze względu, że to temat bardziej teoretyczne – skupiliśmy się głównie na całej otoczce wokół porównania to po tej lekcji nie ma typowej pracy domowej.

Jednak polecam Ci stworzyć własną klasę, zdefiniować w niej metodę equals – w main stworzyć kilka obiektów tej klasy i porównywać je na wszelakie sposoby. 

Będziesz miał pewność, że wszystko rozumiesz, gdy faktycznie zobaczysz wyniki porównań.

A my z porównaniami jeszcze nie kończymy – widzimy się teraz na porównywaniu Stringów. 😉

Subscribe
Powiadom o
guest
0 komentarzy
Inline Feedbacks
View all comments