Porównanie obiektów w Javie || Równość logiczna i metoda equals( )
🛠️

Porównanie obiektów w Javie || Równość logiczna i metoda equals( )

notion image
 

W ostatnim wpisie
1️⃣
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.

 
notion image
 

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.

 
notion image
 

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.

 
notion image
 

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.

 
notion image
 

💥 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 str2ró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.

 
notion image
 

💥 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.

 
notion image
 

💥 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.

 
notion image

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.

 
notion image
 

💥 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.

 
notion image
 

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.

 
notion image

Ś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