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 i 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. 😉