W ostatnim wpisie Porównanie obiektów w Javie || Równość referencyjna
analizowalłem obiekty pod kątem ich adresów w pamięci.
W Javie operator ==
porównuje referencje obiektów, jednak nie ich zawartość. Sprawdza, czy owe referencje wskazują dokładnie na ten sam obszar pamięci. Jeśli dwa obiekty mają te same adresy referencyjne (są identyczne pod względem referencji), to operator ==
zwróci true
; w przeciwnym razie zwróci false
.
Skoro tu jesteś, wnioskuję, że temat porównywania obiektów może sprawiać Ci pewne trudności. Bez obaw, czasami mam podobne odczucia 😄. Jednakże, głównym celem tego bloga jest stworzenie przestrzeni do nauki. Poprzez zanurzenie się w temacie, staram się nie tylko zrozumieć go lepiej, ale także dostarczyć wartościową wiedzę dla Ciebie.
Równość logiczna
Równość logiczna
, znana również jako równość semantyczna, odnosi się do porównywania zawartości dwóch obiektów, niezależnie od tego, czy są to te same obiekty w pamięci (czy mają tę samą referencję). Równość logiczna
jest bardziej skomplikowaną koncepcją niż równość referencyjna
, ponieważ wymaga od nas zdefiniowania, co oznacza, że dwa obiekty są "równe" na podstawie ich stanu, a nie fizycznej lokalizacji w pamięci.
Generalnie, równość logiczna
jest bardziej elastyczna niż równość referencyjna
, ale wymaga od programisty jasnego określenia, co oznacza "równość" w danym kontekście aplikacyjnym. W Javie, w klasie Object
znajdują się dwie metody, które grają istotną rolę w kontekście równości logicznej
.
Mowa tutaj o equals()
i hashCode()
. To właśnie przesłonięcie tych metod jest często używane do zdefiniowania równości logicznej
dla obiektów. Zaraz pokażemy to na przykładach.
Metoda equals()
Domyślna implementacja metody equals()
w klasie Object
porównuje referencje obiektów:
public class Object { // ... public boolean equals(Object obj) { return (this == obj); } // ... }
Innymi słowy, domyślna implementacja equals()
sprawdza, czy dwa obiekty są identyczne, tzn. czy są to dokładnie te same obiekty w pamięci. Działa to tak samo, jak operator ==
. Metoda equals()
zwraca true
tylko wtedy, gdy obie referencje odnoszą się do tego samego obiektu.
Jednak w wielu przypadkach konieczne jest dostosowanie implementacji metody equals()
do własnych potrzeb, tak aby porównywała wartości obiektów, a nie tylko ich referencje.
Po co przysłaniać metodę equals()?
W Javie równość logiczna
jest określana poprzez przesłonięcie metody equals()
w danej klasie. Dla większości klas biblioteki standardowej (np. String, Integer) metoda equals()
została już w nich odpowiednio przesłonięta w taki sposób, aby porównywać zawartość obiektów, a nie ich referencje.
Nasza własna implementacja metody equals()
powinna być dostosowana do konkretnego typu obiektów i określać, kiedy dwa obiekty są uważane za równe
w sensie logicznym. Zaraz wytłumaczymy to sobie na przykładach.
Jakie obiekty porównywać?
Generalnie, nie zaleca się porównywania obiektów dwóch różnych typów w Javie. Istnieje kilka powodów dla których to podejście może być problematyczne:
1.
Bezpieczeństwo typów:
Porównywanie obiektów różnych typów łamie zasadę bezpieczeństwa typów, która jest fundamentem języków programowania obiektowego. Typy obiektów powinny być kompatybilne w kontekście porównań.
2.
Dziedziczenie:
Porównywanie obiektów różnych klas może prowadzić do problemów z dziedziczeniem. Jeśli jedna klasa dziedziczy po drugiej, a obie klasy mają różne implementacje metody equals()
, to może to prowadzić do niejednoznaczności i błędów.
3.
Brak sensu:
Obiekty różnych klas mogą reprezentować różne aspekty systemu, dlatego ich porównywanie może być nieintuicyjne i nieprzewidywalne. W praktyce porównywanie takich obiektów nie ma często sensu z punktu widzenia logiki aplikacji.
💥 Przykład 1: Obiekty typu String
String str1 = "rabbit"; String str2 = new String("rabbit"); if (str1.equals(str2)) System.out.println("str1 i str2 są równe logicznie."); else System.out.println("str1 i str2 nie są równe logicznie.");
W klasie Object
, metoda equals()
domyślnie porównuje referencje obiektów, co oznacza, że dwa obiekty są uważane za równe tylko wtedy, gdy wskazują na dokładnie ten sam obiekt w pamięci. Jednak klasa String
zmienia to zachowanie, aby zapewnić porównywanie zawartości ciągów znaków.
Mimo że zmienne str1
i str2
wskazują na różne obiekty w pamięci, metoda equals()
wskaże, że str1
i str2
są równe logicznie
.
W klasie String
metoda equals()
(dziedziczona z klasy Object
) została domyślnie przesłonięta w taki sposób, że porównuje zawartości tekstowej dwóch obiektów String
, a nie referencje.
String str1 = new String("hello"); String str2 = new String("hello"); String str3 = new String("world"); // Porównanie referencji System.out.println(str1 == str2); // false, różne referencje obiektów System.out.println(str1.equals(str2)); // true, równość logiczna (porównuje zawartość) // Inny ciąg znaków System.out.println(str1.equals(str3)); // false, różne ciągi znaków
W przypadku obiektów klasy String
zaleca się używanie metody equals()
do porównywania zawartości ciągów, ponieważ metoda jest już przesłonięta i działa zgodnie z oczekiwaniami w kontekście porównywania ciągów znaków.
💥 Przykład 2: Obiekty typu Cat
public class Cat { String sound; public Cat(String sound) { this.sound = sound; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Cat otherCat = (Cat) obj; return sound.equals(otherCat.sound); } public static void main(String[] args) { Cat cat1 = new Cat("Miau"); Cat cat2 = new Cat("Miau"); Cat cat3 = cat1; System.out.println(cat1 == cat2); // false, różne referencje obiektów System.out.println(cat1 == cat3); // true, ta sama referencja obiektu System.out.println(cat1.equals(cat2)); // true, porównanie logiczne } }
W powyższym kodzie metoda equals()
została przesłonięta, w taki sposób, aby porównywać wartość sound
dwóch obiektów klasy Cat
. Poniżej opis tego, co dzieje się podczas przesłonięcia metody equals()
:
@Override public boolean equals(Object obj) { if (this == obj) return true; /* Jeśli this (aktualny obiekt) i obj (obiekt porównywany) wskazują na ten sam obszar pamięci, zwracane jest true */ if (obj == null || getClass() != obj.getClass()) return false; /* Jeśli obj (obiekt porównywany) jest nullem lub nie należy do tej samej klasy co this (obiekt porównywany) zwracane jest false */ Cat otherCat = (Cat) obj; // rzutownie obj na obiekt klasy Cat return sound.equals(otherCat.sound); /* Porównanie wartości sound aktualnego obiektu (this) z sound obiektu otherCat (rzutowanego z obj) i zwrócenie wyniku porównania zawartości sound */ }
W rezultacie cat1.equals(cat2)
zwróci teraz true
, ponieważ oba obiekty klasy Cat
wydają sam głos.
💥 Przykład 3: Obiekty typu Employee
W tym przykładzie porównamy obiekty klasy Employee
, w których zastosujemy przesłonięcie metody equals()
:
public class Employee { private String name; private int employeeId; public Employee(String name, int employeeId) { this.name = name; this.employeeId = employeeId; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } Employee employee = (Employee) obj; return employeeId == employee.employeeId && name.equals(employee.name); } }
Teraz porównamy obiekty. Zwróć szczególną uwagę na wartości zmiennych:
public class Main { public static void main(String[] args) { Employee employee1 = new Employee("John", 555); Employee employee2 = new Employee("John", 123); // Porównanie logiczne za pomocą metody equals() if (employee1.equals(employee2)) { System.out.println("true"); } else { System.out.println("false"); } } }
W tym przypadku, wynikiem porównania tych obiektów będzie false
, ponieważ wartości pola employeeId
są różne (mimo, że name
są takie same). Aby te dwa obiekty były uważane za równe logicznie, musielibyśmy dostosować implementację metody equals()
w taki sposób, aby porównywała wartości tylko tych pól, które są istotne dla porównania, np. name
.
Spróbujmy zatem zmodyfikować implementację metody equals()
, aby porównywała tylko wartości pól name
. W tym przypadku, gdy obiekty mają takie same imię, zostaną uznane za równe logicznie.
public class Employee { //... @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } Employee employee = (Employee) obj; return name.equals(employee.name); } }
public class Main { public static void main(String[] args) { Employee employee1 = new Employee("John", 555); Employee employee2 = new Employee("John", 123); // Porównanie logiczne za pomocą metody equals() if (employee1.equals(employee2)) { System.out.println("true"); } else { System.out.println("false."); } }
Zostosowaliśmy odmienną implementację metody equals()
, która wskazuje, że obiekty będą równe pod względem logicznym (porównanie zwróci true
), kiedy wartości pola name
będą takie same.
💥 Przykład 4: Obiekty w ArrayList
Tutaj metoda equals()
porównuje zawartość dwóch list.
import java.util.ArrayList; import java.util.List; public class ListEqualsExample { public static void main(String[] args) { List<String> list1 = new ArrayList<>(); list1.add("apple"); list1.add("pear"); List<String> list2 = new ArrayList<>(); list2.add("apple"); list2.add("pear"); System.out.println(list1.equals(list2)); // true, porównanie logiczne }
Mamy dwie listy list1
i list2
z identycznymi elementami apple
oraz pear
w tej samej kolejności.
W tym przypadku używana jest domyślna implementacja metody equals()
z klasy AbstractList
, która następnie korzysta z implementacji z klasy AbstractCollection
. To dla mnie jeszcze obce pojęcia, więc nie będę udawał, że wiem, co to za klasy 😄 Teraz to mało istotne.
Musimy wiedzieć tylko, że dla listy ArrayList
domyślna implementacja equals()
sprawdza, czy obie listy zawierają takie same elementy w tej samej kolejności.
Obie listy list1
i list2
zawierają te same elementy (apple
i pear
) w tej samej kolejności, więc wywołanie list1.equals(list2)
zwróci true
.
Podsumowanie
Zarówno operator ==
jak i metoda equals()
(bez przesłonięcia) porównują referencje obiektów.
Równość referencyjna
sprawdza, czy dwie referencje wskazują na ten sam obiekt w pamięci, podczas gdy równość logiczna
, za pośrednictwem metody equals()
, umożliwia bardziej elastyczne porównywanie zawartości obiektów zgodnie z logiką zdefiniowaną w danej klasie. W praktyce warto często korzystać z equals()
do porównywania zawartości obiektów, szczególnie w przypadku klas, które tą metodę przesłaniają dla bardziej intuicyjnego zachowania.
Gdy przesłaniasz metodę equals()
, zazwyczaj dobrą praktyką jest również przesłanianie tajemniczej metody hashCode()
. Ale tym zajmiemy się już w następnym wpisie.
Śledź mnie na LinkedIn
Zapisz się na mój newsletter:
Popełnianie błędów jest rzeczą naturalną zanim perfekcyjnie opanujemy nowy materiał. Jeśli wyłapałeś jakieś nieprawidłowości w moim tekście, proszę daj mi znać mailowo. Jeśli masz jakieś sugestie lub pytania, proszę napisz do mnie wiadomość: kuba@javampokaze.pl