Programowanie obiektowe

Wykład 7

Dziedziczenie i polimorfizm - cd.

Metoda equals

Metoda equals() w Javie jest używana do porównywania dwóch obiektów pod kątem równości. Standardowa implementacja equals() w klasie Object porównuje referencje, co oznacza, że dwa obiekty są uznawane za równe, jeśli wskazują na to samo miejsce w pamięci. W praktyce często chcemy porównywać obiekty na podstawie ich stanu wewnętrznego (wartości ich pól), co wymaga nadpisania metody equals().

Oto zasady tworzenia dobrze zaprojektowanej metody equals():

  1. Użycie słowa kluczowego @Override: Wskazuje, że metoda nadpisuje metodę z klasy bazowej.

  2. Sprawdzenie, czy obiekt nie jest porównywany sam do siebie: Jeśli tak, metoda powinna zwrócić true.

  3. Sprawdzenie, czy obiekt do porównania nie jest null i czy jest tego samego typu.

  1. Rzutowanie obiektu do porównania na odpowiedni typ: Po potwierdzeniu, że obiekty są tego samego typu, można bezpiecznie rzutować.

  2. Porównanie pól obiektów: Porównaj wszystkie istotne pola. Dla pól, które są typami prymitywnymi, użyj operatorów porównania (==). Dla obiektów użyj equals().

  3. Zwrócenie true, jeśli wszystkie porównania pól są równe: W przeciwnym razie zwróć false.

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object obj) {
        // Sprawdzenie, czy obiekty są tym samym obiektem
        if (this == obj) return true;

        // Sprawdzenie, czy obj nie jest null i czy jest typu Person
        if (obj == null || getClass() != obj.getClass()) return false;

        // Rzutowanie obj do typu Person
        Person person = (Person) obj;

        // Porównanie pól
        if (age != person.age) return false;
        return name != null ? name.equals(person.name) : person.name == null;
    }

}
  • Kontrakt equals() i hashCode(): Jeśli nadpisujesz equals(), musisz także nadpisać hashCode(). Kontrakt między tymi dwiema metodami mówi, że dwa obiekty uznane za równe przez equals() muszą zwracać tę samą wartość hashCode().
  • Null-safety: Używanie Objects.equals() z Java 7 i nowszych w celu uniknięcia NullPointerException podczas porównywania pól, które mogą być null.
  • Porównanie pól typu float i double: Dla pól typu float użyj Float.compare(float, float), a dla double - Double.compare(double, double), aby uniknąć problemów z porównywaniem liczb zmiennoprzecinkowych.
class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object obj) {
        // Sprawdzenie, czy obiekty są tym samym obiektem
        if (this == obj) return true;

        // Sprawdzenie, czy obj jest instancją Person i rzutowanie
        if (obj instanceof Person person) {
            // Porównanie pól
            if (age != person.age) return false;
            return name != null ? name.equals(person.name) : person.name == null;
        }

        return false;
    }

    // Gettery, settery, hashCode(), toString() itp.
}
class Employee extends Person {
    private String department;

    public Employee(String name, int age, String department) {
        super(name, age);
        this.department = department;
    }

    @Override
    public boolean equals(Object obj) {
        // Sprawdzenie równości z perspektywy klasy bazowej
        if (!super.equals(obj)) return false;

        // Rzutowanie obj do typu Employee
        Employee employee = (Employee) obj;

        // Porównanie dodatkowego pola
        return department != null ? department.equals(employee.department) : employee.department == null;
    }

    // Gettery, settery, hashCode(), toString() itp.
}

Porównywanie typów prymitywnych

  • typy prymitywne: byte, short, int, long, float, double, char, i boolean
  • używamy zwykle == oraz !=
  • ostrożnie z float i double
  1. Operator == i !=: Używanie tych operatorów jest bezpieczne tylko wtedy, gdy porównujemy konkretne, dobrze zdefiniowane wartości float lub double, które nie są wynikiem obliczeń. Na przykład, porównywanie stałych lub przypisanych wartości może być bezpieczne. Jednak w przypadku wartości wynikających z obliczeń matematycznych, nawet niewielkie błędy zaokrąglenia mogą prowadzić do nieoczekiwanych wyników.
  1. Metoda equals: Metoda equals dla obiektów typu Float i Double sprawdza, czy dwa obiekty są dokładnie takie same, co oznacza, że również może prowadzić do problemów podobnych do tych związanych z użyciem == i !=, ponieważ również porównuje dokładne wartości bitowe.

2a. Metoda compare: nieco lepsza do porównywania. Zwraca liczbę całkowitą w zależności od relacji między argumentami.

double a = 1.2;
double b = 1.1+0.1;
System.out.println(a == b);
System.out.println(Double.compare(a, b)==0);
  1. Porównywanie z określoną tolerancją: Najbezpieczniejszym podejściem jest porównywanie liczb zmiennoprzecinkowych z określoną tolerancją. Możesz to zrobić, sprawdzając, czy absolutna różnica między dwoma liczbami jest mniejsza niż pewna mała wartość epsilon, która reprezentuje akceptowalny błąd zaokrąglenia.
double epsilon = 0.000001;
double a = 0.1;
double b = 0.1 + 0.0000001;
if (Math.abs(a - b) < epsilon) {
   System.out.println("a i b są wystarczająco blisko siebie");
}

Wybór wartości epsilon zależy od kontekstu i wymaganej precyzji.

  1. Użycie BigDecimal: W niektórych przypadkach, gdzie wymagana jest bardzo wysoka precyzja, można użyć klasy BigDecimal. Pozwala ona na dokładne porównywanie liczb zmiennoprzecinkowych, ale kosztem wydajności i zwiększonej złożoności kodu.

Porównywanie napisów

  1. Operator == i !=: Te operatory porównują referencje, a nie faktyczną zawartość napisów. Oznacza to, że == zwróci true tylko wtedy, gdy obie zmienne odnoszą się do tego samego obiektu w pamięci. Jest to zwykle niewłaściwe do porównywania zawartości napisów.

  2. Metoda equals: Ta metoda porównuje zawartość dwóch napisów i jest właściwym sposobem porównywania napisów pod kątem ich wartości. Metoda equals jest czuła na wielkość liter.

  3. Metoda equalsIgnoreCase: Podobnie jak equals, ale ignoruje różnice w wielkości liter.

  4. Object.equals: Jest to ogólna metoda porównywania w Javie. Można jej używać do porównywania napisów, gdy oba mogą być null. To podejście jest bezpieczne, ponieważ unika ryzyka wystąpienia wyjątku NullPointerException.

public class StringCompareExample {
    public static void main(String[] args) {
        String str1 = new String("Hello");
        String str2 = new String("Hello");
        String str3 = "Hello";
        String str4 = null;

        // Porównywanie za pomocą ==
        System.out.println("Porównanie za pomocą == : " + (str1 == str2)); // Zwróci false
        System.out.println("Porównanie referencji : " + (str3 == "Hello")); // Zwróci true

        // Porównywanie za pomocą equals
        System.out.println("Porównanie za pomocą equals : " + str1.equals(str2)); // Zwróci true

        // Porównywanie ignorując wielkość liter
        System.out.println("Porównanie za pomocą equalsIgnoreCase : " + str1.equalsIgnoreCase("hello")); // Zwróci true

        // Bezpieczne porównywanie za pomocą Object.equals
        System.out.println("Bezpieczne porównanie : " + java.util.Objects.equals(str1, str4)); // Zwróci false
    }
}

hashCode()

Metoda hashCode() jest fundamentalną częścią klasy Object i ma kluczowe znaczenie w działaniu kolekcji takich jak HashSet, HashMap i Hashtable, które wykorzystują hashowanie do efektywnego przechowywania i odzyskiwania obiektów.

  1. Metoda hashCode() zwraca wartość całkowitą, która jest używana jako kod hash obiektu. Ten kod hash służy do określenia miejsca obiektu w strukturze danych opartej na hashowaniu.

  2. Kontrakt hashCode():

    • Spójność: Wartość zwracana przez hashCode() musi pozostać stała dla danego obiektu podczas jego życia, o ile informacje użyte w porównaniach equals() na tym obiekcie nie ulegną zmianie.
    • Równość: Jeśli dwa obiekty są równe zgodnie z metodą equals(), wtedy ich kody hash zwrócone przez hashCode() również muszą być równe.
    • Nierówność: Jeśli dwa obiekty są nierówne zgodnie z equals(), preferowane jest, aby ich kody hash były różne. Jednak nie jest to wymóg i różne obiekty mogą mieć tę samą wartość hash (co prowadzi do tzw. kolizji hash).
@Override
public int hashCode() {
    int result = 17; // Można zacząć od dowolnej liczby niezerowej
    result = 31 * result + age;
    result = 31 * result + (name != null ? name.hashCode() : 0);
    return result;
}
@Override
public int hashCode() {
    return  super.hashCode() + (department != null ? department.hashCode() : 0);
}

hashCode dla typów prymitywnych

  1. Dla typów int, short, byte, char, boolean: Dla tych typów, wartość prymitywna może być bezpośrednio użyta jako wartość hashcode, ponieważ sama w sobie jest wystarczająco unikalna. Na przykład, dla typu int, wartość hashcode to po prostu wartość tego int.
  1. Dla typu long: Typ long ma 64 bity, więc nie można go bezpośrednio przypisać jako 32-bitowy hashcode. Można użyć następującej metody:
long value = 123456789L;
int hash = (int)(value ^ (value >>> 32));

  1. Dla typów float i double:
  • Dla float, można użyć metody Float.floatToIntBits:
float floatValue = 10.5f;
int hash = Float.floatToIntBits(floatValue);
  • Dla double, proces jest bardziej skomplikowany, ponieważ double jest 64-bitowy. Najpierw konwertujesz go na long za pomocą Double.doubleToLongBits, a następnie postępujesz podobnie jak w przypadku long:
double doubleValue = 123.456;
long longBits = Double.doubleToLongBits(doubleValue);
int hash = (int)(longBits ^ (longBits >>> 32));
  • opakowanie w typ wrapper
double value = 10.5;
int hash = Double.valueOf(value).hashCode();
  1. Dla boolean: Można po prostu przypisać 1 dla true i 0 dla false:

    boolean booleanValue = true;
    int hash = booleanValue ? 1 : 0;
  2. Wykorzystanie java.util.Objects.hash: Jeśli chcesz obliczyć hashcode dla kombinacji kilku wartości, w tym typów prymitywnych, możesz użyć Objects.hash:

    int a = 5;
    double b = 10.5;
    int hash = Objects.hash(a, b);

Przykładowe zadanie egzaminacyjne

Zadanie 1. Klasa School (pol. Szkoła)

A. Klasa School powinna być umieszczona w pakiecie education.

B. Klasa powinna posiadać prywatne pola:

  • name, (nazwa szkoły), typ String
  • address, (adres zawierający ulicę, numer posesji, kod pocztowy i miejscowość), typ String
  • students, (liczba uczniów), typ int

C. Napisz trzyargumentowy konstruktor tej klasy. Kolejność argumentów powinna być taka sama jak w punkcie B. Zapewnij niezależnie warunki sprawdzające poprawność:

  • adres nie może być pusty lub równy null (równe "") - wtedy ustaw adres WMII czyli "ul. Słoneczna 54, 10-710 Olsztyn
  • liczba uczniów musi być liczbą dodatnią, w przeciwnym wypadku ustaw ją na 100.
  • zwróć uwagę na wielkość znaków i znaki interpunkcyjne

D. Napisz metody typu getter i setter dla wszystkich pól. Pamiętaj by sprawdzić kryteria podane w konstruktorze. W przypadku błędny argumentów, metoda ma nic nie robić.

E. Nadpisz metodę toString tak, aby zwracała napis z reprezentacją obiektu. Na początku powinna być nazwa klasy - potem wartości wszystkich pól. Powinno odbyć się do według schematu (zwróć uwagę na wielkość znaków i znaki interpunkcyjne, wszystko w jednej linii):

[NazwaKlasy]: Name: [name]. Address: [address]. Number of students: [students].

lub jeśli nazwa nie jest ustalona (jest pustym napisem lub nullem):

[NazwaKlasy]: Address: [address]. Number of students: [students].

F. Nadpisz metodę equals. Dwie szkoły są sobie “równe” wtedy i tylko wtedy, gdy mają ten sam adres. Nadpisz metodę hashCode(), która generuje kod hash dla odpowiedniego obiektu. Metoda ta powinna być zgodna z metodą equals(),

G. Napisz metodę (zwykłą) recruitment (pol. rekrutacja) z argumentem typu int. Metoda powiększa pole students o wartość przekazaną przez argument. Jeśli po powiększeniu pole students będzie większe niż 500, to ustaw je na 500.

Zadanie 2. Klasa University (pol. uniwersytet)

A. Klasa University powinna być umieszczona w pakiecie education w innym pliku niż klasa School.

B. Klasa University dziedziczy po klasie School. Klasa powinna posiadać prywatne pola:

  • type typu String (np. rodzaj np. rolniczy - agricultural, politechnika - university of technology, itp)
  • studies typu int (liczba kierunków)

C. Napisz pięcio-argumentowy konstruktor tej klasy. Kolejność argumentów powinna być taka sama jak w punkcie B (najpierw z klasy bazowej, potem pochodnej). Zapewnij niezależnie warunki sprawdzające poprawność dodatkowo:

  • typ powinien nie być pusty (równy "") - w przeciwnym wypadku ustaw "university of technology"
  • liczba kierunków musi być liczbą nieujemną - w przeciwnym wypadku ustaw ją jako 10.

D. Napisz metody typu getter i setter dla wszystkich pól. Pamiętaj by sprawdzić kryteria podane w konstruktorze. W przypadku błędnych argumentów, metoda ma nic nie robić.

E. Nadpisz metodę toString tak, aby zwracała napis z reprezentacją obiektu. Na początku powinna być nazwa klasy - potem wartości wszystkich pól. Powinno odbyć się do według schematu (zwróć uwagę na wielkość znaków i znaki interpunkcyjne, zwróć uwagę na łamanie linii):

[NazwaKlasy]: Name: [name]. Address: [address]. Number of students: [students].
Type: [type]. Number of fields of study: [studies].

lub jeśli nazwa nie jest ustalona (jest pustym napisem lub nullem):

[NazwaKlasy]: Address: [address]. Number of students: [students].
Type: [type]. Number of fields of study: [studies].

F. Nadpisz metodę (zwykłą) recruitment z argumentem typu int. Metoda powiększa pole students o wartość przekazaną przez argument. Jeśli po powiększeniu pole students będzie większe niż 500, to ustaw je na 500. Dodatkowo zwiększ liczbę kierunków o 1/10 przekazanego argumentu (w zaokrągleniu lub obcięciu do liczby całkowitej).

G. Nadpisz metodę equals. Dwa obiekty są sobie “równe” wtedy i tylko wtedy, gdy mają ten sam adres oraz tą samą liczbę kierunków. Nadpisz metodę hashCode(), która generuje kod hash dla odpowiedniego obiektu. Metoda ta powinna być zgodna z metodą equals(),

Zadanie 3. Klasa TestSchool (pol. klasa testująca dla szkoły)

A. Klasę TestSchool umieść bezpośrednio w katalogu src poza pakietami. Umieść w tej klasie tylko metodę main.

B. Wywołaj wszystkie metody z zadania 1 i 2 (np. zwykłe, statyczne, konstruktory). Wywołanie getter-ów i setter-ów nie jest obowiązkowe.

Nadpisywanie i przesłanianie - jeszcze raz

W Javie często, ale nie zawsze:

  • override - nadpisywanie - zwykłe metody
  • hidding - przesłanie - statyczne metody (brak użycia adnotacji)
class BaseClass {
    public static void staticMethod() {
        System.out.println("Static method in BaseClass");
    }

    public void instanceMethod() {
        System.out.println("Instance method in BaseClass");
    }
}

class DerivedClass extends BaseClass {
    public static void staticMethod() {
        System.out.println("Static method in DerivedClass");
    }

    @Override
    public void instanceMethod() {
        System.out.println("Instance method in DerivedClass");
    }
}