1. Zapoznanie z klasą Objects
Przechodzimy do tematu! Jeśli masz dość ręcznego sprawdzania wartości pod kątem null i pisania obliczania hashy dla każdego pola, z pomocą przyjdzie klasa narzędziowa java.util.Objects. Jej zadanie — uprościć pracę z obiektami, uczynić ją zwięzłą i bezpieczną.
To naprawdę „szwajcarski scyzoryk”: klasa potrafi bezpiecznie porównywać obiekty pod względem równości (bez ryzyka NullPointerException), wygodnie obliczać kody skrótu (hash), porównywać przez komparator oraz sprawdzać argumenty pod kątem null.
Objects.equals: bezpieczne porównanie z uwzględnieniem null
Jeśli napiszesz po prostu a.equals(b), a a okaże się null, poleci NullPointerException. Ręczne sprawdzanie jest rozwlekłe. Objects.equals(a, b) robi to za ciebie:
- Jeśli oba są null — zwraca true.
- Jeśli dokładnie jeden jest null — zwraca false.
- Jeśli oba nie są null — wywołuje zwykłe equals.
import java.util.Objects;
String a = null;
String b = "Java";
System.out.println(Objects.equals(a, b)); // false
String c = null;
System.out.println(Objects.equals(a, c)); // true
String d = "Java";
String e = "Java";
System.out.println(Objects.equals(d, e)); // true
Dlaczego to wygodne? Kod jest krótszy, czytelniejszy i odporny na przypadkowe NPE.
2. Objects.hash i hashCode: zwięzłe obliczanie hashy
Nadpisując hashCode wraz z equals, łatwo o błąd, zwłaszcza gdy pól jest dużo. Ręczny kod często bywa rozwlekły i kruchy:
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + age;
return result;
}
Metoda Objects.hash rozwiązuje problem — krótko, bezpiecznie i z obsługą null:
import java.util.Objects;
public class Person {
private String name;
private int age;
// ... konstruktor, gettery itp.
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
Ważna uwaga: Objects.hash używa varargs i tworzy tablicę — w rzadkich, wysoko obciążonych miejscach ręcznie napisany hashCode może być szybszy. Dla większości aplikacji różnica jest nieistotna.
3. Objects.compare: delegujemy porównanie komparatorowi
Czasem trzeba porównać dwa obiekty poprzez wcześniej przygotowany Comparator. Zamiast wywoływać wprost comparator.compare(a, b) można użyć:
int result = Objects.compare(a, b, comparator);
Ta metoda:
- Zwraca 0, jeśli obiekty są równe.
- Uznaje null za „mniejsze” od każdego nie-null obiektu.
- W pozostałych przypadkach deleguje logikę przekazanemu komparatorowi.
import java.util.Comparator;
import java.util.Objects;
class Person {
private String name;
Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Main {
public static void main(String[] args) {
Person a = new Person("Anna");
Person b = new Person("Boris");
Comparator<Person> byName = Comparator.comparing(Person::getName);
System.out.println(Objects.compare(a, b, byName)); // <0, ponieważ "Anna" < "Boris"
System.out.println(Objects.compare(a, null, byName)); // >0, bo a != null
System.out.println(Objects.compare(null, b, byName)); // <0, bo null < b
System.out.println(Objects.compare(null, null, byName)); // 0
}
}
4. Objects.requireNonNull: asekuracja przed „niewidzialnymi” błędami
Jeśli metoda powinna przyjmować tylko wartości nie-null, sprawdzaj to od razu. Objects.requireNonNull rzuci NullPointerException z twoją wiadomością:
public void setName(String name) {
this.name = Objects.requireNonNull(name, "Imię nie może być null");
}
5. Przykład: poprawna implementacja equals, hashCode i compareTo z Objects
import java.util.Objects;
public class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = Objects.requireNonNull(name, "Imię nie może być null");
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) {
if (this == o) return true; // Porównanie przez referencję
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
// Bezpieczne porównanie z null
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age); // Zwięźle i bezpiecznie
}
@Override
public int compareTo(Person other) {
// Najpierw porównujemy po imieniu, potem po wieku
int cmp = name.compareTo(other.name);
if (cmp != 0) return cmp;
return Integer.compare(age, other.age);
}
}
Teraz można przechowywać obiekty w HashSet, używać ich jako kluczy w HashMap, porównywać pod względem równości i sortować listy (np. przez Collections.sort).
6. Zastosowanie w realnych zadaniach: mniej kodu i mniej błędów
Przykład: lista użytkowników w aplikacji
Dzięki poprawnej parze equals/hashCode wyszukiwanie w kolekcjach działa przewidywalnie:
import java.util.ArrayList;
import java.util.List;
List<Person> users = new ArrayList<>();
users.add(new Person("Anna", 25));
users.add(new Person("Boris", 30));
Person search = new Person("Anna", 25);
System.out.println(users.contains(search)); // true
Przykład: praca z polami nullable
Jeśli klasa ma pola, które mogą być null (np. patronimik), używaj Objects.equals i Objects.hash:
import java.util.Objects;
public class User {
private String firstName;
private String middleName; // Może być null
private String lastName;
@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(firstName, user.firstName)
&& Objects.equals(middleName, user.middleName)
&& Objects.equals(lastName, user.lastName);
}
@Override
public int hashCode() {
return Objects.hash(firstName, middleName, lastName);
}
}
7. Tabela: główne metody klasy Objects
| Metoda | Przeznaczenie | Przykład użycia |
|---|---|---|
|
Bezpieczne porównanie dwóch obiektów z uwzględnieniem null | |
|
Zwięzłe obliczanie kodu hash na podstawie wielu pól | |
|
Porównanie przez komparator, bezpieczne dla null | |
|
Sprawdzenie na null, rzuca NullPointerException | |
|
Sprawdzenie null/nie null (wygodne w Stream API) | |
8. Typowe błędy przy używaniu metod klasy Objects
Błąd nr 1: zapomniano użyć Objects.equals dla pól nullable. Jeśli porównujesz pola bezpośrednio przez equals, możesz złapać NullPointerException. Użyj Objects.equals(middleName, other.middleName).
Błąd nr 2: nie wszystkie pola uwzględnione w hashCode. Pola biorące udział w equals powinny brać udział także w hashCode, inaczej zachowanie HashSet/HashMap stanie się nieprzewidywalne.
Błąd nr 3: ręczny hashCode z błędem. Dbanie o współczynniki i sprawdzanie null nie jest proste. Objects.hash robi to za ciebie; używaj go, jeśli nie masz rygorystycznych wymagań wydajnościowych.
Błąd nr 4: nie używasz Objects.requireNonNull tam, gdzie to kontrakt klasy. Jeśli pole nie dopuszcza null, sprawdzaj to w konstruktorze/setterze — błąd ujawni się od razu, a nie w „gąszczu” stosu wywołań.
Błąd nr 5: używasz Objects.hash dla tablic. Dla tablic należy użyć Arrays.hashCode, a dla zagnieżdżonych tablic — Arrays.deepHashCode; analogicznie do porównywania zawartości istnieją Arrays.equals/Arrays.deepEquals.
GO TO FULL VERSION