Metoda "equals" w języku Java jest bardzo ważnym elementem w ustalaniu kiedy kolekcja typu zbiór ma uznać dwa różne obiekty za "równe sobie". Przechodzimy do wyjaśnienia definiowania "równości" obiektów w Javie, więc każdego adepta tego języka zapraszam do środka ➡️!

JAK "EQUALS" W JĘZYKU JAVA MOŻE POMÓC NAM PRZY USTALANIU IDENTYCZNOŚCI OBIEKTÓW?

Zagadnienie to ma ścisły związek z metodą zwracającą kod skrótu obiektu ("hashCode"), także zanim zaczniesz to czytać, to zerknij proszę w pierwszej kolejności na tamten materiał ⚠️.

Trochę dużo teorii

Najsampierw musimy rozumieć na czym polega definiowanie identyczności obiektów. Kiedy w tworzonym przez nas programie, musimy zapewnić aby te same elementy nie dodawały się ponownie do jakiegoś zbioru, to zazwyczaj nie wystarczy opieranie się na pojedynczej składowej.

Rozpatrzmy taki przypadek. Jeżeli masz dwa łańcuchy:

var stringA = "Mój łańcuch znaków";
var stringB = "Mój łańcuch znaków";

i porównasz je metodą "equals":

var stringsComparisonResult = stringA.equals(stringB);

to wynik będzie prawdziwy. Dlaczego? Bo łańcuchy zostały uznane za równe (takie same), dlatego że każdy znak jest identyczny ✅. Łańcuchy, tak jak reszta wbudowanych typów danych, już posiada implementację kiedy uznać, że dwie różne instancje tego samego typu są sobie równe (a co za tym idzie, został wykryty duplikat i nie dojdzie do dodania tegoż elementu do zbioru 💡).

Tak na marginesie, możesz pominąć wstawianie konkretnego typu zmiennej używając hasła "var", czyli niejawnej typizacji zmiennej lokalnej ℹ️.

Problem robi się w chwili, gdy nasza implementacja opiera się na jakiejś klasie, która posiada wiele własnych składowych i chcemy oprzeć się na którejkolwiek z nich (albo na kilku!). Właśnie wtedy musimy przesłonić metodę "equals" w języku Java.

Jeżeli nasza zmienna typu "HashSet" (albo dowolna inna kolekcja należąca do typu "zbiór") ma rzeczywiście niezawodnie identyfikować identyczność wprowadzanych elementów, nie wystarczy przesłonięcie metody "hashCode". Oprócz wymienionej, dochodzi jeszcze druga o nazwie "equals" 💥.

Obie metody należą do klasy "Object", czyli do "matki wszystkich obiektów", gdyż od niej pochodzi każda inna klasa. Zatem, pierwszym krokiem na udane weryfikowanie "równości" obiektów jest jawne wprowadzenie obu przesłonięć metod do klasy, która ma posiadać naszą definicję kryterium identyczności.

Przykład kodu źródłowego

Oto domyślna struktura metody "equals" w języku Java 👇:

@Override
public boolean equals(Object obj) {
	return super.equals(obj);
}

Zauważ, że metoda zwraca wartość logiczną "boolean". To oznacza, że podstawiamy wynik do instrukcji warunkowych bądź do zmiennych typu logicznego. "super" odnosi się do bazowej "wersji" metody (więcej szczegółów w osobnym materiale).

Zbadajmy przykład wstawiania do programu książek, bo na przykład chcemy zrobić ogólnodostępny spis literatury jaka istnieje na świecie. Nie powinniśmy wtedy dopuszczać do sytuacji, w której dwa razy trafi się ten sam wpis (jakie konkretnie będą to wartości, to za chwilę). Zakładamy, że mamy klasę "Book" z wieloma danymi składowymi i jesteśmy zmuszeni określić w jaki sposób filtrować obiekty jej typu.

Zadaję pytanie: jak my, jako programiści, możemy zaimplementować zabezpieczenie przed wstawianiem zduplikowanych wpisów do zbioru używając metod "hashCode" oraz "equals"?

Najpierw musimy ustalić na podstawie czego decydować o dopuszczeniu obiektu do zbioru 1️⃣. W pierwszej chwili może nasuwać się tytuł, jednak dużo lepiej będzie opierać się na ISBN, czyli międzynarodowym znormalizowanym numerze książki, ponieważ on nigdy się nie powtórzy - jest jeden na daną książkę.

Dla ISBN najlepiej będzie pasować łańcuchowy typ danych, czyli "String". Pominiemy walidację danych, natomiast pamiętaj, że w prawdziwie dobrze napisanej aplikacji, należy zadbać o gwarancję poprawności formatu wpisywanego numeru (dokładnie 13 cyfr oddzielanych myślnikami).

Wtedy, zakładając że mielibyśmy w naszej klasie "Book", taką oto składową dla ISBN:

private String isbn;

możemy napisać coś takiego:

@Override
public int hashCode() {
	return isbn.hashCode();
}

Wyjaśniam co tu się wyrabia 😊.

Jeżeli identyczność ma być określana na podstawie numeru ISBN, to w przesłonięciu "hashCode" możemy odwołać się do kodu skrótu pobieranego z łańcucha. Natomiast chcąc zaprogramować dobrze działającą filtrację duplikatów, MUSIMY również zająć się metodą "equals" i tam operacja wygląda ciut bardziej przerażająco 😳:

@Override
public boolean equals(Object obj) {
	if(getClass() != obj.getClass()) {
		return false;
	}

	var otherBook = (Book)obj;

	return isbn.equals(otherBook.isbn);
}

Na samej górze wstawiamy instrukcję warunkową, która ma na celu sprawdzić zgodność typów obiektów czy "ten drugi" (w parametrze) jest typu tej klasy ("Book"). Jeżeli nie, to jasne jest, że nie będzie posiadać numeru ISBN do porównania i z góry możemy zwrócić fałsz 🙂.

W przeciwnym razie rzutujemy obiekt na naszą klasę i wtedy możemy korzystać ze wszystkich jego składowych. Musimy tak zrobić, gdyż metoda "equals" w języku Java przyjmuje parametr typu "Object". Możemy tak zrobić, ponieważ wyżej wspomniana instrukcja warunkowa, "blokuje" elementy o innym typie danych. Następnie w instrukcji "return", wstawiamy wyrażenie porównujące łańcuchy znaków dla ISBN metodą o tej samej nazwie. Jak będą identyczne, wtedy metoda "wnioskuje", że obie książki są sobie "równe" i nie dojdzie do jej dodania do zbioru, jeśli to duplikat 👍.

Kiedy skompilujesz sobie program z wyżej opisanymi zmianami, to próba wstawienia książki o tym samym tytule nie będzie wnosiła żadnego efektu. Nie będzie żadnego błędu ani wyjątku, tylko po prostu wywołanie metody będzie "wytłumione", tak samo jakby się wywołało pustą metodę, bez żadnych instrukcji w środku. Możesz to wypróbować stosując poniższy kod:

var booksSet = new HashSet();

booksSet.add(new Book("978-83-246-2773-8"));
booksSet.add(new Book("978-83-246-2773-8"));
System.out.println(booksSet.size());

Ostatnia instrukcja wypisze liczbę elementów jakie znajdują się w zmiennej "booksSet"...i nie będzie to 2 😝.

WIĘCEJ KRYTERIÓW

A co, gdybyśmy chcieli polegać na więcej składowych, niż jedna 🤔? Przypuśćmy, że interesuje nas też porównanie tytułów. Czyli co, nowa zmienna:

private String title;

i zmieniona implementacja:

@Override
public int hashCode() {
	return isbn.hashCode() + title.hashCode();
}

@Override
public boolean equals(Object obj) {
	if(getClass() != obj.getClass()) {
		return false;
	}

	var otherBook = (Book)obj;

	return isbn.equals(otherBook.isbn) && title.equals(otherBook.title);
}

To oznaczać będzie, że dwie książki zostaną uznane za równe wtedy i tylko wtedy, gdy:

  • numer ISBN będzie ten sam,
  • tytuł będzie ten sam.

To jest koniunkcja, zatem metoda "equals" w języku Java zwróci fałsz, jeśli tylko jeden z tych warunków zostanie spełniony (albo żaden) ℹ️. Dzięki takiemu zabiegowi, możemy ustalać dowolne kryterium, na którym będzie się opierać zbiór celem zweryfikowania, czy dodawany element należy "wpuścić" do siebie, czy nie.

Warto wiedzieć

Na zakończenie dodam parę zdań w charakterze porady. Jeżeli potrzebujesz określić na bazie jakich zmiennych ma się odbyć porównanie elementów, to najlepiej posługiwać się unikatowymi identyfikatorami w postaci łańcuchów znaków. Powód? Opieranie się na ich metodach "equals" będzie najłatwiejsze pod kątem definiowania kodu skrótu ("hashCode") 💡.

Są jednak takie przypadki, w których nie będzie się dało operować na łańcuchach z uwagi choćby na sam sens implementacji np. punkty w układzie współrzędnych (w ich przypadku, dodawanie łańcucha byłoby nadmiarowe). Kiedy zdecydujesz się na taki manewr (albo nie będziesz mieć innego wyjścia, bo Twoja klasa będzie potrzebowała takich obliczeń), pamiętaj że to nie może być byle jaka wartość! Algorytm musi dobierać taki kod, żeby dwie różne kombinacje nie zwróciły tego samego wyniku 🛑! Jeżeli dla przykładu mamy dwie pary RÓŻNYCH liczb w punktach (x; y) i w obu obiektach wychodzi ten sam kod skrótu, to program będzie mylnie wnioskować, że to są te same punkty, podczas gdy nie są.

Taką anomalię dałaby na przykład suma składowych punktu w układzie współrzędnych (x + y). Jeżeli mamy dwa punkty:

var pointA = new Point(3, 5);
var pointB = new Point(5, 3);

to widzimy gołym okiem, że nie są sobie równe. Ale że w sumie (jako operacji arytmetycznej) kolejność składników nie robi znaczenia, to elementy zostałyby uznane za równe, co byłoby banialuką ✋!

Z tego powodu, zagadnienie definiowania kryterium identyczności jest twardym orzechem do zgryzienia 🥜. Z dwóch powodów:

  1. zapamiętanie, że przesłaniamy DWIE metody,
  2. opracowanie dobrych wzorów na kod skrótu i ustalenie pod jakimi względami uznajemy oba obiekty za równe.

W artykule o kodzie skrótu wyjaśniałem formułę dającą niezawodną wartość niezależnie od kombinacji ℹ️.

Metoda "equals" w języku Java

Metoda "equals" w języku Java służy do ustalania, które składowe klasy program ma wziąć pod uwagę, żeby móc określić czy dwa różne obiekty są sobie równe. Oprócz samych kryteriów, należy też zadbać o dobry generator kodu skrótu w metodzie "hashCode".


Zostało wszystko wyłożone na ten temat, jak na tacy 📥.

NASTĘPNY ARTYKUŁ: TreeSet w języku Java. Zbiór sortujący automatycznie

PODOBNE ARTYKUŁY