Programowanie obiektowe

Wykład 8

Dziedziczenie i polimorfizm - cd.

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.

Modyfikator protected w Javie?

  1. Dostępność w Tym Samym Pakiecie: Pola, metody lub konstruktory oznaczone jako protected są dostępne dla innych klas w tym samym pakiecie.

  2. Dostępność w Klasach Pochodnych: Co ważniejsze, protected pozwala na dostęp do elementów członkowskich klasy w jej klasach pochodnych, nawet jeśli te klasy pochodne są w innym pakiecie.

Często jednak hermetyzacja jest ważniejsza w zakresie pól/zmiennych w klasie.

Teoretycznie o polimorfizmie

Polimorfizm to kluczowe pojęcie w programowaniu obiektowym, umożliwiające obiektom zachowanie się w różny sposób w zależności od ich rzeczywistego typu. W Javie i innych językach obiektowych, polimorfizm jest realizowany głównie na dwa sposoby: statyczny i dynamiczny.

Polimorfizm Statyczny (Przeciążanie)

Polimorfizm statyczny, znany również jako przeciążanie (ang. overloading), odnosi się do możliwości definiowania w jednej klasie wielu metod o tej samej nazwie, ale z różnymi listami parametrów. Decyzja o tym, która wersja metody zostanie wywołana, dokonywana jest w czasie kompilacji na podstawie sygnatury metody, którą wywołuje kod. Kluczowe aspekty polimorfizmu statycznego to:

  • Różne Sygnatury: Przeciążone metody muszą się różnić listą parametrów - mogą mieć różną liczbę parametrów, typy parametrów lub obie te rzeczy.
  • Ta Sama Klasa lub Klasy Pochodne: Metody muszą być zdefiniowane w tej samej klasie lub w klasach pochodnych.
  • Decyzja w Czasie Kompilacji: Która metoda zostanie wywołana, jest decydowane w czasie kompilacji, na podstawie typu referencji, nie rzeczywistego obiektu.
public class ExampleClass {
    public void display(int a) {
        System.out.println("Liczba całkowita: " + a);
    }

    public void display(String b) {
        System.out.println("Łańcuch znaków: " + b);
    }
}

Polimorfizm Dynamiczny (Nadpisywanie)

Polimorfizm dynamiczny, znany także jako nadpisywanie (ang. overriding), odnosi się do możliwości zastępowania metod klasy bazowej w klasach pochodnych. W tym przypadku metody w klasie pochodnej mają tę samą sygnaturę co metody w klasie bazowej. Kluczowe aspekty polimorfizmu dynamicznego to:

  • Ta Sama Sygnatura: Nadpisane metody w klasie pochodnej muszą mieć tę samą sygnaturę co metody w klasie bazowej.
  • Decyzja w Czasie Wykonania: Która metoda zostanie wywołana, jest decydowane w czasie wykonania programu, na podstawie rzeczywistego typu obiektu, a nie typu referencji.
  • Dziedziczenie: Polimorfizm dynamiczny wykorzystuje dziedziczenie do zastępowania metod.
class BaseClass {
    public void display() {
        System.out.println("Wyświetlanie z klasy bazowej");
    }
}

class DerivedClass extends BaseClass {
    @Override
    public void display() {
        System.out.println("Wyświetlanie z klasy pochodnej");
    }
}

public class TestPolymorphism {
    public static void main(String[] args) {
        BaseClass obj = new DerivedClass();
        obj.display(); // Wywoła metodę z klasy DerivedClass
    }
}

Podsumowanie

  • Polimorfizm Statyczny: Wiąże się z przeciążaniem metod; decyzja o wyborze metody jest dokonywana w czasie kompilacji.
  • Polimorfizm Dynamiczny: Wiąże się z przesłanianiem metod; decyzja o wyborze metody jest dokonywana w czasie wykonania programu.

Polimorfizm, zarówno statyczny, jak i dynamiczny, jest fundamentalnym elementem programowania obiektowego, umożliwiającym większą elastyczność i możliwość ponownego wykorzyst

Przepis na niemodyfikowalną klasę w Javie

  1. Klasa powinna być oznaczona jako final, aby zapobiec jej rozszerzeniu (dziedziczeniu), co mogłoby pozwolić na zmianę jej zachowania.
  2. Wszystkie pola w klasie powinny być oznaczone jako final, co oznacza, że ich wartości mogą być przypisane tylko raz.
  3. Klasa nie powinna zawierać setterów ani innych metod, które mogłyby zmienić stan obiektu po jego utworzeniu.
  4. Wszystkie wartości pól powinny być ustawiane wyłącznie przez konstruktor.
  5. Jeśli klasa zawiera pola, które są referencjami do innych obiektów, te obiekty również powinny być niezmienne. Jeśli nie jest to możliwe (np. jeśli są to kolekcje lub klasy zewnętrzne), klasa powinna zwracać kopie tych obiektów, a nie ich bezpośrednie referencje.
import java.util.List;
import java.util.ArrayList;

public final class MyImmutableClass {
    private final int value;
    private final String name;
    private final ArrayList<String> items;

    public MyImmutableClass(int value, String name, ArrayList<String> items) {
        this.value = value;
        this.name = name;
        this.items = new ArrayList<>(items); // Tworzenie kopii listy
    }

    public int getValue() {
        return value;
    }

    public String getName() {
        return name;
    }

    public ArrayList<String> getItems() {
        return new ArrayList<>(items); // Zwraca kopię listy, a nie oryginał
    }
}

Rekord

Rekordy (ang. records) to funkcjonalność wprowadzona w Javie w wersji 16 jako sposób na uproszczenie definicji klas, które głównie służą jako pojemniki danych. Rekordy są szczególnym rodzajem klas, które automatycznie dostarczają implementacji metod takich jak equals(), hashCode() i toString() na podstawie zadeklarowanych pól. Są one szczególnie przydatne w przypadkach, gdy klasa jest używana do modelowania prostych struktur danych.

Właściwości rekordu

  1. Niezmienność: Wszystkie pola rekordu są niezmienne (final), co oznacza, że ich wartości nie mogą być zmienione po utworzeniu obiektu rekordu.

  2. Automatyczne Metody: Java automatycznie generuje metody equals(), hashCode() i toString(), które uwzględniają wszystkie pola rekordu.

  3. Kompaktowa Składnia: Rekordy pozwalają na zwięzłą definicję klas bez konieczności pisania boilerplate code, tzn. kodu, który jest powtarzany w wielu miejscach z niewielkimi zmianami.

  4. Konstruktory: Rekordy dostarczają domyślny konstruktor, który przyjmuje wszystkie zadeklarowane pola jako argumenty, ale można także zdefiniować własne konstruktory.

record Person(String name, int age) {}

public class TestPerson {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);

        System.out.println(person);
        System.out.println("Name: " + person.name());
        System.out.println("Age: " + person.age());
    }
}

W tym przykładzie Person jest rekordem z dwoma polami: name i age. Java automatycznie generuje metody name() i age() jako gettery do pól rekordu, a także implementacje metod equals(), hashCode() i toString().

Rozszerzenie

Rekordy można rozszerzać o dodatkowe metody. Jednak nie mogą one dziedziczyć po innych klasach (ponieważ już dziedziczą po java.lang.Record) i nie mogą mieć klas pochodnych.

public record Employee(String name, int id) {
    public String employeeInfo() {
        return "Employee Info: Name = " + name + ", ID = " + id;
    }
}

public class TestEmployeee {
    public static void main(String[] args) {
        Employee employee = new Employee("Bob", 101);
        System.out.println(employee.employeeInfo());
    }
}

Tworzenie Niestandardowego Konstruktora

  1. Wywołanie Domyślnego Konstruktora: Niestandardowy konstruktor powinien wywołać domyślny konstruktor rekordu. W Javie robi się to za pomocą wywołania this(), które przekazuje odpowiednie argumenty do domyślnego konstruktora.

  2. Walidacja i Przetwarzanie Danych: Możesz dodać logikę do niestandardowego konstruktora, aby sprawdzić poprawność danych lub przetworzyć je przed przypisaniem do pól rekordu.

record Person(String name, int age) {

    // Niestandardowy konstruktor bez argumentów
    public Person() {
        this("Nieznane", 0); // Wywołanie domyślnego konstruktora z wartościami domyślnymi
    }
}

public class TestPerson {
    public static void main(String[] args) {
        // Tworzenie instancji Person za pomocą niestandardowego konstruktora
        Person defaultPerson = new Person();
        System.out.println("Domyślna osoba: " + defaultPerson);

        // Tworzenie instancji Person z konkretnymi danymi
        Person alice = new Person("Alice", 30);
        System.out.println("Osoba: " + alice);
    }
}

Forma kompaktowa

record Person(String name, int age) {

    // Niestandardowy konstruktor
    public Person {
        if (name == null || name.isBlank()) {
            System.out.println("Uwaga: Podano puste imię.");
            name = "Nieznane";
        }
        if (age < 0) {
            System.out.println("Uwaga: Podano ujemny wiek. Ustawiamy na 0.");
            age = 0;
        }
    }
}

public class TestPerson {
    public static void main(String[] args) {
        Person person1 = new Person("Alice", 30);
        System.out.println(person1);

        // Przykłady, które spowodują ostrzeżenia w konstruktorze
        Person person2 = new Person("", -5);
        System.out.println(person2);
    }
}

Podsumowanie dziedziczenia

Rady przy projektowaniu:

  1. Zachowanie Is-a Relationship: Dziedziczenie jest odpowiednie, gdy nowa klasa jest specjalnym przypadkiem klasy bazowej. Na przykład, jeśli masz klasę Vehicle (pojazd), klasy takie jak Car (samochód) i Bike (rower) mogą być jej rozszerzeniami, ponieważ samochód i rower są rodzajami pojazdów.
  1. Ponowne Wykorzystanie Kodu: Jeśli nowa klasa powinna zawierać funkcjonalność klasy bazowej, dziedziczenie pozwala na ponowne wykorzystanie kodu bez jego powielania. Dzięki temu zmiany w klasie bazowej automatycznie odzwierciedlają się we wszystkich klasach pochodnych, co ułatwia utrzymanie i rozwój kodu.
  1. Rozszerzenie Funkcjonalności: Dziedziczenie jest użyteczne, gdy chcesz rozszerzyć lub modyfikować funkcjonalność klasy bazowej, dodając nowe pola, metody lub nadpisując istniejące metody.

  2. Polimorfizm: Dziedziczenie umożliwia polimorfizm, który jest sposobem na interakcję z obiektami różnych klas pochodnych za pomocą referencji klasy bazowej. Umożliwia to pisanie bardziej generycznego i elastycznego kodu.

  1. Hierarchie Klas: W sytuacjach, gdy istnieje naturalna hierarchia klas, dziedziczenie może być stosowane do odzwierciedlenia tej hierarchii w kodzie. Na przykład, w systemie zarządzania personelem, można mieć klasę Employee z klasami pochodnymi takimi jak Manager, Technician, itd.

Istnieją również sytuacje, gdy dziedziczenie nie jest najlepszym rozwiązaniem:

  • Unikaj Dziedziczenia dla Dziedziczenia: Nie używaj dziedziczenia tylko dlatego, że dwie klasy mają wspólne cechy. Jeśli relacja “is-a” nie jest całkowicie jasna, lepiej rozważyć inne podejścia, takie jak kompozycja.

  • Klasa Bazowa Zbyt Ogólna: Jeśli klasa bazowa jest zbyt ogólna i wymaga wielu modyfikacji w klasach pochodnych, prawdopodobnie nie jest to dobry przypadek do użycia dziedziczenia.

  • Zbyt Wiele Poziomów Dziedziczenia: Unikaj głębokich hierarchii dziedziczenia, ponieważ mogą one prowadzić do złożonego i trudnego do zrozumienia kodu.

Łamigłówka

Autor: dr Krzysztof Sopyła

public class Main {

    public static void main(String[] args) {
        F obj = new F();
        obj.normal();
        obj.virt1();
        obj.virt2();
    }
}


class A {
    public void normal() {
        System.out.println("A.Normal()");
    }

    public void virt1() {
        System.out.println("A.Virt1()");
        virt2();
    }

    public void virt2() {
        System.out.println("A.Virt2()");
    }
}

class B extends A {
    public void normal() {
        System.out.println("B.Normal()");
    }

    public void virt1() {
        System.out.println("B.Virt1()");
    }

    @Override
    public void virt2() {
        System.out.println("B.Virt2()");
    }
}

class C extends B {
    @Override
    public void virt1() {
        System.out.println("C.Virt1()");
    }
}

class D extends C {
    public void normal() {
        System.out.println("D.Normal()");
    }

    @Override
    public void virt1() {
        System.out.println("D.Virt1()");
    }

    @Override
    public void virt2() {
        System.out.println("D.Virt2()");
    }
}

abstract class E extends D {
    public void virt1() {
        System.out.println("E.Virt1()");
    }

    public abstract void virt2();
}

class F extends E {
    @Override
    public void virt1() {
        System.out.println("F.Virt1()");
    }

    @Override
    public void virt2() {
        System.out.println("F.Virt2()");
    }
}

Łamigłówka

Autor: dr Krzysztof Sopyła.

Wersja oryginalna w C#:

public class A {
    public void Normal() { Console.WriteLine("A.Normal()"); }
    virtual public void Virt1() { Console.WriteLine("A.Virt1()"); Virt2();}
    virtual public void Virt2(){  Console.WriteLine("A.Virt2()"); }
}


public class B : A {
    public void Normal() { Console.WriteLine("B.Normal()"); }
    public virtual void Virt1() { Console.WriteLine("B.Virt1()"); }
    public override void Virt2() { Console.WriteLine("B.Virt2()"); }
}

public class C : B {
    public override void Virt1() { Console.WriteLine("C.Virt1()"); }
}

public class D : C {
    public void Normal() { Console.WriteLine("D.Normal()"); }
    public override void Virt1() { Console.WriteLine("D.Virt1()"); }
    public override void Virt2() { Console.WriteLine("D.Virt2()"); }
}
public abstract class E : D {
    public virtual void Virt1() { Console.WriteLine("E.Virt1()"); }
    public abstract void Virt2();
}

public class F : E {
    public override void Virt1() { Console.WriteLine("F.Virt1()"); }
    public override void Virt2() { Console.WriteLine("F.Virt2()"); }
}

Związki między klasami

Dziedziczenie

Już było :)

Asocjacja (Association)

Asocjacja to związek między dwoma klasami, które są ze sobą powiązane, ale mogą istnieć niezależnie.

class Driver {
    private String name;

    Driver(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

class Car {
    private Driver driver;

    public void setDriver(Driver driver) {
        this.driver = driver;
    }

    public Driver getDriver() {
        return driver;
    }
}

public class Main {
    public static void main(String[] args) {
        Driver driver = new Driver("Alice");
        Car car = new Car();
        car.setDriver(driver);
    }
}

Agregacja (Aggregation)

Agregacja to specjalny rodzaj asocjacji, w której obie klasy mogą istnieć niezależnie, ale jedna z nich (całość) zawiera drugą (część).

class Wheel {
    // Detale implementacji
}

class Car {
    private Wheel wheel;

    public Car(Wheel wheel) {
        this.wheel = wheel;
    }
}

public class Main {
    public static void main(String[] args) {
        Wheel wheel = new Wheel();
        Car car = new Car(wheel);
    }
}

Kompozycja (Composition)

Kompozycja to bardziej rygorystyczna forma agregacji, gdzie część nie może istnieć bez całości.

class Engine {
    // Detale implementacji
}

class Car {
    private Engine engine;

    public Car() {
        this.engine = new Engine(); // Engine jest nierozerwalnie związany z Car
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car();
    }
}

Realizacja Interfejsu (Interface Implementation)

W Javie klasy mogą implementować interfejsy. Oznacza to, że klasa zgadza się dostarczyć implementacje dla wszystkich metod zadeklarowanych w interfejsie.

interface Drivable {
    void drive();
}

class Car implements Drivable {
    public void drive() {
        System.out.println("Samochód jedzie");
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.drive();
    }
}

Parę przykładów na podsumowanie


import java.util.ArrayList;
import java.util.Objects;

public class Student {

    private String name;
    private ArrayList<Double> grades;

    public Student(String name, ArrayList<Double> grades) {
        this.name = name != null ? name : "";
        this.grades = grades != null ? new ArrayList<>(grades) : new ArrayList<>();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name != null ? name : "";
    }

    public ArrayList<Double> getGrades() {
        return new ArrayList<>(grades);
    }

    public void setGrades(ArrayList<Double> grades) {
        this.grades = grades != null ? new ArrayList<>(grades) : new ArrayList<>();
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", grades=" + grades +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
//      return name.equals(student.name) && grades.equals(student.grades);
        return Objects.equals(name, student.name) && Objects.equals(grades, student.grades);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, grades);
    }
}
import java.util.ArrayList;
import java.util.Arrays;

public class TestStudent {

    public static void main(String[] args) {
        ArrayList<Double> grades = new ArrayList<>(Arrays.asList(5.5, 4.0, 3.5));
        Student student = new Student("Jan Kowalski", grades);
        System.out.println(student);
        student.setName("Anna Nowak");
        student.getGrades().add(2.0);
        System.out.println(student);
        ArrayList<Double> grades2 = new ArrayList<>(Arrays.asList(5.5, 4.0, 3.5));
        Student student2 = new Student("Anna Nowak", grades2);
        System.out.println(student.equals(student2));
        System.out.println(student.hashCode());
        System.out.println(student2.hashCode());
    }
}

import java.util.Arrays;
import java.util.Objects;

public class Student {

    private String name;
    private double[] grades;

    public Student(String name, double[] grades) {
        this.name = name != null ? name : "";
        this.grades = grades != null ? Arrays.copyOf(grades, grades.length) : new double[0];
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name != null ? name : "";
    }

    public double[] getGrades() {
        return Arrays.copyOf(grades, grades.length);
    }

    public void setGrades(double[] grades) {
        this.grades = grades != null ? Arrays.copyOf(grades, grades.length) : new double[0];
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", grades=" + Arrays.toString(grades) +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return Objects.equals(name, student.name) && Arrays.equals(grades, student.grades);
        
        // uwaga: zastąpienie Object.equals dla tablicy prymitywów nie jest poprawne
    }

    @Override
    public int hashCode() {
        int result = Objects.hash(name);
        result = 31 * result + Arrays.hashCode(grades);
        return result;
    }
}
public class TestStudent {

    public static void main(String[] args) {
        double[] grades = new double[]{5.5, 4.0, 3.5};
        Student student = new Student("Jan Kowalski", grades);
        System.out.println(student);
        student.setName("Anna Nowak");
        double[] grades1 = student.getGrades();
        grades1[0] = 2.0;
        System.out.println(student);
        double[] grades2 = new double[]{5.5, 4.0, 3.5};
        Student student2 = new Student("Anna Nowak", grades2);
        System.out.println(student.equals(student2));
        System.out.println(student.hashCode());
        System.out.println(student2.hashCode());
    }
}

import java.util.Objects;

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

    public Author(String name, int age) {
        this.name = name != null ? name : "";
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name != null ? name : "";
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Author{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Author author = (Author) o;
        return age == author.age &&
                Objects.equals(name, author.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}
import java.util.Objects;

public class Book {
    private Author author;
    private double price;

    public Book(Author author, double price) {
        this.author = author != null ? new Author(author.getName(), author.getAge()) : new Author("", 0);
        this.price = price;
    }

    public Author getAuthor() {
        return new Author(author.getName(), author.getAge());
    }

    public void setAuthor(Author author) {
        this.author = author != null ? new Author(author.getName(), author.getAge()) : new Author("", 0);
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Book{" +
                "author=" + author +
                ", price=" + price +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Book book = (Book) o;
        return Double.compare(book.price, price) == 0 &&
                Objects.equals(author, book.author);
    }

    @Override
    public int hashCode() {
        return Objects.hash(author, price);
    }
}
public class TestBook {

    public static void main(String[] args) {
        Author author = new Author("George Orwell", 46);
        Book book = new Book(author, 19.99);

        System.out.println(book);
        Author author1 = book.getAuthor();
        author1.setName("Adam Mickiewicz");
        System.out.println(book);

        Book sameBook = new Book(new Author("George Orwell", 46), 19.99);
        System.out.println("Are books equal? " + book.equals(sameBook));
        System.out.println("Hashcode of book: " + book.hashCode());
        System.out.println("Hashcode of sameBook: " + sameBook.hashCode());
    }
}