Wykład 8
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ść:
""
) - wtedy ustaw adres WMII czyli "ul. Słoneczna 54, 10-710 Olsztyn
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:
""
) - w przeciwnym wypadku ustaw "university of technology"
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.
protected
w Javie?Dostępność w Tym Samym Pakiecie: Pola, metody lub konstruktory oznaczone jako protected
są dostępne dla innych klas w tym samym pakiecie.
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.
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, 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:
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:
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
}
}
Polimorfizm, zarówno statyczny, jak i dynamiczny, jest fundamentalnym elementem programowania obiektowego, umożliwiającym większą elastyczność i możliwość ponownego wykorzyst
final
, aby zapobiec jej rozszerzeniu (dziedziczeniu), co mogłoby pozwolić na zmianę jej zachowania.final
, co oznacza, że ich wartości mogą być przypisane tylko raz.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ł
}
}
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.
Niezmienność: Wszystkie pola rekordu są niezmienne (final), co oznacza, że ich wartości nie mogą być zmienione po utworzeniu obiektu rekordu.
Automatyczne Metody: Java automatycznie generuje metody equals()
, hashCode()
i toString()
, które uwzględniają wszystkie pola rekordu.
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.
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()
.
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());
}
}
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.
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);
}
}
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);
}
}
Rady przy projektowaniu:
Vehicle
(pojazd), klasy takie jak Car
(samochód) i Bike
(rower) mogą być jej rozszerzeniami, ponieważ samochód i rower są rodzajami pojazdów.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.
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.
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.
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()");
}
}
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()"); }
}
Już było :)
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 to specjalny rodzaj asocjacji, w której obie klasy mogą istnieć niezależnie, ale jedna z nich (całość) zawiera drugą (część).
Kompozycja to bardziej rygorystyczna forma agregacji, gdzie część nie może istnieć bez całości.
W Javie klasy mogą implementować interfejsy. Oznacza to, że klasa zgadza się dostarczyć implementacje dla wszystkich metod zadeklarowanych w interfejsie.
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());
}
}