Programowanie obiektowe

Wykład 11

Interfejsy - cd.

Związek między 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.

  1. Zgodność equals i compareTo:
    • Jeśli 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.
    • Jeśli 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.
  2. Implementacja equals:
    • Musi sprawdzać, czy obiekt, z którym jest porównywany, jest tej samej klasy.
    • Następnie porównuje istotne pola, aby stwierdzić, czy obiekty są równoważne.
  3. Implementacja compareTo:
    • Musi porównywać te same pola, które są brane pod uwagę w metodzie equals.
    • Powinna zwracać 0, gdy obiekty są równe, wartość ujemną, gdy obiekt, na którym jest wywoływana, jest mniejszy, i wartość dodatnią, gdy jest większy.

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));
    }
}

Interfejs Comparable<T> a dziedziczenie

Jeś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>.

Pierwszy sposób - rekomendowany

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);
    }
}

Drugi sposób

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);
    }
}

Jak sortować tablice?

int[] array = {3, 1, 4, 1, 5, 9};
Arrays.sort(array);
Integer[] array = {3, 1, 4, 1, 5, 9};
Arrays.sort(array, (a, b) -> b - a); // Sortowanie malejące
int[] largeArray = { /* duża tablica danych */ };
Arrays.parallelSort(largeArray);
  • ręczne sortowanie

Wyrażenia lambda

Wyrażenia lambda w Javie stanowią krótką i wydajną formę zapisu dla anonimowych funkcji, szczególnie przydatną w kontekstach takich jak sortowanie tablic.

  1. Podstawowa Forma:
(parametry) -> { ciało wyrażenia }
  • Parametry: Mogą to być zadeklarowane typy lub mogą być pominięte, jeśli kompilator może wywnioskować typy.
  • Strzałka: Symbol -> oddziela listę parametrów od ciała wyrażenia lambda.
  • Ciało Wyrażenia: Może zawierać jedną lub wiele instrukcji. Jeśli ciało zawiera tylko jedną instrukcję, nawiasy klamrowe są opcjonalne.
  1. Przykład bez Nawiasów dla Jednej Instrukcji:
(a, b) -> a - b
  • Jest to wyrażenie lambda, które przyjmuje dwa parametry (a i b) i zwraca ich różnicę.

Użycie z Arrays.sort()

Projekt W11, pakiet: example5

Integer[] array = {3, 1, 4, 1, 5, 9};
Arrays.sort(array, (a, b) -> a - b);
String[] words = {"apple", "orange", "banana"};
Arrays.sort(words, (s1, s2) -> s1.length() - s2.length());
Arrays.sort(array, (a, b) -> b - a);

Zalety:

  • Zwięzłość: Pozwalają na zapisanie komparatora w jednej linii, bez konieczności tworzenia oddzielnej klasy lub anonimowej klasy wewnętrznej.
  • Czytelność: Ułatwiają zrozumienie kodu, ponieważ logika porównywania jest bezpośrednio tam, gdzie jest używana.
  • Elastyczność: Pozwalają na szybką zmianę kryterium sortowania bez zmiany reszty kodu.

Jak sortować listy tablicowe?

Projekt W11, pakiet: example6

  1. Użycie Collections.sort():
  • Metoda 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.
  • Przykład:
List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 4, 1, 5, 9));
Collections.sort(list);
  • Można również użyć własnego komparatora, aby zdefiniować niestandardowy sposób sortowania.
  1. Sortowanie z Wyrażeniami Lambda (Java 8+):
  • Umożliwia sortowanie listy za pomocą niestandardowego kryterium, definiowanego przez wyrażenie lambda.
  • Przykład sortowania ciągów znaków według długości:
List<String> words = Arrays.asList("apple", "orange", "banana");
words.sort((s1, s2) -> s1.length() - s2.length());
  1. Sortowanie za pomocą Metody Referencyjnej (Java 8+):
  • Metody referencyjne pozwalają na jeszcze bardziej zwięzły zapis, szczególnie gdy sortujemy obiekty według ich naturalnego porządku.
  • Przykład:
List<String> words2 = Arrays.asList("apple", "orange", "banana");
word.sort(null);
words2.sort(Comparator.naturalOrder());
words.sort(String::compareTo);
  1. Sortowanie za pomocą Stream.sorted() (Java 8+):
  • Strumienie (Streams) oferują alternatywny sposób sortowania list. Metoda sorted() zwraca nowy strumień z posortowanymi elementami.
  • Przykład:
List<Integer> list = Arrays.asList(3, 1, 4, 1, 5, 9);
List<Integer> sortedList = list.stream().sorted().collect(Collectors.toList());
  1. Ręczne Sortowanie:
  • Można również zaimplementować własne algorytmy sortowania, takie jak sortowanie przez wstawianie, sortowanie szybkie, itp.
  • Jest to mniej wydajne niż wykorzystanie wbudowanych metod, ale może być pomocne do celów edukacyjnych lub dla specyficznych potrzeb.
  1. Sortowanie w Odwrotnej Kolejności:
  • Używając Collections.sort() lub List.sort() z komparatorem odwrotnym, można sortować listę w odwrotnej kolejności.
  • Przykład:
List<Integer> list = Arrays.asList(3, 1, 4, 1, 5, 9);
Collections.sort(list, Collections.reverseOrder());

Zadanie na egzamin

  1. 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.

  2. 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

Interfejs 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:

  1. 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.

  2. 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”.

  3. 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.

  4. 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.

Przykład

Projekt W11, pakiet: example8

Inne przykłady

Projekt W11, pakiet: example9

Przykład złożony

Projekt W11, pakiet: example10

Częste błędy

  • mylenie/zapominanie kontekstu obu komparatorów
  • błędne dziedziczenia
  • logika na poziomie pierwszego roku

Interfejs 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:

  1. 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.

  2. 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).

  3. 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.

  4. 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
    }
}

Przykład

Projekt W11, pakiet: example11

Projekt W11, pakiet: example12

Projekt W11, pakiet: example13

Projekt W11, pakiet: example14

Projekt W11, pakiet: example15

Rózniece między klasami abstrakcyjnymi a interfejsami

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

Zasady SOLID

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.

  1. Single Responsibility Principle (Zasada Jednej Odpowiedzialności):
    • Każda klasa powinna mieć tylko jeden powód do zmiany. Innymi słowy, klasa powinna być odpowiedzialna tylko za jedną rzecz lub funkcję. Dzięki temu kod jest łatwiejszy do zrozumienia i utrzymania.
  2. Open/Closed Principle (Zasada Otwarte/Zamknięte):
    • Oprogramowanie powinno być otwarte na rozszerzenia, ale zamknięte na modyfikacje. Oznacza to, że powinno być możliwe dodawanie nowych funkcjonalności bez zmieniania istniejącego kodu. Zazwyczaj osiąga się to poprzez stosowanie abstrakcji.
  3. Liskov Substitution Principle (Zasada Podstawienia Liskov):
    • Obiekty w programie powinny być zastępowalne ich podtypami bez wpływu na poprawność działania programu. Na przykład, jeśli klasa A jest podtypem klasy B, to obiekty klasy B można zastąpić obiektami klasy A bez wpływu na działanie programu.
  4. Interface Segregation Principle (Zasada Segregacji Interfejsów):
    • Klienci nie powinni być zmuszani do polegania na interfejsach, których nie używają. Zamiast tworzyć jeden “duży” interfejs, lepiej jest stworzyć kilka mniejszych, bardziej specyficznych interfejsów.
  5. Dependency Inversion Principle (Zasada Odwrócenia Zależności):
    • Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Obie te warstwy powinny zależeć od abstrakcji. Abstrakcje nie powinny zależeć od szczegółów, ale szczegóły powinny zależeć od abstrakcji. Oznacza to, że zamiast klas wysokiego poziomu bezpośrednio zależnych od klas niskiego poziomu, powinny one być połączone przez abstrakcyjne interfejsy.

Wyjątki i obsługa błędów - jeszcze raz

Rodzaje błędów

  1. Błędy Kompilacji (Compile-Time Errors) To błędy wykrywane w trakcie kompilacji programu, zanim zostanie on uruchomiony. Obejmują:
  • Błędy składniowe: Związane z naruszeniem reguł składni języka Java, np. brak średnika na końcu instrukcji, niewłaściwe użycie nawiasów, błędy w deklaracji zmiennych itd.
  • Błędy typowania: Pojawiają się, gdy kod narusza reguły dotyczące typów danych, np. próba przypisania wartości typu String do zmiennej typu int.
  • Błędy związane z zakresem zmiennych: Występują, gdy próbuje się uzyskać dostęp do zmiennej poza jej zakresem (np. poza blokiem, w którym została zadeklarowana).
  1. Błędy Wykonania (Runtime Errors) To błędy, które pojawiają się podczas działania programu i zazwyczaj prowadzą do awarii programu. Obejmują:
  • Wyjątki: Są to błędy, które można obsłużyć w programie za pomocą bloków 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).
  • Błędy systemowe: Są to poważniejsze problemy, które są trudne lub niemożliwe do obsłużenia przez program, np. OutOfMemoryError (brak dostępnej pamięci).
  1. Błędy Logiczne (Logical Errors) To błędy, które powodują, że program działa, ale nie daje oczekiwanych wyników. Są to najtrudniejsze do wykrycia i naprawienia błędy, ponieważ program “działa”, ale nie robi tego, co powinien. Przykłady obejmują:
  • Błędy w algorytmie: Nieprawidłowa logika lub procedura stosowana do rozwiązania problemu.
  • Nieprawidłowe przypisanie lub porównanie: Na przykład używanie operatora = (przypisanie) zamiast == (porównanie) w instrukcji warunkowej.
  1. 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.

  2. 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)

try {
    // kod, który może zgłosić wyjątek
} catch (SpecificExceptionType e) {
    // obsługa specyficznego wyjątku
} catch (AnotherExceptionType e) {
    // obsługa innego wyjątku
} finally {
    // kod, który zostanie wykonany niezależnie od wyjątku
}

Interfejsy a wyjątki

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

  1. Podstawowe informacje:
    • Zdefiniowany w pakiecie java.lang.
    • Zawiera jedną metodę: void close() throws Exception.
    • Jest bardziej ogólny niż Closeable.
  2. Użycie:
    • Obiekty, które implementują AutoCloseable, mogą być używane w bloku try-with-resources.
    • Metoda close() zostanie automatycznie wywołana na końcu bloku try, zapewniając, że zasoby są zwalniane nawet w przypadku wystąpienia wyjątków.
  3. Zakres:
    • 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

  1. Podstawowe informacje:
    • Zdefiniowany w pakiecie java.io.
    • Rozszerza AutoCloseable.
    • Zawiera jedną metodę: void close() throws IOException.
  2. Użycie:
    • Przeznaczony głównie do zamykania zasobów wejścia/wyjścia (I/O), takich jak strumienie, czytelniki i zapisywacze.
    • Jego użycie w try-with-resources zapewnia, że zasoby I/O są prawidłowo zamykane, co pomaga w zapobieganiu wyciekom zasobów i błędom związanym z plikami.
  3. Zakres:
    • 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.

Samodzielne tworzenie wyjątków

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:

  1. Zdecyduj, Czy Twój Wyjątek Będzie Sprawdzany (Checked) czy Niesprawdzany (Unchecked)
  • Wyjątki sprawdzane (Checked Exceptions): Muszą być obsłużone w bloku try-catch lub zadeklarowane w sygnaturze metody za pomocą throws. Są one dziedziczone z klasy Exception.
  • Wyjątki niesprawdzane (Unchecked Exceptions): Nie muszą być jawnie obsługiwane i zazwyczaj wynikają z błędów w logice programu. Są one dziedziczone z klasy RuntimeException.
  1. Utwórz Nową Klasę Dziedziczącą z Exception lub RuntimeException
  • Przykład klasy wyjątku sprawdzanego:

    public class MyCheckedException extends Exception {
        public MyCheckedException() {
            super();
        }
    
        public MyCheckedException(String message) {
            super(message);
        }
    
        // Można dodać więcej konstruktorów, w tym te, które przyjmują inne wyjątki (cause)
    }
  • Przykład klasy wyjątku niesprawdzanego:

    public class MyUncheckedException extends RuntimeException {
        public MyUncheckedException() {
            super();
        }
    
        public MyUncheckedException(String message) {
            super(message);
        }
    
        // Podobnie jak wyżej, można dodać więcej konstruktorów
    }
  1. Dostosuj Klasę Wyjątku
  • Możesz dodać dodatkowe pola, metody czy konstruktory, które będą dostarczały więcej informacji o błędzie, np. kod błędu lub kontekst, w jakim wystąpił wyjątek.
  1. Przykłady rzucania wyjątku:
throw new MyCheckedException("Coś poszło nie tak");
  1. Przykład - Obsługa wyjątku:
try {
    // Kod, który może rzucić MyCheckedException
} catch (MyCheckedException e) {
    // Obsługa wyjątku
}

Dobre Praktyki

  • Nazewnictwo: Nazwy klas wyjątków powinny kończyć się na “Exception” i być opisowe, np. InvalidUserInputException.
  • Wiadomości błędów: Podawaj jasne i zrozumiałe wiadomości błędów, które pomogą zidentyfikować i rozwiązać problem.
  • Dokumentacja: Dokumentuj swoje wyjątki, wyjaśniając, kiedy i dlaczego powinny być rzucone.

Przykład praktyczny

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

Definicja

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:

  1. 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.

  2. 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.

  3. 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).

  4. 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.

  5. 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:

  • Kolekcje: W językach takich jak Java, generyki są powszechnie stosowane w bibliotekach kolekcji (np. 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.
  • Algorytmy: Generyki pozwalają na pisanie algorytmów, które mogą pracować na różnych typach danych.

Misz masz pojęciowy

  1. Klasa Generyczna (Generic Class) Klasa generyczna w programowaniu obiektowym to taka, która pozwala na zdefiniowanie klasy z jednym lub więcej nieokreślonymi typami. Te typy są określone dopiero podczas tworzenia instancji klasy. Klasy generyczne są używane do tworzenia kodu, który jest niezależny od konkretnych typów, a więc może być używany w sposób bardziej elastyczny i bezpieczny pod względem typów.
  • Przykład w Javie:

    public class Box<T> {
        private T t; // T to typ generyczny
    
        public void set(T t) { this.t = t; }
        public T get() { return t; }
    }
  1. Typ Parametryzowany (Parameterized Type) Typ parametryzowany to konkretyzacja klasy generycznej z określonymi typami. Kiedy tworzysz obiekt klasy generycznej, musisz określić konkretne typy dla jej parametrów generycznych.
  • Przykład:
    • Mając klasę generyczną Box<T>, możesz utworzyć jej instancję jako Box<Integer> lub Box<String>. Tutaj Box<Integer> i Box<String> są typami parametryzowanymi.
  1. 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>).

  2. 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++:

    template <typename T>
    class Box {
        T t;
    public:
        void set(T t) { this->t = t; }
        T get() { return t; }
    };

Przykład klasyczny z ksiązki Horstmana

Cay S. Horstmann, Java. Podstawy. Wydanie XII , Wyd. Helion, 2021.

Projekt W11, pakiet: example17