Wykład 11
equals
i compareTo
W Javie, gdy implementujesz metody equals
i compareTo
w konkretnej klasie, istnieje ważna zasada, która powinna być zachowana, znana jako zgodność equals i compareTo. Zgodność ta oznacza, że wynik metody equals
powinien być zgodny z wynikiem metody compareTo
.
compareTo
zwraca 0 (co wskazuje, że obiekty są równe pod względem porządku), to equals
powinno również zwrócić true
, sugerując, że obiekty są równoważne.equals
zwraca true
(wskazując, że obiekty są równoważne), to compareTo
powinno zwrócić 0, wskazując, że są one równe pod względem porządku.equals
.Projekt W11, pakiet: example1
public class Person implements Comparable<Person> {
private String name;
private int age;
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int compareTo(Person other) {
int nameComparison = name.compareTo(other.name);
if (nameComparison != 0) {
return nameComparison;
}
return Integer.compare(age, other.age);
}
}
Uwaga! null
nie należy do żadnej klasy. obj.compareTo(null)
wyrzuca wyjątek, ale obj.equals(null)
zwraca false
.
Większość implementujących interfejs Comparable klas API Javy honoruje tę zasadę, ale jednym z ważnych wyjątków jest klasa BigDecimal
.
Projekt W11, pakiet: example2
import java.math.BigDecimal;
import java.util.Arrays;
public class TestBigDecimal {
public static void main(String[] args) {
BigDecimal[] numbers = new BigDecimal[3];
numbers[0] = new BigDecimal("1.0");
numbers[1] = new BigDecimal("1.00");
numbers[2] = new BigDecimal("1.000");
System.out.println(Arrays.toString(numbers));
Arrays.sort(numbers);
System.out.println(Arrays.toString(numbers));
}
}
Comparable<T>
a dziedziczenieJeśli klasa bazowa X
implementuje interfejs Comparable<X>
, to klasa pochodna Y
dziedzicząca po X
nie może implementować interfejsu Comparable<Y>
.
Można dodać w klasie pochodnej Y
implementację interfejsu Comparable<X>
.
Wyrzucanie ClassCastException
, jeśli typy są różne
Projekt W11, pakiet: example3
public class Employee implements Comparable<Employee> {
private String name;
private int salary;
// Konstruktory, gettery, settery itp.
@Override
public int compareTo(Employee other) {
// Porównanie na podstawie nazwiska
int nameComparison = this.name.compareTo(other.name);
if (nameComparison != 0) {
return nameComparison;
}
// Porównanie na podstawie wynagrodzenia
return Integer.compare(this.salary, other.salary);
}
}
public class Manager extends Employee {
private int bonus;
// Konstruktory, gettery, settery itp.
@Override
public int compareTo(Employee other) {
if (other.getClass() != Manager.class) {
throw new ClassCastException("Nie można porównać Managera z innym typem Employee");
}
Manager otherManager = (Manager) other;
int baseComparison = super.compareTo(otherManager);
if (baseComparison != 0) {
return baseComparison;
}
// Porównanie na podstawie bonusu
return Integer.compare(this.bonus, otherManager.bonus);
}
}
Obiekty klasy pochodnej są uzupełniane o dodatkowe pole.
instanceof
używane jest do zachowania hierarchii w porządku.
Projekt W11, pakiet: example4
public class Employee implements Comparable<Employee> {
private String name;
private int salary;
// Konstruktory, gettery, settery itp.
@Override
public int compareTo(Employee other) {
// Porównanie na podstawie nazwiska
int nameComparison = this.name.compareTo(other.name);
if (nameComparison != 0) {
return nameComparison;
}
// Porównanie na podstawie wynagrodzenia
return Integer.compare(this.salary, other.salary);
}
}
public class Manager extends Employee {
private int bonus;
// Konstruktory, gettery, settery itp.
@Override
public int compareTo(Employee other) {
if (other instanceof Manager) {
Manager otherManager = (Manager) other;
int baseComparison = super.compareTo(otherManager);
if (baseComparison != 0) {
return baseComparison;
}
// Porównanie na podstawie bonusu
return Integer.compare(this.bonus, otherManager.bonus);
}
return super.compareTo(other);
}
}
Wyrażenia lambda w Javie stanowią krótką i wydajną formę zapisu dla anonimowych funkcji, szczególnie przydatną w kontekstach takich jak sortowanie tablic.
->
oddziela listę parametrów od ciała wyrażenia lambda.Arrays.sort()
Projekt W11, pakiet: example5
String[] words = {"apple", "orange", "banana"};
Arrays.sort(words, (s1, s2) -> s1.length() - s2.length());
Zalety:
Projekt W11, pakiet: example6
Collections.sort()
:Collections.sort()
służy do sortowania list i jest jednym z najprostszych sposobów sortowania ArrayList
. Przyjmuje listę jako argument i sortuje ją w miejscu.List<String> words = Arrays.asList("apple", "orange", "banana");
words.sort((s1, s2) -> s1.length() - s2.length());
List<String> words2 = Arrays.asList("apple", "orange", "banana");
word.sort(null);
words2.sort(Comparator.naturalOrder());
words.sort(String::compareTo);
Stream.sorted()
(Java 8+):sorted()
zwraca nowy strumień z posortowanymi elementami.List<Integer> list = Arrays.asList(3, 1, 4, 1, 5, 9);
List<Integer> sortedList = list.stream().sorted().collect(Collectors.toList());
Collections.sort()
lub List.sort()
z komparatorem odwrotnym, można sortować listę w odwrotnej kolejności.Zdefiniuj klasę SportsCompetition
, która będzie implementować generyczny interfejs Comparable
. W klasie tej zadeklaruj prywatne pola name
typu String
oraz year
typu int
. Implementując metodę compareTo
interfejsu Comparable
, porównuj zawody sportowe na podstawie roku ich rozegrania, a w przypadku takiego samego roku - na podstawie nazwy. Następnie zdefiniuj klasę Olympics
dziedziczącą po klasie SportsCompetition
. Klasa Olympics
ma dodatkowo posiadać prywatne pole hostCity
typu String
. Implementując metodę compareTo
interfejsu Comparable
w klasie Olympics
, skorzystaj z metody compareTo
zdefiniowanej w klasie SportsCompetition
oraz, w razie potrzeby, uwzględnij pole hostCity
.
Napisz program TestCompetitions
, w którym utwórz listę tablicową różnych zawodów sportowych i igrzysk olimpijskich o nazwie competitionList
posługując się klasą ArrayList
. W składzie listy powinny wystąpić przynajmniej po 3 obiekty różnych typów, reprezentujące różne zawody sportowe oraz różne edycje igrzysk olimpijskich.
Projekt W11, pakiet: example7
Comparator
Interfejs Comparator
w Javie jest funkcjonalnym interfejsem używanym do definiowania porządku w kolekcjach obiektów. Służy do tworzenia zasad sortowania, które mogą być następnie stosowane do sortowania tablic, list i innych kolekcji danych. Jest szczególnie przydatny, gdy chcesz sortować obiekty według własnych, niestandardowych kryteriów.
Podstawowe cechy interfejsu Comparator:
Funkcjonalny interfejs: Jako funkcjonalny interfejs, Comparator
zawiera tylko jedną metodę abstrakcyjną, compare(T o1, T o2)
, co sprawia, że jest idealny do użycia z wyrażeniami lambda i metodami referencyjnymi w Javie 8 i nowszych.
Metoda compare
: Jest to główna metoda w interfejsie Comparator
. Przyjmuje dwa obiekty tego samego typu i zwraca wartość całkowitą. Jeśli zwrócona wartość jest mniejsza od zera, oznacza to, że pierwszy obiekt (o1) jest “mniejszy” niż drugi (o2). Wartość równa zero oznacza, że obiekty są równe, a wartość większa od zera wskazuje, że pierwszy obiekt jest “większy”.
Metody domyślne i statyczne: Od Javy 8, interfejs Comparator
zawiera również różne metody domyślne i statyczne, takie jak reversed()
, thenComparing()
, nullsFirst()
, nullsLast()
, które umożliwiają bardziej zaawansowane i wyrafinowane sposoby tworzenia komparatorów.
Zastosowanie: Comparator
jest szeroko stosowany w metodach takich jak Collections.sort()
i Arrays.sort()
, a także w strukturach danych opartych na sortowaniu, takich jak TreeSet
i TreeMap
.
Projekt W11, pakiet: example8
Projekt W11, pakiet: example9
Projekt W11, pakiet: example10
Cloneable
Interfejs Cloneable
w Javie jest znacznikiem (marker interface), który wskazuje, że klasa zezwala na tworzenie kopii swoich instancji poprzez wywołanie metody clone()
z klasy Object
. W praktyce, implementacja tego interfejsu informuje metodę clone()
z klasy Object
, że jest dozwolone tworzenie polowych kopii (shallow copy) obiektu.
Charakterystyka interfejsu Cloneable
:
Brak Metod: Jako interfejs znacznikowy, Cloneable
nie zawiera żadnych metod do implementacji. Jego jedynym celem jest wskazanie, że klasa implementująca ten interfejs może być bezpiecznie klonowana.
Metoda clone()
z Klasy Object
: Metoda clone()
jest zdefiniowana w klasie Object
i domyślnie wykonuje płytkie kopiowanie obiektu. Oznacza to, że kopiuje wartości pól obiektu, ale nie kopiuje obiektów, na które te pola mogą wskazywać (np. referencje do innych obiektów).
Płytkie Kopiowanie vs Głębokie Kopiowanie: Płytkie kopiowanie (shallow copy) oznacza, że skopiowane są tylko wartości pól, ale jeśli pole jest referencją do innego obiektu, to obie kopie (oryginał i klon) będą wskazywać na ten sam obiekt. Głębokie kopiowanie (deep copy) wymaga dodatkowej logiki, aby skopiować także obiekty, na które wskazują pola.
Nadpisywanie clone()
: Aby umożliwić klonowanie obiektów klasy, która implementuje Cloneable
, zazwyczaj trzeba nadpisać metodę clone()
i zadeklarować ją jako public
. Wewnątrz tej metody, zwykle wywołuje się super.clone()
i dodaje logikę dla głębokiego kopiowania, jeśli jest to potrzebne.
class Example implements Cloneable {
int number;
SomeObject reference;
public Example(int number, SomeObject reference) {
this.number = number;
this.reference = reference;
}
@Override
public Object clone() throws CloneNotSupportedException {
// Płytkie kopiowanie
return super.clone();
// Dla głębokiego kopiowania, dodaj logikę kopiowania obiektów, na które wskazują pola
}
}
Projekt W11, pakiet: example11
Projekt W11, pakiet: example12
Projekt W11, pakiet: example13
Projekt W11, pakiet: example14
Projekt W11, pakiet: example15
W praktyce, wybór między klasą abstrakcyjną a interfejsem zależy od konkretnego przypadku użycia. Klasy abstrakcyjne są bardziej odpowiednie do modelowania hierarchii dziedziczenia dla obiektów, podczas gdy interfejsy są lepsze do definiowania wspólnych funkcjonalności, które mogą być współdzielone przez różne klasy, niekoniecznie powiązane hierarchią dziedziczenia.
Zasady SOLID to zbiór pięciu zasad projektowania oprogramowania w programowaniu obiektowym, które mają na celu zwiększenie czytelności, elastyczności i podatności na rozbudowę kodu.
String
do zmiennej typu int
.try-catch
. Przykłady to NullPointerException
(odwołanie do obiektu, którego wartość jest null
), ArrayIndexOutOfBoundsException
(odwołanie do indeksu tablicy poza jej zakresem), ArithmeticException
(np. dzielenie przez zero).OutOfMemoryError
(brak dostępnej pamięci).=
(przypisanie) zamiast ==
(porównanie) w instrukcji warunkowej.Błędy Semantyczne Są to błędy, które powodują niezgodność kodu z logiką biznesową lub zadanymi wymaganiami, mimo że kod kompiluje się i wykonuje poprawnie.
Błędy Środowiska Obejmują one problemy z konfiguracją środowiska uruchomieniowego Java (np. niewłaściwe ścieżki, brak wymaganych bibliotek) oraz problemy związane z platformą (np. różnice w zachowaniu programu na różnych systemach operacyjnych)
W Javie interfejsy AutoCloseable
i Closeable
odgrywają kluczową rolę w zarządzaniu zasobami oraz obsłudze błędów, zwłaszcza w kontekście mechanizmu try-with-resources. Oba te interfejsy są używane do zapewnienia, że zasoby, takie jak pliki, połączenia z bazą danych lub inne zasoby systemowe, będą prawidłowo zamknięte po ich użyciu.
Interfejs AutoCloseable
java.lang
.void close() throws Exception
.Closeable
.AutoCloseable
, mogą być używane w bloku try-with-resources.close()
zostanie automatycznie wywołana na końcu bloku try, zapewniając, że zasoby są zwalniane nawet w przypadku wystąpienia wyjątków.AutoCloseable
jest przeznaczony do szerszego zastosowania, nie tylko w kontekście operacji wejścia/wyjścia. Może być używany do zwalniania dowolnych zasobów.Interfejs Closeable
java.io
.AutoCloseable
.void close() throws IOException
.Closeable
jest bardziej specyficzny dla operacji I/O i jest używany głównie w kontekście operacji na strumieniach danych.Przykład
try (BufferedReader reader = new BufferedReader(new FileReader("plik.txt"))) {
// Przetwarzanie pliku
} catch (IOException e) {
// Obsługa wyjątku IOException
}
W tym przykładzie BufferedReader
implementuje Closeable
. Po zakończeniu bloku try, metoda close()
BufferedReader
zostanie automatycznie wywołana, zamykając zasób i zarządzając wszelkimi wyjątkami związanymi z operacjami wejścia/wyjścia.
W Javie tworzenie własnych klas wyjątków jest stosunkowo proste i pozwala na lepsze zarządzanie wyjątkami specyficznymi dla danej aplikacji. Oto podstawowe kroki do stworzenia własnej klasy wyjątku:
try-catch
lub zadeklarowane w sygnaturze metody za pomocą throws
. Są one dziedziczone z klasy Exception
.RuntimeException
.Exception
lub RuntimeException
Przykład klasy wyjątku sprawdzanego:
Przykład klasy wyjątku niesprawdzanego:
try {
// Kod, który może rzucić MyCheckedException
} catch (MyCheckedException e) {
// Obsługa wyjątku
}
Dobre Praktyki
InvalidUserInputException
.W Javie, najlepszą praktyką jest sprawdzanie poprawności argumentów metody bezpośrednio wewnątrz samej metody. Dzięki temu zapewniamy, że metoda zawsze działa poprawnie, niezależnie od tego, skąd jest wywoływana. W przypadku metody obliczającej silnię, powinniśmy sprawdzić, czy przekazany argument nie jest liczbą ujemną. Jeśli jest, należy wyrzucić wyjątek IllegalArgumentException, który jest standardowym wyjątkiem systemowym używanym do sygnalizowania nieprawidłowych argumentów wywołania metody.
Projekt W11, pakiet: example16
Programowanie generyczne to koncepcja w programowaniu, która pozwala na pisanie kodu, który może być używany z różnymi typami danych, bez konieczności powtarzania tego samego kodu dla każdego typu danych. Jest to szczególnie przydatne w językach programowania silnie typowanych, takich jak Java, C# czy C++, gdzie typy danych muszą być określone podczas kompilacji. Oto kluczowe aspekty programowania generycznego:
Typy Parametryzowane: Programowanie generyczne umożliwia tworzenie klas, interfejsów i metod, które działają na “typach generycznych”. Te typy generyczne są określone jako parametry, zazwyczaj reprezentowane przez litery, takie jak T, E, K, V itp.
Zwiększona Znacząco Bezpieczeństwo Typów: Dzięki temu, że typy są określone podczas kompilacji, programowanie generyczne pomaga uniknąć błędów związanych z nieprawidłowym rzutowaniem typów, które mogą wystąpić w trakcie działania programu.
Ograniczenia Typów: Możliwe jest narzucenie ograniczeń na typy generyczne, tak aby akceptowały tylko klasy, które spełniają określone wymagania (np. dziedziczenie po konkretnej klasie bazowej lub implementowanie określonego interfejsu).
Kod Współużytkowany: Kod napisany w sposób generyczny może być używany z różnymi typami danych, co zmniejsza redundancję i ułatwia utrzymanie kodu.
Kompilacja Typu Bezpiecznego: Podczas kompilacji, kompilator sprawdza, czy kod generyczny jest używany poprawnie zgodnie z określonymi typami, co zapewnia wyższe bezpieczeństwo typów i pomaga w wykrywaniu błędów na wcześniejszym etapie rozwoju oprogramowania.
Przykładowe Zastosowania:
List<T>
, Map<K,V>
), co pozwala na tworzenie kolekcji, które mogą przechowywać elementy dowolnego typu, jednocześnie zapewniając bezpieczeństwo typów.Przykład w Javie:
Box<T>
, możesz utworzyć jej instancję jako Box<Integer>
lub Box<String>
. Tutaj Box<Integer>
i Box<String>
są typami parametryzowanymi.Typ Generyczny (Generic Type) Typ generyczny to termin ogólnie odnoszący się do klas, interfejsów i metod, które używają typów parametryzowanych. Obejmuje on zarówno definicję klasy generycznej (jak Box<T>
), jak i konkretne typy parametryzowane (jak Box<Integer>
).
Szablon Klas (Class Template) Szablon klas jest pojęciem bardziej związanym z językami programowania takimi jak C++, które stosują “templates” do osiągnięcia podobnych celów, co generyki w Javie. Szablon klasy w C++ jest schematem dla tworzenia klas lub funkcji, które mogą działać z dowolnym typem.
Przykład w C++:
Cay S. Horstmann, Java. Podstawy. Wydanie XII , Wyd. Helion, 2021.
Projekt W11, pakiet: example17