Rozdział 9 - Klasy - Equals i porównywanie obiektów

W jednym z podrozdziałów rozdziału o metodach dowiedzieliśmy się, że nie powinniśmy porównywać obiektów typu String za pomocą operatora ==, lecz za pomocą metody equals.

W tym rozdziale dowiemy się z czego to wymaganie wynika.

Porównywanie wartości zmiennych

Jak już wiemy, w Javie wyróżniamy dwa rodzaje typów: prymitywne oraz referencyjne (złożone). Typów prymitywnych jest osiem, natomiast typy referencyjne możemy definiować sami poprzez pisanie klas.

Dowiedzieliśmy się już także, że zmienne typów referencyjnych wskazują na obiekty w pamięci – nie są one obiektami, lecz referencjami do obiektów.

W poniższym przykładzie tworzymy trzy zmienne typu Wspolrzedne, ale tylko dwa obiekty tego typu:

Nazwa pliku: Wspolrzedne.java
public class Wspolrzedne {
  private int x, y;

  public Wspolrzedne(int x, int y) {
    this.x = x;
    this.y = y;
  }

  public static void main(String[] args) {
    Wspolrzedne w1 = new Wspolrzedne(10, 20); // 1
    Wspolrzedne w2 = w1; // 2
    Wspolrzedne w3 = new Wspolrzedne(1, -5); // 3
  }
}

Poniższy obrazek przedstawia zmienne oraz obiekty z powyższego przykładu:

Trzy zmienne wskazujace na dwa obiekty w pamieci

Zmienne w1 oraz w2 wskazują na ten sam obiekt – utworzony w linii (1) i w tej samej linii przypisany do zmiennej w1. W linii (2) przypisujemy do zmiennej w2 referencję do tego samego obiektu, na który wskazuje w1. Zmienna w3 wskazuje na inny obiekt, utworzony w linii (3).

Spróbujmy teraz wykonać następujące ćwiczenie: zmienimy wartości x oraz y obiektu, na który wskazuje zmienna w3, aby wartości te odpowiadały wartościom x i y obiektu, na który wskazują zmienne w1 oraz w2.

Następnie, spróbujemy porównać do siebie zmienne w1, w2, oraz w3, używając operatora == :

Nazwa pliku: Wspolrzedne.java
public class Wspolrzedne {
  private int x, y;

  public Wspolrzedne(int x, int y) {
    this.x = x;
    this.y = y;
  }

  public void setX(int x) {
    this.x = x;
  }

  public void setY(int y) {
    this.y = y;
  }

  public static void main(String[] args) {
    Wspolrzedne w1 = new Wspolrzedne(10, 20);
    Wspolrzedne w2 = w1;
    Wspolrzedne w3 = new Wspolrzedne(1, -5);

    w3.setX(10); // 1
    w3.setY(20); // 1

    if (w1 == w2) {
      System.out.println("w1 jest rowne w2");
    } else {
      System.out.println("w1 nie jest rowne w2");
    }

    if (w1 == w3) {
      System.out.println("w1 jest rowne w3");
    } else {
      System.out.println("w1 nie jest rowne w3");
    }
  }
}

W liniach (1), ustawiamy, za pomocą setterów, wartości pól x oraz y obiektu wskazywanego przez w3 na takie same wartości, jakie mają pola x i y obiektu wskazywanego przez zmienne w1 i w2. Mamy teraz w pamięci dwa obiekty typu Wspolrzedne – oba mają pola x i y ustawione na te same wartości: x wynosi 10, a y wynosi 20.

Pytanie: jakie komunikaty zobaczymy na ekranie?

W wyniku działania powyższego programu, na ekranie zobaczymy następujące komunikaty:

w1 jest rowne w2 w1 nie jest rowne w3

Zobaczyliśmy takie komunikaty, ponieważ użycie operatora porównania == powoduje, że porównywane są do siebie wartości zmiennych. Dlaczego w takim razie wartości zmiennych w1 i w3 nie zostały uznane za równe?

Mogłoby się wydawać, że skoro pola x i y obiektów wskazywanych przez zmienne w1 i w3 są takie same (x wynosi 10, a y jest równe 20), to powinniśmy zobaczyć na ekranie komunikat "w1 jest rowne w3":

if (w1 == w3) {
  System.out.println("w1 jest rowne w3");
} else {
  System.out.println("w1 nie jest rowne w3");
}

Komunikat, który jednak widzimy, to "w1 nie jest rowne w3", ponieważ operator == nie porównuje stanu obiektów, na które te zmienne wskazują (to znaczy nie sprawdza, czy wszystkie pola tych obiektów mają takie same wartości), lecz sprawdza, czy obie te zmienne wskazuję na ten sam obiekt w pamięci.

Powtórzmy: wartościami zmiennych w1 i w3referencje do dwóch obiektów w pamięci – tak się składa, że oba te obiekty mają pola x i y ustawione na takie same wartości, jednakże są to dwa osobne obiekty w pamięci – dlatego wynik porównania operatorem == zwraca false – zmienne te wskazują na dwa różne obiekty typu Wspolrzedne w pamięci.

Z tego powodu, z kolei, porównanie zmiennych w1 i w2 zwraca true – ponieważ obie te zmienne wskazują na ten sam obiekt w pamięci:

Wspolrzedne w1 = new Wspolrzedne(10, 20);
// w2 bedzie pokazywac na ten sam obiekt, co zmienna w1
Wspolrzedne w2 = w1;

// zwroci true – zmienne pokazuja na ten sam obiekt w pamieci
if (w1 == w2) {
  System.out.println("w1 jest rowne w2");
} else {
  System.out.println("w1 nie jest rowne w2");
}

Zapamiętajmy: jeżeli porównujemy dwie zmienne za pomocą operatora porównania ==, to porównujemy wartości tych zmiennych. Wartościami zmiennych typów złożonych są referencje do obiektów w pamięci – porównując dwie takie zmienne, zadajemy pytanie:

"Czy te zmienne wskazują na ten sam obiekt w pamięci?"

Ze zmiennymi typów prymitywnych jest tak samo – porównujemy wartości tych zmiennych:

int x = 5;
int y = 5;
int z = y;

if (x == y) {
  System.out.println("x == y");
} else {
  System.out.println("x != y");
}

if (x == z) {
  System.out.println("x == z");
} else {
  System.out.println("x != z");
}

if (y == z) {
  System.out.println("y == z");
} else {
  System.out.println("y != z");
}

W wyniku działania powyższego fragmentu kodu, na ekranie zobaczymy:

x == y x == z y == z

Każda ze zdefiniowanych zmiennych ma taką samą wartość: 5, dlatego wszystkie trzy komunikaty wskazują, że zmienne są sobie równe.

Nie mamy tutaj do czynienia z referencjami – wszystkie trzy zmienne x, y, oraz z, są zmiennymi typu prymitywnego int. Porównujemy wartości tych zmiennych do siebie, a każda z nich ma przypisaną wartość 5.

Czy jest w takim razie jakiś sposób, by porównywać obiekty danej klasy nie na podstawie tego, czy są tym samym obiektem, lecz na podstawie innych kryteriów, jak na przykład: czy wartości ich pól są takie same?

Tak – służy do tego specjalna metoda equals.

Porównywanie obiektów za pomocą equals

Przypomnijmy działanie operatora == (a także !=): gdy używamy operatora porównania == by porównać dwie zmienne typów referencyjnych, porównywane są adresy obiektów, na które te zmienne wskazują. Operator == zwróci true tylko w przypadku, gdy obie zmienne wskazują na dokładnie ten sam obiekt w pamięci:

Nazwa pliku: Osoba.java
public class Osoba {
  private String imie;
  private String nazwisko;
  private int wiek;

  public Osoba(String imie, String nazwisko, int wiek) {
    this.imie = imie;
    this.nazwisko = nazwisko;
    this.wiek = wiek;
  }
}
Nazwa pliku: PorownywanieObiektow.java
public class PorownywanieObiektow {
  public static void main(String[] args) {
    Osoba o1 = new Osoba("Jan", "Nowak", 25);
    Osoba o2 = o1;
    Osoba o3 = new Osoba("Jan", "Nowak", 25);

    if (o1 == o2) {
      System.out.println("o1 i o2 sa rowne");
    } else {
      System.out.println("o1 i o2 nie sa rowne");
    }

    if (o1 == o3) {
      System.out.println("o1 i o3 sa rowne.");
    } else {
      System.out.println("o1 i o3 nie sa rowne");
    }
  }
}

Uruchomienie powyższej klasy spowoduje, że na ekranie zobaczymy:

o1 i o2 są rowne o1 i o3 nie sa rowne

Pomimo tych samych wartości przechowywanych w dwóch utworzonych obiektach, operator == użyty z obiektami o1 oraz o3 zwraca false, co widzimy na ekranie. Z kolei użycie == z obiektami o1 i o2 zwraca true, ponieważ wskazują one na ten sam obiekt w pamięci.

Operatorów == oraz != (różne od) powinniśmy używać tylko wtedy, gdy chcemy sprawdzić, czy dwie zmienne typu referencyjnego wskazują (bądź nie) na ten sam obiekt w pamięci. Jak już także wiemy, możemy je też stosować do sprawdzenia, czy dana zmienna wskazuje na jakiś obiekt, czy nie – poprzez przyrównanie zmiennej do wartości null.

Z drugiej jednak strony, w naszych programach często będziemy mieli potrzebę, aby porównać dwa obiekty pod względem wartości ich pól. Jest to na tyle powszechne wymaganie, że w języku Java istnieje specjalny mechanizm służący do właśnie tego celu – metoda equals. Spotkaliśmy się już z tą metodą – używaliśmy jej do porównywania zmiennych typu String.

Każda klasa, które chce umożliwić porównywanie jej obiektów w określony sposób, może dostarczyć taką metodę. Musi ona jednak spełniać dwa rodzaje wymagań.

Sygnatura metody equals

W pierwszej kolejności, metoda equals musi:

  • przyjmować jako parametr jeden argument typu Object,
  • być publiczna (posiadać modyfikator public),
  • zwracać wartość typu boolean.

Sygnatura metody equals wygląda więc następująco:

public boolean equals(Object o) {
  // implementacja
}

W metodzie tej powinniśmy porównać obiekt, na rzecz którego metoda equals została wywołana, do obiektu przesłanego jako argument o nazwie o. Jeżeli, wedle naszych kryteriów, obiekty zostaną uznane za równe, to metoda powinna zwrócić wartość true. Jeżeli obiekty są od siebie różne, powinna zwrócić wartość false. Zaraz zobaczymy przykładową implementację metody equals dla klasy Osoba, ale najpierw zaznajomimy się z tajemniczym typem Object.

Typ Object i krótko o dziedziczeniu

Do tej pory nie wspominaliśmy jeszcze o typie Object. Język Java, jak wiele innych języków obiektowych, udostępnia mechanizm dziedziczenia, któremu poświęcony jest osobny rozdział. Pozwala on na tworzenie hierarchii klas, które dziedziczą, jedna po drugiej, posiadając cechy i metody klas nadrzędnych, oraz wprowadzając nowe pola i metody.

Wszystkie klasy, jeżeli nie zdefiniują swojej klasy-rodzica, dziedziczą automatycznie po klasie o nazwie Object, która, jako jedyna, nie ma klasy nadrzędnej.

Dzięki mechanizmowi dziedziczenia, wszystkie obiekty klasy podrzędnej mogą być traktowane jak obiekty klasy nadrzędnej. Dla przykładu, klasa Zwierze mogłaby mieć klasy po niej dziedziczące o nazwach Pies i Kot – można by wtedy powiedzieć, że każdy obiekt klasy Pies (lub Kot) jest równocześnie obiektem klasy Zwierze. Tego samego nie można jednak powiedzieć o obiektach klasy Zwierze – nie każdy obiekt klasy Zwierze jest równocześnie obiektem klasy Pies – mógłby to być przecież obiekt klasy Kot.

Dlaczego w takim razie argumentem metody equals musi być obiekt typu Object? Metoda equals zdefiniowana jest w klasie Object – typem argumentu tej metody jest Object. Klasy, które dziedziczą po klasie Object (czyli wszystkie klasy), chcąc dostarczyć własną wersję metody equals, powinny używać takiej samej sygnatury tej metody, jaka użyta jest w klasie Object. Więcej na temat rozszerzenia klas i dziedziczenia metod dowiemy się wkrótce.

Podobnie, jak z metodą equals, było z poznaną już metodą toString. Ona także zdefiniowana jest w klasie Object, a my, dodając ją do naszych klas, dostarczamy własną implementację tej metody, którą nasze klasy dziedziczą po klasie Object. Jeżeli nie napiszemy sami metody toString, to wykorzystywana będzie jej domyślna implementacja z klasy Object, która wypisuje na ekran nazwę obiektu i jego „hash code”, rozdzielone znakiem @ (małpa), co widzieliśmy w jednym z poprzednich podrozdziałów. Dla przykładu, jeżeli wypisalibyśmy na ekran obiekt klasy Samochod, a klasa ta nie miałaby własnej metody toString, to zobaczylibyśmy na ekranie komunikat w postaci: Samochod@1b6d3586. Czym jest hash code dowiemy się w rozdziale o dziedziczeniu.

Klasa Object dostarcza "domyślną" implementację metody equals, którą dziedziczą po niej automatycznie wszystkie klasy, o ile nie dostarczą one własnej implementacji tej metody. Oznacza to więc, że możemy na obiektach typu Osoba wywoływać metodę equals pomimo, że jeszcze tej metody w klasie Osoba nie zdefiniowaliśmy:

Nazwa pliku: PorownywanieObiektow.java
public class PorownywanieObiektow {
  public static void main(String[] args) {
    Osoba o1 = new Osoba("Jan", "Nowak", 25);
    Osoba o2 = o1;
    Osoba o3 = new Osoba("Jan", "Nowak", 25);

    if (o1.equals(o2)) { // 1
      System.out.println("o1 i o2 sa rowne");
    } else {
      System.out.println("o1 i o2 nie sa rowne");
    }

    if (o1.equals(o3)) { // 2
      System.out.println("o1 i o3 sa rowne");
    } else {
      System.out.println("o1 i o3 nie sa rowne");
    }
  }
}

W powyższym przykładzie korzystamy z metody equals (1) (2), której klasa Osoba co prawda sama nie definiuje, ale dziedziczy ją po klasie Object. Jak, w takim razie, działa "domyślna" implementacja metody equals, której używamy powyżej?

Spójrzmy na implementację metody equals w klasie Object:

Fragment klasy Object z biblioteki standardowej Java
public boolean equals(Object obj) {
  return (this == obj);
}

Pytanie: co robi powyższa metoda i co zobaczymy na ekranie w wyniku uruchomienia klasy PorownywanieObiektow?

Domyślna wersja metody equals sprawdza, czy przesłany jako argument obiekt jest tym samym obiektem, co this (czyli obiekt, na rzecz którego equals zostało wywołane). Używany jest tutaj po prostu operator porównania. Domyślna metoda equals sprawdza po prostu, czy dwa obiekty są tym samym obiektem w pamięci.

Powyższy program zadziała więc dokładnie tak samo, jak poprzednia wersja, która używała operatora ==. Na ekranie zobaczymy więc:

o1 i o2 sa rowne o1 i o3 nie sa rowne
Każda klasa, która nie zdefiniuje własnej metody equals, dziedziczy implementację metody equals z klasy Object, która to implementacja działa tak samo, jak użycie operatora ==.

Implementacja metody equals w klasie Osoba

Metoda equals ma jeszcze jeden zestaw wymagań, o którym zaraz sobie opowiemy, ale najpierw zobaczymy, jak mogłaby wyglądać implementacja metody equals dla klasy Osoba. Przejdziemy krok po kroku po kolejnych aspektach metody equals, ponieważ jest tutaj sporo do wyjaśnienia!

Klasa Osoba posiada trzy pola: imie, nazwisko, oraz wiek. Chcielibyśmy, aby metoda equals oceniała równość obiektów typu Osoba nie na podstawie tego, czy są tym samym obiektem w pamięci, lecz na podstawie tego, czy wszystkie trzy pola: imie, nazwisko, oraz wiek, mają takie same wartości.

Mając dwa obiekty:

Osoba o1 = new Osoba("Jan", "Nowak", 25);
Osoba o3 = new Osoba("Jan", "Nowak", 25);

Operator porównania oraz domyślna implementacja equals z klasy Object zwróciłyby false:

// czy zmienne wskazuja na ten sam obiekt?
o1 == o3 // false

// equals odziedziczone z klasy Object
// ponownie sprawdzane jest, czy o1 i o3 wskazuja na ten sam obiekt
o1.equals(o3) // false

Natomiast wersja equals, którą chcemy zaimplementować, w przypadku dwóch powyższych obiektów powinna zwrócić true:

// equals dedykowane dla klasy Osoba (ktorego jeszcze nie napisalismy)
o1.equals(o3) // true

Dodajmy do klasy Osoba metodę equals o odpowiedniej sygnaturze, która porównuje pola obiektów typu Osoba. Zwróćmy uwagę, że porównując pola imie oraz nazwisko, także korzystamy z metody equals:

Nazwa pliku: Osoba.java
public class Osoba {
  private String imie;
  private String nazwisko;
  private int wiek;

  public Osoba(String imie, String nazwisko, int wiek) {
    this.imie = imie;
    this.nazwisko = nazwisko;
    this.wiek = wiek;
  }

  public boolean equals(Object o) {
    return this.imie.equals(o.imie) &&
        this.nazwisko.equals(o.nazwisko) &&
        this.wiek == o.wiek;
  }
}

Pierwsza wersja metody equals dla klasy Osoba zwraca true, jeżeli wszystkie pola obiektu this (czyli obiektu, na rzecz którego wywołamy equals) są równe polom obiektu o. Jeżeli jednak spróbowalibyśmy teraz skompilować klasę Osoba, to zobaczylibyśmy na ekranie następujące błędy:

Osoba.java:13: error: cannot find symbol return this.imie.equals(o.imie) && ^ symbol: variable imie location: variable o of type Object Osoba.java:14: error: cannot find symbol this.nazwisko.equals(o.nazwisko) && ^ symbol: variable nazwisko location: variable o of type Object Osoba.java:15: error: cannot find symbol this.wiek == o.wiek; ^ symbol: variable wiek location: variable o of type Object 3 errors

Kompilator informuje nas, że obiekt o nie posiada pól imie, nazwisko, oraz wiek. W końcu to obiekty klasy Osoba mają te pola, a nie obiekty klasy Object. Jak w takim razie wykonać porównanie pól?

W rozdziale o zmiennych dowiedzieliśmy się o istnieniu mechanizmu zwanego rzutowaniem. Wartość pewnego typu możemy zrzutować na inny typ, o ile ma to sens – liczbę rzeczywistą możemy zrzutować na liczbę całkowitą, w wyniku czego wartość po przecinku liczby rzeczywistej zostanie ucięta. Z drugiej jednak strony, zamiana stringu "Witajcie!" na liczbę rzeczywistą nie ma sensu i nie jest możliwa – kompilator nie zezwoli na taką próbę rzutowania.

Aby zrzutować wartość jednego typu na wartość innego typu, piszemy nazwę typu docelowego w nawiasach przed wartością, którą chcemy zrzutować:

// zrzutuj liczbe 3.14 na liczbe calkowita
// wartosc x bedzie wynosic 3
int x = (int) 3.14;

// rzutowanie niewykonalne - blad kompilacji
// Error incompatible types: java.lang.String cannot
//  be converted to double
double d = (double) "Witajcie!"; // BLAD!

W poprzednim podrozdziale wspomnieliśmy, że każdy obiekt klasy podrzędnej możemy traktować jako obiekt klasy-rodzica (każdy obiekt klasy Pies to Zwierze). Dzięki rzutowaniu, możemy także zadziałać w drugą stronę, to znaczy potraktować obiekt klasy nadrzędnej jako obiekt klasy podrzędnej – czyli powiedzieć kompilatorowi: ten obiekt to Zwierze, ale ja wiem, że w pamięci jest faktycznie obiekt klasy Pies. Dzięki temu, uzyskamy dostęp do pól i metod specyficznych dla klasy Pies, które nie występują w klasie Zwierze.

Skoro klasa Object jest klasą nadrzędną dla wszystkich innych klas, to możemy powiedzieć kompilatorowi, że chcemy przesłany jako argument obiekt o traktować jako obiekt klasy Osoba:

Kolejna wersja metody equals w klasie Osoba
public boolean equals(Object o) {
  Osoba innaOsoba = (Osoba) o;

  return this.imie.equals(innaOsoba.imie) &&
      this.nazwisko.equals(innaOsoba.nazwisko) &&
      this.wiek == innaOsoba.wiek;
}

Spróbujmy teraz uruchomić klasę PorownywanieObiektow i zobaczmy, co zostanie wypisane na ekran – przypomnijmy kod metody main z tej klasy:

Metoda main z klasy PorownywanieObiektow
public static void main(String[] args) {
  Osoba o1 = new Osoba("Jan", "Nowak", 25);
  Osoba o2 = o1;
  Osoba o3 = new Osoba("Jan", "Nowak", 25);

  if (o1.equals(o2)) {
    System.out.println("o1 i o2 sa rowne");
  } else {
    System.out.println("o1 i o2 nie sa rowne");
  }

  if (o1.equals(o3)) {
    System.out.println("o1 i o3 sa rowne");
  } else {
    System.out.println("o1 i o3 nie sa rowne");
  }
}

Aktualna wersja metody equals, którą zdefiniowaliśmy w klasie Osoba, spowoduje, że tym razem, w wyniku działania powyższego kodu, na ekranie zobaczymy:

o1 i o2 sa rowne o1 i o3 sa rowne

Nasza metoda equals działa! Pomimo, że zmienne o1 i o3 wskazują na różne obiekty w pamięci, to wynik porównania obiektów o1 i o3 dał wynik "są równe" – porównane zostały wartości pól obu obiektów, a nie ich adresy w pamięci. Jest to jednak dopiero początek naszej implementacji metody equals – musi zadbać o jeszcze kilka rzeczy.

Zauważmy, że typem argumentu metody equals jest Object, a jako argument przesyłamy do niej obiekt typu Osoba – jak już wiemy, nie ma z tym problemu, ponieważ obiekty typu Osoba mogą być także traktowane jako obiekty klasy Object.

Ale skoro tak, to czy możemy przesłać jako argument do metody equals obiekt innej klasy, na przykład String? Spróbujmy:

Osoba o1 = new Osoba("Jan", "Nowak", 25);
String powitanie = "Witajcie!";

if (o1.equals(powitanie)) {
  System.out.println("o1 i powitanie sa rowne");
}

Powyższy kod skompiluje się bez problemów – w końcu klasa String także może być traktowana jako obiekt klasy Object. Kompilacja, co prawda nie zakończy się błędem, ale wykonanie programu już tak:

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to Osoba at Osoba.equals(Osoba.java:13) at PorownywanieObiektow.main(PorownywanieObiektow.java:6)

Błąd wykonania programu wynika z poniższej linii kodu metody equals z klasy Osoba:

Osoba innaOsoba = (Osoba) o;

W tej linii zakładamy, że każdy obiekt przesłany do metody equals będziemy mogli zrzutować do obiektu klasy Osoba. Jest to założenie zdecydowanie zbyt optymistyczne – gdy typem argumentu jest Object, do metody możemy przesłać obiekt dowolnej klasy. Jak, w takim razie, uchronić się przed nieprawidłowym rzutowaniem?

Każda klasa dziedziczy po klasie Object jeszcze inną, przydatną metodę, z której możemy teraz skorzystać. Ta metoda to getClass, która zwraca obiekt służący do identyfikacji klasy obiektu.

Korzystając z wyniku metody getClass możemy sprawdzić, czy dwa obiekty są obiektami tego samego typu:

Nazwa pliku: PrzykladGetClass.java
public class PrzykladGetClass {
  public static void main(String[] args) {
    Osoba osoba = new Osoba("Jan", "Kowalski", 35);
    String powitanie = "Witajcie!";
    Object pewienObiekt = new Osoba("Marek", "Nowak", 30); // 1

    // 2
    System.out.println(osoba.getClass());
    System.out.println(powitanie.getClass());
    System.out.println(pewienObiekt.getClass());

    if (pewienObiekt.getClass() == powitanie.getClass()) { // 3
      System.out.println("Klasy obiektu 'pewienObiekt' i 'powitanie' " +
          "sa takie same.");
    } else {
      System.out.println("Klasy obiektu 'pewienObiekt' i 'powitanie' " +
          "sa rozne.");
    }

    if (pewienObiekt.getClass() == osoba.getClass()) { // 4
      System.out.println("Klasy obiektu 'pewienObiekt' i 'osoba' " +
          "sa takie same.");
    } else {
      System.out.println("Klasy obiektu 'pewienObiekt' i 'osoba' " +
          "sa rozne.");
    }
  }
}

W powyższym przykładzie tworzymy trzy obiekty: dwa klasy Osoba oraz jeden klasy String. Zmiennej typu Object (1) przypisujemy obiekt klasy Osoba – możemy to zrobić, ponieważ każdy obiekt może być traktowany jak obiekt klasy Object. Następnie (2), wypisujemy na ekran tekstową reprezentację wyników wywołania metody getClass na każdej ze zmiennych.

W wyniku działania powyższego programu, na ekranie zobaczymy:

class Osoba class java.lang.String class Osoba Klasy obiektu 'pewienObiekt' i 'powitanie' sa rozne. Klasy obiektu 'pewienObiekt' i 'osoba' sa takie same.

Pierwsze trzy linijki informują nas o typach każdego z obiektów wskazywanego przez trzy zdefiniowane zmienne. Jak widać, pomimo, że zmienna pewienObiekt została zdefiniowana jako Object, to na ekranie widzimy wypisany tekst "class Osoba", ponieważ w rzeczywistości na obiekt właśnie tej klasy zmienna pewienObiekt wskazuje.

Możemy porównać wyniki wywołania metody getClass – jak widzimy powyżej, porównanie klas obiektów pewienObiekt i powitanie daje w wyniku false, co symbolizuje komunikat:

Klasy obiektu 'pewienObiekt' i 'powitanie' sa rozne.

Z drugiej jednak strony, porównanie klas obiektów pewienObiekt i osoba daje w wyniku true, ponieważ obie zmienne wskazują na obiekt klasy Osoba.

Z powyższego faktu możemy skorzystać w naszej metodzie equals – zanim zrzutujemy argument o nazwie o typu Object na obiekt typu Osoba, sprawdzimy, czy aby na pewno pod tym argumentem kryje się obiekt tej klasy – spójrzmy na nową wersję metody equals w klasie Osoba:

Metoda equals z klasy Osoba
public boolean equals(Object o) {
  if (this.getClass() != o.getClass()) { // 1
    return false;
  }

  Osoba innaOsoba = (Osoba) o; // 2

  return this.imie.equals(innaOsoba.imie) &&
      this.nazwisko.equals(innaOsoba.nazwisko) &&
      this.wiek == innaOsoba.wiek;
}

Na początek metody equals dodaliśmy sprawdzanie, czy klasa aktualnego obiektu (this) jest różna od klasy obiektu przesłanego jako argument – jeżeli klasy obiektów są od siebie różne, to oba obiekty nie mogą być sobie równe – w takim przypadku, zwracamy od razu false, dzięki czemu nie dojdzie sytuacji, w której spróbujemy zrzutować obiekt innej klasy na obiekt klasy Osoba (2).

Dzięki powyższej zmianie, poniższy kod nie spowoduje już błędu działania programu:

Osoba o1 = new Osoba("Jan", "Nowak", 25);
String powitanie = "Witajcie!";

if (o1.equals(powitanie)) { // 1
  System.out.println("o1 i powitanie sa rowne");
}

Wywołanie metody equals z argumentem powitanie typu String nie stanowi już dla nas problemu – przed potencjalnym problemem broni nas sprawdzenie z użyciem metody getClass.

To jednak nie koniec implementacji metody equals. Jaki będzie teraz wynik działania poniższego kodu?

Fragment metody main klasy PorownywanieObiektow
Osoba o1 = new Osoba("Jan", "Nowak", 25);

if (o1.equals(null)) {
  System.out.println("o1 to null");
} else {
  System.out.println("o1 nie jest nullem");
}

Program zakończy się znanym nam już błędem Null Pointer Exception:

Exception in thread "main" java.lang.NullPointerException at Osoba.equals(Osoba.java:13) at PorownywanieObiektow.main(PorownywanieObiektow.java:10)

Dlaczego zobaczyliśmy taki komunikat?

Winna jest następująca linijka z metody equals klasy Osoba:

if (this.getClass() != o.getClass()) {

Próbujemy wywołać metodę getClass na obiekcie o, który jest nullem. Aby uchronić się przed potencjalnym działaniem na nullowym argumencie, musimy najpierw sprawdzić, czy nie jest nullem:

public boolean equals(Object o) {
  if (o == null || this.getClass() != o.getClass()) { // 1
    return false;
  }

  Osoba innaOsoba = (Osoba) o;

  return this.imie.equals(innaOsoba.imie) &&
      this.nazwisko.equals(innaOsoba.nazwisko) &&
      this.wiek == innaOsoba.wiek;
}

Przed jakimkolwiek operowaniem na obiekcie o dodaliśmy sprawdzenie (1), czy jest on nullem – jeżeli tak, to nie ma szansy, aby obiekt, na rzecz którego wywołaliśmy metodę equals, był równy null – zwracamy od razu false.

Teraz uruchomienie wcześniejszego kodu podającego argument null do equals nie zakończy się błędem, a na ekranie zobaczymy:

o1 nie jest nullem

Nasza metoda equals jest już prawie gotowa. Spójrzmy na kolejny przypadek do rozważenia:

Fragment metody main z klasy PorownywanieObiektow
Osoba o3 = new Osoba("Jan", "Nowak", 25);
Osoba o4 = new Osoba(null, "Nowak", 40);

if (o4.equals(o3)) {
  System.out.println("o3 i o4 sa rowne");
} else {
  System.out.println("o3 i o4 nie sa rowne");
}

Jaki będzie efekt wykonania powyższego kodu?

Ponownie zobaczymy na ekranie błąd Null Pointer Exception:

Exception in thread "main" java.lang.NullPointerException at Osoba.equals(Osoba.java:19) at PorownywanieObiektow.main(PorownywanieObiektow.java:32)

Czym spowodowany jest powyższy błąd?

Ponownie próbowaliśmy wywołać metodę na nullowym obiekcie – tym razem podczas porównywanie do siebie pól obiektów:

Fragment metody equals z klasy Osoba
return this.imie.equals(innaOsoba.imie) && // 1
    this.nazwisko.equals(innaOsoba.nazwisko) &&
    this.wiek == innaOsoba.wiek;

Zawiniła linijka (1). Obiekt o4, na rzecz którego wywołaliśmy metodę equals, został utworzony z polem imie zainicjalizowanym wartością null (taką wartość przesłaliśmy jako argument do konstruktora). W linijce (1) próbujemy, na rzecz pola imie aktualnego obiektu (this), wywołać metodę equals, by wartość tego pola porównać z wartością pola imie obiektu innaOsoba. Wynikiem, w takim przypadku, jest błąd działania programu Null Pointer Exception.

Aby naprawić naszą metodę equals, dodamy sprawdzanie nulla dla pól imie oraz nazwisko. Dodatkowo, jeżeli zarówno pole obiektu this, jak i obiektu obiektu przesłanego jako argument, będą wskazywać na null, to uznamy, że pole imie (lub nazwisko) są takie same. Innymi słowy, obiekty o np. takich samych polach nazwisko i wiek i polach imie ustawionych na null uznamy za takie same – spójrzmy na kolejną wersję metody equals:

public boolean equals(Object o) {
  if (o == null || this.getClass() != o.getClass()) {
    return false;
  }

  Osoba innaOsoba = (Osoba) o;

  if ((this.imie == null && innaOsoba.imie != null) || // 1
      (this.imie != null && !this.imie.equals(innaOsoba.imie))) { // 2
    return false;
  }

  // 3
  if ((this.nazwisko == null && innaOsoba.nazwisko != null) ||
      (this.nazwisko != null && !this.nazwisko.equals(innaOsoba.nazwisko))) {
    return false;
  }

  return this.wiek == innaOsoba.wiek; // 4
}

Dodaliśmy następujące sprawdzenie:

  • jeżeli pole imie aktualnego obiektu jest nullem, a pole imie przesłanego jako argumentu obiektu nie (1)
  • lub
  • pole imie aktualnego obiektu nie jestem nullem i nie jest ono równe polu imie obiektu przesłanego jako argument (2)

to zwracamy false – obiekty na pewno nie będą sobie równe. Ponowne sprawdzenie wykonujemy dla pola nazwisko (3). Na końcu zwracamy wynik porównania pól wiek – jeżeli dotarliśmy w metodzie do tej tego miejsca, to znaczy, że wszystkie poprzednie warunki zostały spełnione i zostaje nam już do sprawdzenia tylko pole wiek.

Nasza metoda jest już w zasadzie gotowa, jednak możemy do niej dodać jeszcze jeden warunek. Załóżmy, że nasza klasa ma kilkadziesiąt pól – czasem porównywanie ich wszystkich może być czasochłonne – jeżeli używalibyśmy często metody equals, mogłoby się to odbić negatywnie na wydajności naszego programu – możemy jednak wprowadzić proste usprawnienie do naszej metody. Zauważmy, że każdy obiekt jest zawsze równy sobie – w końcu to ten sam obiekt! Możemy w takim razie dodać na początku metody equals warunek sprawdzający, czy obiekt przesłany jako argument jest tym samym obiektem, na rzecz którego equals zostało wywołane – w takim przypadku możemy od razu zwrócić wartość true.

Finalna wersja klasy Osoba, z opisanym powyżej usprawnieniem, prezentuje się następująco:

Nazwa pliku: Osoba.java
public class Osoba {
  private String imie;
  private String nazwisko;
  private int wiek;

  public Osoba(String imie, String nazwisko, int wiek) {
    this.imie = imie;
    this.nazwisko = nazwisko;
    this.wiek = wiek;
  }

  public boolean equals(Object o) {
    if (this == o) { // 1
      return true;
    }

    if (o == null || this.getClass() != o.getClass()) {
      return false;
    }

    Osoba innaOsoba = (Osoba) o;

    if ((this.imie == null && innaOsoba.imie != null) ||
      (this.imie != null && !this.imie.equals(innaOsoba.imie))) {
      return false;
    }

    if ((this.nazwisko == null && innaOsoba.nazwisko != null) ||
      (this.nazwisko != null && !this.nazwisko.equals(innaOsoba.nazwisko))) {
      return false;
    }

    return this.wiek == innaOsoba.wiek;
  }
}

Na początek metody equals dodaliśmy sprawdzanie, czy this (aktualny obiekt) jest tym samym obiektem, co obiekt o przesłany jako argument – to znaczy, czy zarówno this, jaki i argument o, wskazują na ten sam obiekt w pamięci.

Spójrzmy teraz na kilka przykładów użycia powyższej metody equals:

Fragment pliku PorownywanieObiektow.java
Osoba x = new Osoba("Jan", "Kowalski", 20);
Osoba y = new Osoba("Jan", "Kowalski", 30);
Osoba z = new Osoba("Jan", "Kowalski", 20);

System.out.println("x rowne y? " + x.equals(y));
System.out.println("x rowne z? " + x.equals(z));
System.out.println("x rowne null? " + x.equals(null));
System.out.println("x rowne 'Witajcie!'? " + x.equals("Witajcie!"));


Osoba a = new Osoba(null, null, 30);
Osoba b = new Osoba(null, null, 30);

System.out.println("a rowne b? " + a.equals(b));
System.out.println("a rowne x? " + a.equals(x));

Wynikiem działania powyższego fragmentu kodu jest:

x rowne y? false x rowne z? true x rowne null? false x rowne 'Witajcie!'? false a rowne b? true a rowne x? false

Jak widać po tym podrozdziale, pisanie metod equals nie należy do najprostszych – musimy wziąć pod uwagę wiele czynników. Dodatkowo, metody equals powinny spełniać pewien kontrakt, o którym zaraz sobie opowiemy. Poza tym, musieliśmy przyswoić dużo podstawowych informacji o dziedziczeniu, o którym będziemy się uczyć w jednym z kolejnych rozdziałów. Jeżeli coś jest w tej chwili niezrozumiałe – cierpliwości! Wkrótce wszystko sobie dokładnie wyjaśnimy.

Istnieje wiele bibliotek (na przykład lombok), których możemy użyć w naszych programach, które mogą wygenerować za nas ciało metody equals na podstawie pól, jakie zawiera nasza klasa, ale: wiedza, do czego służy i jak powinna być pisana metoda equals jest fundamentalna i bardzo ważna. Można stosować automatyczne generowanie metod equals wtedy, gdy wiemy jak działa ta metoda i jakie problemy mogą wyniknąć, gdy jest napisana w sposób nieprawidłowy. Do tych kwestii będziemy jeszcze wracać w rozdziale o dziedziczeniu oraz o kolekcjach.

W ostatnim podrozdziale o metodzie equals rozpisany został krok po kroku sposób pisania tej metody. Można tam zajrzeć w celu znalezienia zwięzłego, podstawowego algorytmu dotyczącego pisania metody equals.

Kontrakt equals

Poza wymaganiami zaadresowanymi w poprzednim podrozdziale, by nasza metoda equals działała w różnych przypadkach i nie powodowała kończenia się naszych programów błędem, metoda equals musi także spełniać tak zwany kontrakt equals. Kontrakt ten ma zapewnić spójne działanie metody equals w różnych przypadkach.

Kontrakt equals to zestaw pięciu reguł, które powinna spełniać każda metoda equals. To my, jako programiści, jesteśmy odpowiedzialni za napisanie metody equals w taki sposób, by te reguły były spełnione. Reguły te opisane są w oficjalnej dokumentacji Java klasy Object – wedle nich, każda metoda equals powinna:

  1. być zwrotna – dla każdego obiektu x, jeżeli x nie jest nullem, to x.equals(x) powinno zwracać true,
  2. być symetryczna – dla nienullowych obiektów x oraz y, jeżeli x.equals(y) zwraca true, to y.equals(x) także powinno zwracać true,
  3. być przechodnia – dla nienullowych obiektów x, y, oraz z, jeżeli x.equals(y) zwraca true, oraz y.equals(z) zwraca true, to x.equals(z) także powinno zwrócić true,
  4. być spójna – dla nienullowych obiektów x oraz y, jeżeli x.equals(y) zwraca true bądź false, to ponowne wywołanie x.equals(y) powinno zwrócić taką samą wartość jak poprzednio, o ile żadne z pól, które jest wykorzystywane do porównania obiektów w metodzie equals, nie zostało zmienione w obiektach wskazywanych przez x oraz y.
  5. dla każdego nienullowego obiektu x, x.equals(null) powinno zawsze zwracać false.

Przekładając powyższe na język polski:

  1. Każdy obiekt powinien być równy sobie.
  2. Jeżeli obiekt x jest równy y, to powinno z tego wynikać, że y jest równy x.
  3. Jeżeli obiekt x i y są równe oraz y i z są równe, to powinno z tego wynikać, że także x i z są sobie równe.
  4. Jeżeli x i y są równe (bądź nierówne), i nie zmienimy tych obiektów, to powinny pozostać równe (bądź nierówne).
  5. Żaden nienullowy obiekt nie powinien być uznany za równy nullowi.

Po co nam kontrakt equals? Nasze programy powinny działać deterministycznie i to mają zapewnić powyższe założenia. Jeżeli naruszylibyśmy np. regułę drugą, to nasz program mógłby zachowywać się w nieprzewidziany sposób – wynik porównania obiektów, które de facto powinny być uznawane za równe, zależałby od kolejności ich porównywania.

Na pierwszy rzut oka może się wydawać, że nasza implementacja metody equals spełnia wszystkie powyższe założenia – i tak jest w rzeczywistości. Łatwo jednak napisać metodę equals w taki sposób, żeby naruszyć jedną bądź więcej reguł kontraktu equals, szczególnie, gdy w grę wchodzi dziedziczenie. Wrócimy do kontraktu equals w rozdziale o dziedziczeniu, gdzie omówimy sobie potencjalne problemy z wynikające z naruszenia powyższych reguł.

Spójrzmy na przykłady użycia metody equals z klasy Osoba, które udowodnią, że nasza implementacja spełnia kontrakt equals:

Nazwa pliku: KontraktEquals.java
public class KontraktEquals {
  public static void main(String[] args) {
    Osoba x = new Osoba("Jan", "Nowak", 30);
    Osoba y = new Osoba("Jan", "Nowak", 30);
    Osoba z = new Osoba("Jan", "Nowak", 30);
    Osoba a = new Osoba("Marek", "Kowalski", 35);

    // 1. zwrotnosc - obiekt jest rowny samemu sobie
    System.out.println("1. Czy obiekt x jest rowny sobie? " + x.equals(x));

    // 2. symetrycznosc - jesli x jest rowne y, to y jest rowne x
    System.out.println("2. Czy obiekt x jest rowny obiektowi y? " + x.equals(y));
    System.out.println("2. Czy obiekt y jest rowny obiektowi x? " + y.equals(x));

    // 3. przechodniosc - jesli x jest rowne y i y jest rowne z, to x jest rowne z
    System.out.println("3. Czy obiekt x jest rowny obiektowi y? " + x.equals(y));
    System.out.println("3. Czy obiekt y jest rowny obiektowi z? " + y.equals(z));
    System.out.println("3. Czy obiekt x jest rowny obiektowi z? " + x.equals(z));

    // 4. spojnosc - wynik equals nie zmienia sie, jezeli obiekty sie nie zmienia
    System.out.println("4. Czy obiekt x jest rowny obiektowi y? " + x.equals(y));
    System.out.println("4. Czy obiekt x jest rowny obiektowi a? " + x.equals(a));
    System.out.println("4. Czy obiekt x jest rowny obiektowi y? " + x.equals(y));
    System.out.println("4. Czy obiekt x jest rowny obiektowi a? " + x.equals(a));

    // 5. zaden obiekt nie jest rowny null
    System.out.println("5. Czy obiekt x jest rowny null? " + x.equals(null));
    System.out.println("5. Czy obiekt y jest rowny null? " + y.equals(null));
  }
}

W wyniku działania powyższego programu na ekranie zobaczymy – każda z reguł kontraktu equals jest spełniona:

1. Czy obiekt x jest rowny sobie? true 2. Czy obiekt x jest rowny obiektowi y? true 2. Czy obiekt y jest rowny obiektowi x? true 3. Czy obiekt x jest rowny obiektowi y? true 3. Czy obiekt y jest rowny obiektowi z? true 3. Czy obiekt x jest rowny obiektowi z? true 4. Czy obiekt x jest rowny obiektowi y? true 4. Czy obiekt x jest rowny obiektowi a? false 4. Czy obiekt x jest rowny obiektowi y? true 4. Czy obiekt x jest rowny obiektowi a? false 5. Czy obiekt x jest rowny null? false 5. Czy obiekt y jest rowny null? false

Equals – przykład z tablicą

Nasze klasy mogą zawierać także tablice – często będziemy chcieli porównywać je w ramach wykonywania metody equals.

Jak ocenić, czy dwie tablice są równe? Dwie tablicy uznamy za równe, gdy:

  • obie będę nullami
  • lub
  • obie będą miały tyle samo elementów i elementy te będą takie same parami, to znaczy obiekt spod indeksu 0 z tablicy pierwszego obiektu będzie taki sam, jak obiekt pod indeksem 0 z tablicy drugiego obiektu itd. dla każdego indeksu w tablicy.

Dwie uwagi do porównywanie tablic:

  1. Jeżeli tablice przechowują elementy typów złożonych, to pamiętajmy, aby porównywać do siebie te obiekty za pomocą metody equals ich typu, np. mając tablicę Stringów String[], poszczególne elementy tych tablic będziemy do siebie porównywać za pomocą metody equals z klasy String.
  2. Porównywanie dużych tablic może być czasochłonne i wpływać negatywnie na wydajność naszych programów – jeżeli mamy jeszcze inne pola w naszych klasach, to porównujmy je na początku, a porównywanie tablic i innych złożonych typów zostawmy na koniec – w ten sposób, porównując inne pola, istnieje szansa, że znajdziemy różnicę w porównywanych polach i nie będziemy musieli już wykonywać czasochłonnego porównywania tablic.

Jak zaraz zobaczymy, trzeba się trochę napisać, aby porównać w metodzie equals dwie tablice. Pod koniec tego podrozdziału zobaczymy, jak możemy uprościć sobie życie wykorzystując do tego celu bibliotekę standardową Java.

Poniżej zaprezentowane zostały jeszcze dwa przykłady metody equals dla klas Koszyk oraz Owoc. W pierwszej kolejności, przyjrzymy się metodzie equals klasy Owoc:

Nazwa pliku: Owoc.java
public class Owoc {
  private String nazwa;
  private double cena;

  public Owoc(String nazwa, double cena) {
    this.nazwa = nazwa;
    this.cena = cena;
  }

  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }

    if (o == null || this.getClass() != o.getClass()) {
      return false;
    }

    Owoc other = (Owoc) o; // 1

    if ((this.nazwa == null && other.nazwa != null) ||
        (this.nazwa != null && !this.nazwa.equals(other.nazwa))) {
      return false;
    }

    return this.cena == other.cena;
  }
}

Metoda equals klasy Owoc jest bardzo podobna do metody equals klasy Osoba – mamy w niej o jedno pole do porównania mniej, a także rzutujemy argument o nie do klasy Osoba, lecz do klasy Owoc (1).

Spójrzmy teraz na metodę equals klasy Koszyk, która jest zdecydowania ciekawsza – jednym z pól klasy Koszyk jest tablica obiektów typu Owoc – zauważmy, jak w metodzie equals porównujemy do siebie tablice owoce obiektów this oraz other:

Nazwa pliku: Koszyk.java
public class Koszyk {
  private Owoc[] owoce;
  private boolean oplacony;

  public Koszyk(Owoc[] owoce, boolean oplacony) {
    this.owoce = owoce;
    this.oplacony = oplacony;
  }

  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }

    if (o == null || this.getClass() != o.getClass()) {
      return false;
    }

    Koszyk other = (Koszyk) o;

    if (this.oplacony != other.oplacony) {
      return false;
    }

    // jezeli obie tablice sa ta sama tablica w pamieci
    // lub obie tablice sa nullem, to zwracamy true
    if (this.owoce == other.owoce) { // 1
      return true;
    }

    if (this.owoce == null || other.owoce == null) { // 2
      return false;
    }

    // tablice moga byc rowne, gdy obie maja tyle samo elementow
    // jezeli liczba elementow sie rozni - zwracamy false
    if (this.owoce.length != other.owoce.length) { // 3
      return false;
    }

    for (int i = 0; i < this.owoce.length; i++) { // 4
      // jezeli para obiektow z obu tablic pod danym indeksem
      // sie rozni, to zwracamy false
      Owoc o1 = this.owoce[i]; // 5
      Owoc o2 = other.owoce[i]; // 6

      if ((o1 == null && o2 != null) || // 7
        (o1 != null && !o1.equals(o2))) {
        return false; // 8
      }
    }

    // jezeli dotarlismy do tego miejsca, to znaczy, ze
    // nie znalezlismy zadnej roznicy pomiedzy obiekami this i other
    return true; // 9
  }
}

Klasa Koszyk zawiera dwa pola: oplacony oraz owoce. Początek metody equals jest taki sam, jak w klasie Owoc.

Od linii (1) zaczynamy porównywanie tablic owoce z obu obiektów. Jedną instrukcją warunkową sprawdzamy dwa przypadki:

  1. Czy tablica owoce w obiekcie this i other jest tą samą tablicą w pamięci?
  2. Czy obie referencje: this.owoce oraz other.owoce są nullami?

W obu przypadkach uznajemy tablice za równe i zwracamy true.

Jeżeli docieramy do warunku z linii (2), to jesteśmy pewni, że obie tablice nie są nullem – taką możliwość eliminuje warunek z linii (1). Jednakże, nadal jedna z tablica może być nullem – sprawdzamy to w linii (2) – jeżeli tak jest, to tablice nie mogą być równe i zwracamy false.

Mając wyeliminowane przypadki, gdy jedna z tablic (bądź obie) jest nullem, możemy sprawdzić w linii (3), czy obie zawierają tyle samo elementów – jeżeli nie, to na pewno nie uznamy je za takie same – zwracamy false.

Gdy już wiemy, że tablice nie są nullami i zawierają tyle samo elementów, przechodzimy do porównania ich elementów, jeden po drugim (4). Przypisujemy do pomocniczych zmiennych elementy tablic znajdujące się pod aktualnym indeksem pętli (5) (6). Następnie (7), porównujemy do siebie oba elementy tablicy (do których odnosimy się poprzez zmienne o1 i o2). Zauważmy, że skoro elementami tablicy owoce są obiekty typu Owoc, to musimy do ich porównania skorzystać z metody equals klasy Owoc. Bierzemy także pod uwagę, że elementy mogą być nullami. Jeżeli wykryjemy różnicę w którejś z par elementów obu tablic, zwracamy false (8).

Jeżeli dotrzemy na koniec equals do linii (9), będzie to oznaczało, iż nie znaleźliśmy żadnej różnicy w polach obiektów this oraz other i powinniśmy zwrócić true.

Jak widać, porównywanie tablic wymaga napisania kilkunastu linii kodu. Jest to na tyle częsta operacja, że biblioteka standardowa Java udostępnia metodę, która wykonuje właśnie to zadanie – możemy skorzystać z niej po zaimportowaniu klasy Arrays do naszego pliku źródłowego. O importowaniu klas opowiemy sobie w jednym z kolejnych podrozdziałów.

Aby uprościć nasz kod i skorzystać z istniejącej metody do porównywania tablicy, na początku naszego programu powinniśmy dodać następującą linię kodu, dzięki której będziemy mogli skorzystać z klasy Arrays:

import java.util.Arrays;

Następnie, cały kod zawarty między liniami (1) a (9) możemy zastąpić jedną, poniższą linią kodu:

return Arrays.equals(this.owoce, other.owoce);

Metoda equals z klasy Arrays nie powinna być mylona z metodami equals, które pisaliśmy do tej pory – przyjmuje ona dwie tablice obiektów i zwraca true, jeżeli:

  • obie zmienne wskazują na tą samą tablicę
  • lub
  • obie tablice są nullami
  • lub
  • obie tablice mają taką samą liczbę elementów i każda para elementów pod kolejnymi indeksami jest sobie równa – wykorzystywana jest metoda equals obiektów będących elementami tablic (obiekty są też uznawane za równe, jeżeli oba są nullami).

W przeciwnym razie, metoda Arrays.equals zwraca false. Spójrzmy na prosty przykład użycia Arrays.equals:

Nazwa pliku: PrzykladUzyciaArraysEquals.java
import java.util.Arrays;

public class PrzykladUzyciaArraysEquals {
  public static void main(String[] args) {
    int[] tablica1 = { 1, 2, 3 };
    int[] tablica2 = { 1, 2, 3 };
    int[] tablica3 = { 4, 5 };

    if (Arrays.equals(tablica1, tablica2)) {
      System.out.println("tablica1 == tablica2");
    } else {
      System.out.println("tablica1 != tablica2");
    }

    if (Arrays.equals(tablica1, tablica3)) {
      System.out.println("tablica1 == tablica3");
    } else {
      System.out.println("tablica1 != tablica3");
    }
  }
}

Wynik działania powyższego przykładu:

tablica1 == tablica2 tablica1 != tablica3
Warto sprawdzić, czy dany problem nie został już zaadresowany i czy w bibliotece standardowej Java (i nie tylko) nie znajduje się klasa bądź metoda, którą możemy wykorzystać do rozwiązania danego problemu. Warto także, z drugiej strony, rozumieć z czym wiążę się rozwiązanie danego problemu i mieć to na uwadze korzystając z gotowego rozwiązania.

Na koniec, spójrzmy na kilka przykładów użycia metody equals z klasy Koszyk:

Fragment pliku Koszyk.java
public static void main(String[] args) {
    Owoc czeresnie = new Owoc("Czeresnie", 10.0);
    Owoc jablko = new Owoc("Jablko", 5.0);

    Koszyk koszyk1 = new Koszyk(new Owoc[] { czeresnie, jablko }, false);
    Koszyk koszyk2 = new Koszyk(new Owoc[] { czeresnie, jablko }, true);
    Koszyk koszyk3 = new Koszyk(new Owoc[] { czeresnie, jablko }, false);
    Koszyk koszyk4 = new Koszyk(null, false);
    Koszyk koszyk5 = new Koszyk(new Owoc[] { jablko, czeresnie }, false);
    Koszyk koszyk6 = new Koszyk(
        new Owoc[] { czeresnie, jablko, czeresnie }, false);

    System.out.println(koszyk1.equals(koszyk2));
    System.out.println(koszyk1.equals(koszyk3));
    System.out.println(koszyk1.equals(koszyk4));
    System.out.println(koszyk1.equals(koszyk5));
    System.out.println(koszyk1.equals(koszyk6));
    System.out.println(koszyk1.equals(null));
}

Tylko obiekty wskazywane przez zmienne koszyk1 i koszyk3 są takie same:

false true false false false false

Krok po kroku – pisanie metody equals

Pisząc metodę equals, musimy pamiętać o kilku ważnych aspektach związanych z porównywaniem obiektów. Poniższa instrukcja opisuje krok po kroku jak dodać metodę equals do Twojej klasy:

  1. Zacznij od napisania sygnatury metody equals (pamiętaj o modyfikatorze public i typie argumentu Object!):
    public boolean equals(Object o) {
    
    }
    
  2. Dodaj warunek sprawdzający, czy przesłany jako argument obiekt o nie jest tym samym obiektem, co aktualny obiekt (który reprezentowany jest przez słowo kluczowe this) – jeżeli tak, to od razu zwróć true:
    if (this == o) {
      return true;
    }
    
  3. Nastepnie sprawdź, czy przesłany jako argument obiekt o jest nullem lub jest innego typu, niż obiekt wskazywany przez this (aby wyeliminować próby porównywania obiektów niekompatybilnych typów do naszego obiektu) – jeżeli tak, zwróć od razu false:
    if (o == null || this.getClass() != o.getClass()) {
      return false;
    }
    
  4. Zdefiniuj zmienną typu klasy, do której dodajesz equals i przypisz do niej zrzutowany obiekt o przesłany jako argument (dochodząc w kodzie do tego miejsca będziemy pewni, że rzutowanie się powiedzie, gdyż sprawdzamy typ obiektu w punkcie 3.):
    Osoba other = (Osoba) o;
    
  5. Na końcu porównaj do siebie wszystkie pola obu obiektów (this oraz other), które powinny być wzięte pod uwagę podczas sprawdzania równości dwóch obiektów. Jeżeli któreś z pól się różni, zwróć false. Jeżeli wszystkie pola są sobie równe – zwróć true:
    if ((this.imie == null && innaOsoba.imie != null) ||
        (this.imie != null && !this.imie.equals(innaOsoba.imie))) {
      return false;
    }
    
    if ((this.nazwisko == null && innaOsoba.nazwisko != null) ||
        (this.nazwisko != null && !this.nazwisko.equals(innaOsoba.nazwisko))) {
      return false;
    }
    
    return this.wiek == innaOsoba.wiek;
    

Finalnie, przykładowa metoda equals prezentuje się następująco:

public boolean equals(Object o) {
  // sprawdz, czy przeslany obiekt jest tym samym obiektem,
  // na rzecz ktorego zostala wywolana metoda equals
  if (this == o) {
    return true;
  }

  // sprawdz, czy nie przeslano nulla lub obiektu innej,
  // niekompatybilnej klasy (np. String)
  if (o == null || this.getClass() != o.getClass()) {
    return false;
  }

  // powyzsza instrukcja if zapewnia, ze ponizsza instrukcja
  // zakonczy sie sukcesem - rzutujemy referencje do obiektu typu Object,
  // (przeslana jako argument), do obiektu typu Osoba, aby uzyskac
  // dostep do pol imie, nazwisko, oraz wiek
  Osoba innaOsoba = (Osoba) o;

  // porownujemy kolejne pola do siebie - musimy uwzglednic,
  // ze pola typow zlozonych moga byc nullowe
  // jezeli pola aktualnego i porownywanego obiektu sa nullowe,
  // to uznajemy je za rowne
  if ((this.imie == null && innaOsoba.imie != null) ||
    (this.imie != null && !this.imie.equals(innaOsoba.imie))) {
    return false;
  }

  if ((this.nazwisko == null && innaOsoba.nazwisko != null) ||
    (this.nazwisko != null && !this.nazwisko.equals(innaOsoba.nazwisko))) {
    return false;
 }

  return this.wiek == innaOsoba.wiek;
}

Uwaga! Podczas porównywania pól musimy wziąć pod uwagę kilka bardzo istotnych reguł:

  1. Wszystkie pola typów złożonych należy porównywać do siebie za pomocą ich metod equals.
  2. Musimy wziąć pod uwagę, że pola typów złożonych mogą być nullami. Należy zadecydować także, gdy dane pole ma wartość null w obiekcie this oraz other, czy będą one traktowane jako równe, czy nie.
  3. Jeżeli polem jest tablica, to należy porównać wszystkie elementy obu tablic. Jeżeli jest to tablica typu złożonego (np. String[] – tablica stringów), elementy należy porównywać do siebie za pomocą metody equals typu String (patrz punkt b powyżej).
  4. Jeżeli mamy wiele pól do porównania, to warto zaczynać porównywanie od najprostszych pól, by potencjalnie jak najszybciej znaleźć różnicę i zwrócić false. Dla przykładu, gdyby obiekty naszych klas miały tablice z tysiącem elementów i pole nazwa typu String, to należy najpierw porównać do siebie pola nazwa – porównywanie tablic jako pierwszych może być czasochłonne i wpłynąć na wydajność naszego programu.

Podsumowanie

  • Zmienne typów referencyjnych wskazują na obiekty w pamięci – nie są one obiektami, lecz referencjami do obiektów.
  • W poniższym fragmencie kodu tworzone są trzy zmienne mogące pokazywać na obiekty typu Wspolrzedne, ale tylko dwa obiekty tego typu – zmienne w1 i w2 wskazują na ten sam obiekt w pamięci:
    Wspolrzedne w1 = new Wspolrzedne(10, 20);
    Wspolrzedne w2 = w1;
    Wspolrzedne w3 = new Wspolrzedne(1, -5);
    
  • Operator == użyty do porównania dwóch zmiennych typu referencyjnego odpowiada na pytanie: "Czy te zmienne wskazują na ten sam obiekt w pamięci?" – nie porównuje on wartości pól obu obiektów wskazywanych przez zmienne.
  • W związku z powyższym, poniższy kod wypisze "w1 jest rowne w2" oraz "w1 nie jest rowne w3"w1 i w2 są tym samym obiektem, a w1 i w3 – pomimo, że obiekty, na które wskazują, mają takie same wartości – są dwoma różnymi obiektami:
    if (w1 == w2) {
      System.out.println("w1 jest rowne w2");
    } else {
      System.out.println("w1 nie jest rowne w2");
    }
    
    if (w1 == w3) {
      System.out.println("w1 jest rowne w3");
    } else {
      System.out.println("w1 nie jest rowne w3");
    }
    
  • Operatorów == oraz != (różne od) powinniśmy używać tylko wtedy, gdy chcemy sprawdzić, czy dwie zmienne typu referencyjnego wskazują (bądź nie) na ten sam obiekt w pamięci. Możemy je też stosować do sprawdzenia, czy dana zmienna wskazuje na jakiś obiekt, czy nie – poprzez przyrównanie zmiennej do wartości null.
  • Aby porównać dwa obiekty na podstawie wartości, jakie mają ich pola, stosujemy specjalną metodę equals.
  • Metoda equals musi mieć określoną sygnaturę. Metoda equals musi:
    1. przyjmować jako parametr jeden argument typu Object,
    2. być publiczna (posiadać modyfikator public),
    3. zwracać wartość typu boolean.
    public boolean equals(Object o) {
      // implementacja
    }
    
  • W metodzie equals powinniśmy porównać obiekt, na rzecz którego metoda ta została wywołana, do obiektu przesłanego jako argument o nazwie o. Jeżeli, wedle naszych kryteriów, obiekty zostaną uznane za równe, to metoda powinna zwrócić wartość true. Jeżeli obiekty są od siebie różne, powinna zwrócić wartość false.
  • Typ argumentu metody equals to Object. Jest to typ nadrzędny dla wszystkich typów złożonych (klas) w języku Java.
  • W Javie istnieje mechanizm nazywany dziedziczeniem, który pozwala na rozszerzanie istniejących już klas, w wyniku czego nowa klasa posiada wszystkie pola i metody klasy, po której dziedziczy, i może dostarczyć własne pola i metody.
  • Dziedziczenie pozwala na traktowanie obiektów klas nadrzędnych jak obiekty klasy-rodzica, np. obiekty klas Pies i Kot, dziedziczące po klasie Zwierze, można traktować jak obiekty klasy Zwierze.
  • Klasa Object dostarcza domyślną wersję metody equals, która sprawdza, czy przesłany jako argument obiekt jest tym samym obiektem, co this (czyli obiekt, na rzecz którego equals zostało wywołane). Domyślna metoda equals sprawdza więc, czy dwa obiekty są tym samym obiektem w pamięci (tak jak operator == ).
  • Jako argument equals powinniśmy podać Object, ponieważ tak zdefiniowana jest ta metoda w klasie Object. Dostarczając w naszej klasie własną implementację tej metody, powinniśmy zachować taką samą jej sygnaturę.
  • Poza wymaganiami odnośnie sygnatury metody equals, powinna ona także spełniać tzw. kontrakt equals, który jest zestawem pięciu reguł.
  • Wedle kontraktu equals:
    1. Każdy obiekt powinien być równy sobie (metoda equals jest wtedy zwrotna).
    2. Jeżeli obiekt x jest równy y, to powinno z tego wynikać, że y jest równy x (metoda equals jest wtedy symetryczna)
    3. Jeżeli obiekt x i y są równe oraz y i z są równe, to powinno z tego wynikać, że także x i z są sobie równe (metoda equals jest wtedy przechodnia).
    4. Jeżeli x i y są równe/nierówne, i nie zmienimy tych obiektów, to powinny pozostać równe/nierówne (metoda equals jest wtedy spójna).
    5. Żaden nienullowy obiekt nie powinien być uznany za równy nullowi.
  • Nasze programy powinny działać deterministycznie i to mają zapewnić powyższe założenia. Jeżeli naruszylibyśmy np. regułę drugą, to nasz program mógłby zachowywać się w nieprzewidziany sposób – wynik porównania obiektów, które de facto powinny być uznawane za równe, zależałby od kolejności ich porównywania.
  • Kontrakt equals opisany jest w oficjalnej dokumentacji języka Java.
  • Tworzenie metody equals krok po kroku opisane zostało w podrozdziale Krok po kroku – pisanie metody equals.

Pytania

  1. Czym różni się porównywanie obiektów za pomocą operatora porównania == i metody equals?
  2. Czy do przyrównywania zmiennych do wartości null możemy korzystać z operatorów == i !=, czy powinniśmy korzystać z metody equals?
  3. Jak powinna wyglądać sygnatura metody equals?
  4. Czy dla dwóch zmiennych x i y, wskazujących na ten sam obiekt w pamięci, metoda x.equals(y) może zwrócić false?
  5. Na podstawie jakich warunków metoda equals powinna zwrócić true lub false?
  6. Co to jest kontrakt equals?
  7. Jak rzutuje się wartość danego typu na wartość innego typu?
  8. Do czego służy metoda getClass?
  9. Jeżeli klasa PewnaKlasa ma pola int liczba, String tekst, oraz char[] znaki, to jak, biorąc pod uwagę typy pól klasy PewnaKlasa, powinniśmy sprawdzić równość dwóch obiektów tej klasy?
  10. Do czego służy metoda Arrays.equals, przyjmująca jako argumenty dwie tablice?
  11. Czy metoda equals, gdy jej argumentem jest null, na przykład x.equals(null), może zwrócić true?

    // klasa na potrzeby zadania 12, 13, 14
    public class A {
      private int liczba;
    
      public A(int liczba) {
        this.liczba = liczba;
      }
    }
    
  12. Biorąc pod uwagę klasę A zdefiniowaną powyżej, jaki będzie wynik uruchomienia poniższego fragmentu kodu? Czy kod w ogóle się skompiluje?
    public static void main(String[] args) {
      A a1 = new A(10);
      A a2 = a1;
      A a3 = new A(10);
    
      System.out.println("a1 rowne a2? " + a1.equals(a2));
      System.out.println("a1 rowne a3? " + a1.equals(a3));
    }
    
  13. Czy poniższa metoda equals byłaby poprawna dla klasy A?
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
    
      if (o == null || this.getClass() != o.getClass()) {
        return false;
      }
    
      return this.liczba == o.liczba;
    }
    
  14. Jaki będzie wynik działania poniższej metody main, gdyby klasa A miała załączoną poniżej metodę equals?
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
    
      if (this.getClass() != o.getClass() || o == null) {
        return false;
      }
    
      A other = (A) o;
    
      return this.liczba == other.liczba;
    }
    
    public static void main(String[] args) {
      A a1 = new A(10);
      A a2 = a1;
      A a3 = new A(10);
    
      System.out.println("a1 rowne a2? " + a1.equals(a2));
      System.out.println("a1 rowne a3? " + a1.equals(a3));
      System.out.println("a1 rowne null? " + a1.equals(null));
    }
    

Odpowiedzi do pytań

Zadania

Klasa Punkt z equals

Napisz klasę Punkt, która będzie zawierała punkt na płaszczyźnie opisany przez dwie wartości x oraz y (pola typu int). Napisz konstruktor inicjalizujący pola x i y, a także zaimplementuj metodę equals. Sprawdź, czy metoda działa zgodnie z założeniami.

Klasa Figura z equals

Napisz klasę Figura, która będzie zawierała tablicę obiektów typu Punkt. Pole z tablicą nazwij wierzcholki. Napisz konstruktor inicjalizujący pole wierzcholki, a także zaimplementuj metodę equals dla klasy Figura. Skorzystaj z metody Arrays.equals do porównania tablic.

Rozwiązania do zadań

Komentarze (6):

  1. Dlaczego nie można (to nie rozwiązuje problemu) na początku metody equals dodać częgoś takiego:

    if (this==null && o==null) {
    return true;
    }

    Pozdrawiam.

    1. Nie ma to sensu, ponieważ nie można wywoływać metod na rzecz obiektów, które są nullem. Null to brak wartości, brak obiektu, więc nie można wywoływać jego metod - w takim przypadku rzucony zostanie wyjątek NullPointerException.

  2. Dzień dobry,
    dla klasy Osoba w equals musiałem zmienić kalsę na static aby się skompil;owała:
    public static class Osoba { // why i had to change to static?

    Pana equals dla Osoba nie jest symetryczna dla jednej albo obu osób równych null.
    W dwu na trzy przypadki - NullPointerExeption.

    Pozdrawiam.

    1. Nie ma sensu wywoływać equals na rzecz obiektu, który jest nullem. Metoda ma być symetryczna dla dwóch nie-nullowych obiektów. Czy może Pan napisać jakie były błędy związane z brakiem static? Coś jest prawdopodobnie nie tak w Pańskim pliku źródłowym.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Nie musisz podawać swojego imienia, e-mailu, ani strony www, aby opublikować komentarz. Komentarze muszą zostać zatwierdzone, aby były widoczne na stronie.