Programowanie obiektowe

Wykład 12

Interfejsy - cd.

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 - prosty

Projekt W11, pakiet: example11

package example11;

public class Person implements Cloneable {

    private String name;
    private int age;

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

    @Override
    public String toString() {
        return "Name: " + name + ", Age: " + age;
    }

    @Override
    public Person clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }
}
package example11;

public class TestPerson {

    public static void main(String[] args) {
        Person p1 = new Person("John", 30);
        Person p2 = null;
        try {
            p2 = p1.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        System.out.println("p1: " + p1);
        System.out.println("p2: " + p2);
        System.out.println("p1 == p2: " + (p1 == p2));
        System.out.println("p1.equals(p2): " + p1.equals(p2));
    }
}

Przykład - agregacja

Projekt W11, pakiet: example12

package example12;

public class Engine implements Cloneable{

    private String type;

    public Engine(String type) {
        this.type = type;
    }


    public String toString() {
        return "Engine: " + type;
    }

    @Override
    public Engine clone() throws CloneNotSupportedException {
        return (Engine) super.clone();
    }
}
package example12;

public class Car implements Cloneable{

    private Engine engine;

    public Car(Engine engine) throws CloneNotSupportedException {
        this.engine = engine!= null ? engine.clone() : new Engine("");
    }

    public Engine getEngine() throws CloneNotSupportedException {
        return engine.clone();
    }

    @Override
    public String toString() {
        return "Car: " + engine;
    }

    @Override
    public Car clone() throws CloneNotSupportedException {
        Car car = (Car) super.clone();
        car.engine = this.engine.clone();
        return car;
    }
}
package example12;

public class TestCar {

    public static void main(String[] args) throws CloneNotSupportedException {
        Car c1 = new Car(new Engine("V8"));
        try {
            Car c2 = c1.clone();
            System.out.println("c1: " + c1);
            System.out.println("c2: " + c2);
            System.out.println("c1 == c2: " + (c1 == c2));
            System.out.println("c1.equals(c2): " + c1.equals(c2));
            System.out.println("c1.getEngine() == c2.getEngine(): " + (c1.getEngine() == c2.getEngine()));
            System.out.println("c1.getEngine().equals(c2.getEngine()): " + c1.getEngine().equals(c2.getEngine()));
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

Przykład - dzedziczenie

Projekt W11, pakiet: example12a

package example12a;

public class Shape implements Cloneable {

    private int x;
    private int y;

    public Shape(int x, int y) {
        this.x = x;
        this.y = y;
    }


    @Override
    public Shape clone() throws CloneNotSupportedException {
        //System.out.println("Shape.clone()");
        return (Shape) super.clone();
    }
}
package example12a;

public class Circle extends Shape {

    private int radius;


    public Circle(int x, int y, int radius) {
        super(x, y);
        this.radius = radius;
    }


    @Override
    public Circle clone() throws CloneNotSupportedException {
        //System.out.println("Circle.clone()");
        return (Circle) super.clone();
    }
}
package example12a;

public class TestCircle {

    public static void main(String[] args) throws CloneNotSupportedException {
        Circle c1 = new Circle(10, 20, 30);
        Circle c2 = c1.clone();
        System.out.println("c1: " + c1);
        System.out.println("c2: " + c2);
        System.out.println("c1 == c2: " + (c1 == c2));
        System.out.println("c1.equals(c2): " + c1.equals(c2));
        Shape c3 = c1.clone();
        System.out.println("c3: " + c3);
        System.out.println("c1 == c3: " + (c1 == c3));
        System.out.println("c1.equals(c3): " + c1.equals(c3));
        Shape c4 = new Circle(10, 20, 30);
        Circle c5 = (Circle) c4.clone(); // requires cast
        System.out.println("c4: " + c4);
        System.out.println("c5: " + c5);
        System.out.println("c4 == c5: " + (c4 == c5));
    }
}

Przykład - pole tablica “prosta”

Projekt W11, pakiet: example13

package example13;

import java.util.Arrays;

public class Student implements Cloneable{

    private String name;
    private double[] grades;

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

    @Override
    public String toString() {
        return getClass().getSimpleName() + ": name=" + name + ", grades=" + Arrays.toString(grades);
    }

    @Override
    public Student clone() throws CloneNotSupportedException {
        Student student = (Student) super.clone();
        student.grades = grades.clone();
        return student;
    }

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

public class TestStudent {

    public static void main(String[] args) {
        Student student1 = new Student("John", new double[]{10, 9, 8});
        try{
            Student student2 = student1.clone();
            System.out.println(student1);
            System.out.println(student2);
            System.out.println(student1 == student2);
            System.out.println(student1.equals(student2));
        } catch (CloneNotSupportedException e) {
            System.out.println(e.getMessage());
        }
    }
}

Przykład - pole “lista tablicowa prosta”

Projekt W11, pakiet: example14

package example14;

import java.util.ArrayList;

public class SportTeam implements Cloneable{

    private String name;
    private ArrayList<String> players;

    public SportTeam(String name, ArrayList<String> players) {
        this.name = name;
        this.players = players != null ? new ArrayList<>(players) : new ArrayList<>();
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + ": name=" + name + ", players=" + players;
    }

    @Override
    public SportTeam clone() throws CloneNotSupportedException {
        SportTeam sportTeam = (SportTeam) super.clone();
        sportTeam.players = new ArrayList<>(players);
        return sportTeam;
    }

}
package example14;

import java.util.ArrayList;

public class TestSportTeam {

    public static void main(String[] args) {
        ArrayList<String> players = new ArrayList<>();
        players.add("Player1");
        players.add("Player2");
        SportTeam sportTeam1 = new SportTeam("Team1", players);
        SportTeam sportTeam2 = null;
        try {
            sportTeam2 = sportTeam1.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        System.out.println(sportTeam1);
        System.out.println(sportTeam2);
        System.out.println(sportTeam1 == sportTeam2);
        System.out.println(sportTeam1.equals(sportTeam2));
    }
}

Przykład - pole “tablica złożona”

Projekt W11, pakiet: example15

package example15;

public class Person implements Cloneable{

    private String name;
    private int age;

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

    @Override
    public String toString() {
        return "Name: " + this.name + "|Age: " + this.age;
    }

    @Override
    public Person clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }

}
package example15;

import java.util.Arrays;

public class Team implements Cloneable{

    private Person[] team;

    public Team(Person[] team) throws CloneNotSupportedException {
        //this.team = team!=null ? team.clone() : new Person[0]; //wrong
        this.team = new Person[team.length];
        for (int i = 0; i < team.length; i++) {
            this.team[i] = team[i].clone();
        }
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + ": team=" + Arrays.toString(team);
    }

    @Override
    public Team clone() throws CloneNotSupportedException {
        Team t = (Team) super.clone();
        // t.team = team.clone(); // this is wrong
        t.team = new Person[team.length];
        for (int i = 0; i < team.length; i++) {
            t.team[i] = team[i].clone();
        }

        return t;
    }
}
package example15;

public class TestTeam {

    public static void main(String[] args) throws CloneNotSupportedException {
        Person[] team = new Person[3];
        team[0] = new Person("John", 20);
        team[1] = new Person("Mary", 21);
        team[2] = new Person("Peter", 22);
        Team t1 = new Team(team);
        System.out.println(t1);
        try {
            Team t2 = t1.clone();
            System.out.println(t2);
            System.out.println(t1==t2);
            System.out.println(t1.equals(t2));
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }

    }
}

Róznice 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.

Rekordy a interfejsy

  • ogólnie interfejsy dla rekordów implementujemy tak jak dla zwykłych klas
  • powinniśmy uwzględniać “niezmienniczość” np. przy interfejsie Cloneable

Przykład - pole rekord a kopiowanie

Projekt W11, pakiet: example15a

package example15a;

public record Engine(String type, double capacity) {

}
package example15a;

public class Car implements Cloneable{

    private String brand;
    private String model;
    private Engine engine;

    public Car(String brand, String model, Engine engine) {
        this.brand = brand;
        this.model = model;
        this.engine = engine;
    }

    public String toString() {
        return brand + " " + model + " " + engine;
    }

    public Engine getEngine() {
        return engine;
    }

    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    public Car clone() throws CloneNotSupportedException {
        return (Car) super.clone();
    }


}
package example15a;

public class TestCar {

    public static void main(String[] args) throws CloneNotSupportedException {
        Car car1 = new Car("Ford", "Focus", new Engine("Diesel", 1.6));
        Car car2 = car1.clone();
        System.out.println(car1);
        System.out.println(car2);
        System.out.println(car1 == car2);
        System.out.println(car1.equals(car2));
        System.out.println(car1.getEngine() == car2.getEngine());
    }
}

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
}

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

package example16;

public class FactorialCalculator {

    public static long factorial(int number) {
        if (number < 0) {
            throw new IllegalArgumentException("The number must be greater than or equal to zero.");
        }

        long result = 1;
        for (int i = 1; i <= number; i++) {
            result *= i;
        }

        return result;
    }
}
package example16;

import static example16.FactorialCalculator.factorial;

public class TestFactorialCalculator {

    public static void main(String[] args) {
        try {
            System.out.println("Silnia 5: " + factorial(5));
            System.out.println("Silnia -1: " + factorial(-1));
        } catch (IllegalArgumentException e) {
            System.out.println("Wystąpił błąd: " + e.getMessage());
        }
    }
}

Przykład w klasie

Projekt W11, pakiet: example16a

package example16a;

public class Person {

    private String name;
    private int age;

    public Person(String name, int age) {
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("The name cannot be null or empty.");
        }

        if (age < 0) {
            throw new IllegalArgumentException("The age must be greater than or equal to zero.");
        }

        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public void setName(String name) {
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("The name cannot be null or empty.");
        }

        this.name = name;
    }

    public void setAge(int age) {
        if (age < 0) {
            throw new IllegalArgumentException("The age must be greater than or equal to zero.");
        }

        this.age = age;
    }

    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}
package example16a;

public class TestPerson {

    public static void main(String[] args) {
        Person person = new Person("Jan", 30);
        System.out.println(person);
        // ważne: w przypadku błędu, program zostanie przerwany
        // kod powinien mieć kod wyjścia zero na egzaminie (!!!)
        try {
            Person person2 = new Person(null, 30);
            System.out.println(person2);
        } catch (IllegalArgumentException e) {
            System.out.println("Wystąpił błąd: " + e.getMessage());
        }
    }
}

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

package example17;

// Cay S. Horstmann, Java. Podstawy. Wydanie XII , Wyd. Helion, 2021.
public class Pair<T> {

    private T first;
    private T second;

    public Pair() {
        first = null;
        second = null;
    }

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public T getSecond() {
        return second;
    }

    public void setFirst(T newValue) {
        first = newValue;
    }

    public void setSecond(T newValue) {
        second = newValue;
    }
}
package example17;

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

public class ArrayAlg {

    public static Pair<String> minmax(String[] a) {
        if (a == null || a.length == 0) {
            return null;
        }

        String min = a[0];
        String max = a[0];

        for (int i = 1; i < a.length; i++) {
            if (min.compareTo(a[i]) > 0) {
                min = a[i];
            }

            if (max.compareTo(a[i]) < 0) {
                max = a[i];
            }
        }

        return new Pair<>(min, max);
    }
}
package example17;

public class TestPair {

    public static void main(String[] args) {
        Pair<String> p = new Pair<>("Jan", "Kowalski");
        System.out.println(p.getFirst() + " " + p.getSecond());
        p.setFirst("Adam");
        p.setSecond("Nowak");
        System.out.println(p.getFirst() + " " + p.getSecond());
        String[] words = {"Ala", "ma", "kota", "i", "psa"};
        Pair<String> mm = ArrayAlg.minmax(words);
        System.out.println("min = " + mm.getFirst());
        System.out.println("max = " + mm.getSecond());
    }
}

Jakie nazwy?

W programowaniu generycznym w Javie stosuje się pewne konwencje dotyczące oznaczeń typów generycznych, aby ułatwić zrozumienie kodu. Oto najczęściej używane oznaczenia:

  1. E - Element: Jest używany głównie w kolekcjach, jak java.util.List<E>, java.util.Set<E>, gdzie E oznacza typ elementów w kolekcji.

  2. K - Key: Używany w kontekście map i wpisów mapy, gdzie K reprezentuje typ klucza. Na przykład w java.util.Map<K, V>.

  3. V - Value: Również używany w mapach, gdzie V oznacza typ wartości. W java.util.Map<K, V>, K to klucz, a V to wartość.

  4. T - Type: Jest to ogólny typ, który może być używany w dowolnym kontekście. Na przykład, w klasach generycznych jak java.util.ArrayList<T>, gdzie T oznacza typ przechowywanych elementów.

  5. N - Number: Czasami używany do oznaczania liczbowych typów danych, szczególnie w klasach rozszerzających java.lang.Number.

  6. S, U, V itd.: Te litery są używane, gdy są potrzebne dodatkowe typy generyczne, i zwykle są stosowane w kolejności alfabetycznej.

https://docs.oracle.com/javase/tutorial/java/generics/types.html

Metody generyczne

Tworzenie metod generycznych umożliwia pisaniu metod, które mogą operować na różnych typach danych, jednocześnie zapewniając bezpieczeństwo typów w czasie kompilacji. Oto jak możesz tworzyć metody generyczne:

  1. Deklaracja Typu Generycznego: Typ generyczny jest deklarowany przed typem zwracanym metody. Używa się do tego liter jak T, E, K, V, itd., które działają jako zmienne reprezentujące typy.

    Przykład:

    public <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.println(element);
        }
    }

    W tym przykładzie <T> przed void oznacza, że metoda printArray jest generyczna i operuje na typie T.

  2. Używanie Typów Generycznych w Ciele Metody: Możesz używać tych typów generycznych jako typów zmiennych, parametrów i typów zwracanych w metodzie.

  3. Ograniczenia Typów (Type Bounds): Możesz ograniczyć rodzaje typów, które mogą być używane z danym typem generycznym, używając słowa kluczowego extends (dla klas i interfejsów) lub super (dla ograniczeń dolnych).

    Przykład:

    public <T extends Comparable<T>> T findMax(T[] array) {
        T max = array[0];
        for (T element : array) {
            if (element.compareTo(max) > 0) {
                max = element;
            }
        }
        return max;
    }

    W tym przypadku <T extends Comparable<T>> oznacza, że typ T musi implementować interfejs Comparable<T>.

  4. Wywoływanie Metod Generycznych: Podczas wywoływania metody generycznej, kompilator zazwyczaj jest w stanie wywnioskować typ generyczny na podstawie kontekstu, ale można też jawnie podać typ generyczny.

    Przykład:

    Integer[] intArray = {1, 2, 3};
    printArray(intArray); // Kompilator wywnioskuje, że T to Integer
    
    String[] stringArray = {"Hello", "World"};
    printArray(stringArray); // Kompilator wywnioskuje, że T to String

Przykład - metoda generyczna statyczna z dowolną ilością argumentów

Projekt W12, example18

package example18;

// //Cay S. Horstmann, Java. Podstawy. Wydanie XII , Wyd. Helion, 2021.
public class ArrayAlg {

    public static <T> T getMiddle(T... a) {
        return a[a.length / 2];
    }
}
package example18;

public class TestArrayAlg {

    public static void main(String[] args) {
        String[] words = {"ABC", "DEF", "GHI", "JKL", "MNO"};
        String middle = ArrayAlg.getMiddle(words);
        System.out.println(middle);
        Integer[] numbers = {1, -2, 7, 8, 12};
        Integer middle2 = ArrayAlg.getMiddle(numbers);
        System.out.println(middle2);
        System.out.println(ArrayAlg.getMiddle("ABC", "DEF", "GHI"));
        System.out.println(ArrayAlg.getMiddle(3.4, 177.0, 3.14, -5.6, 177.1));
    }
}

Przykład - statyczna metoda generyczna której argumentem jest tablica

Projekt W12, example19

package example19;

public class Test19 {

    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4, 5};
        Double[] doubleArray = {1.1, 2.2, 3.3, 4.4, 5.5};
        Character[] charArray = {'H', 'E', 'L', 'L', 'O'};
        print(intArray);
        print(doubleArray);
        print(charArray);
    }

    public static <T> void print(T[] array) {
        for (T t : array) {
            System.out.println(t);
        }
    }
}

Przykład - inne kombinacje, nie zawsze zalecane

Projekt W12, example20

package example20;

import java.util.Optional;

public class Test20 {

    public static void main(String[] args) {
        System.out.println(foo("ABC"));
        System.out.println(foo(123));
        System.out.println(foo(3.14));
        System.out.println(foo2(123));
        System.out.println(foo2(3.14));
        //System.out.println(foo2("ABC"));
        System.out.println(Optional.ofNullable(foo3()));
        System.out.println(Optional.ofNullable(foo4(0)));
    }

    public static <T> int foo(T arg){
        return arg.hashCode();
    }

    public static <T> int foo2(T arg) {
        if (arg instanceof Number) {
            return (int) Math.pow(((Number) arg).doubleValue(), 2);
        }
        throw new IllegalArgumentException("Arg musi być liczbą");
    }

    public static <T> T foo3(){
        return null;
    }

    public static <T> T foo4(int arg) {
        if (arg == 0) {
            return (T) Integer.valueOf(arg);
        } else if (arg == 1) {
            return (T) "String";
        }
        return (T) new Object();
    }


}

Ograniczenia zmiennych typowych

package example21;

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

public class ArrayAlg {
    
    public static <T> T min(T... a) {
        if (a == null || a.length == 0) {
            return null;
        }
        
        T smallest = a[0];
        for (int i = 1; i < a.length; i++) {
            if (smallest.compareTo(a[i]) > 0) {
                smallest = a[i];
            }
        }
        
        return smallest;
    }
}

Czy czegoś tu nie brak?

Poprawna forma

Projekt W12, example21

package example21;

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

public class ArrayAlg {

    public static <T extends Comparable<T>> T min(T... a) {
        if (a == null || a.length == 0) {
            return null;
        }

        T smallest = a[0];
        for (int i = 1; i < a.length; i++) {
            if (smallest.compareTo(a[i]) > 0) {
                smallest = a[i];
            }
        }

        return smallest;
    }
}
package example21;

public class Person implements Comparable<Person>{

    private String name;
    private int age;

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

    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "[name=" + name + ",age=" + age + "]";
    }


}
package example21;

public class TestPerson {

    public static void main(String[] args) {
        Double[] numbers = {1.0, 12.0, -3.0};
        System.out.println(ArrayAlg.min(numbers));
        Person[] people = {new Person("Jan", 12), new Person("Anna", 10), new Person("Piotr", 15)};
        System.out.println(ArrayAlg.min(people));
    }
}

Interpretacja

<T extends typ_graniczny> jest składnią używaną w programowaniu generycznym do określenia górnej granicy dla typu generycznego T. Oznacza to, że T musi być podtypem (klasą pochodną) klasy określonej jako typ_graniczny lub sama być tym typem.

Wymazywanie typów

Wymazywanie typów (ang. type erasure) to proces stosowany w Javie w kontekście programowania generycznego, który zapewnia kompatybilność wsteczną z wcześniejszymi wersjami Javy, które nie obsługiwały generyków. Kiedy kod zawierający generyki jest kompilowany, kompilator usuwa (wymazuje) wszelkie informacje o typach generycznych, zastępując je ich ograniczeniami lub, jeśli takie nie istnieją, obiektem najbardziej ogólnym (często Object).

Jak Działa Wymazywanie Typów?

  1. Zastępowanie Typów Generycznych:
    • Kompilator zastępuje wszystkie typy generyczne ich ograniczeniami lub Object, jeśli brak jest ograniczeń. Na przykład, dla class Box<T>, T zostanie zastąpione przez Object podczas kompilacji.
  2. Usuwanie Metadanych o Typach:
    • Informacje o typach generycznych są usuwane, więc w czasie wykonania (runtime) nie ma dostępu do tych informacji. Na przykład, nie można sprawdzić czy lista jest typu List<String> czy List<Integer> w czasie wykonania.
  3. Mostowanie Metod:
    • W niektórych przypadkach kompilator może dodać metody mostowe (bridge methods) w celu utrzymania polimorfizmu dla dziedziczonych klas generycznych.

Wymazywanie typów ma kilka konsekwencji: - Brak Możliwości Przeciążania Metod: Metody różniące się jedynie typem generycznym nie mogą być przeciążone, ponieważ po wymazaniu będą miały ten sam sygnaturę.

  • Konieczność Rzutowania: W czasie wykonania trzeba czasami ręcznie rzutować obiekty na odpowiedni typ, co może prowadzić do błędów ClassCastException.

  • Brak Możliwości Sprawdzenia Typu Generycznego w Runtime: Nie można używać refleksji do dokładnego ustalenia typu generycznego w czasie wykonania, ponieważ informacje te są wymazane.

Przykład wymazywania dla klasy generycznej

// Cay S. Horstmann, Java. Podstawy. Wydanie XII , Wyd. Helion, 2021.
public class Pair<T> {

    private T first;
    private T second;

    public Pair() {
        first = null;
        second = null;
    }

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public T getSecond() {
        return second;
    }

    public void setFirst(T newValue) {
        first = newValue;
    }

    public void setSecond(T newValue) {
        second = newValue;
    }
}

jest wymazywany na:

public class Pair {

    private Object first;
    private Object second;

    public Pair() {
        first = null;
        second = null;
    }

    public Pair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }

    public Object getFirst() {
        return first;
    }

    public Object getSecond() {
        return second;
    }

    public void setFirst(Object newValue) {
        first = newValue;
    }

    public void setSecond(Object newValue) {
        second = newValue;
    }
}

Przykład wymazywania dla metody generycznej

public static <T extends Comparable<T>> T min(T... a) {
    if (a == null || a.length == 0) {
        return null;
    }

    T smallest = a[0];
    for (int i = 1; i < a.length; i++) {
        if (smallest.compareTo(a[i]) > 0) {
            smallest = a[i];
        }
    }

    return smallest;
}

jest wymazywany na:

public static Comparable min(Comparable... a) {
    if (a == null || a.length == 0) {
        return null;
    }

    Comparable smallest = a[0];
    for (int i = 1; i < a.length; i++) {
        if (smallest.compareTo(a[i]) > 0) {
            smallest = a[i];
        }
    }

    return smallest;
}

Typy generyczne a typy proste (prymitywne)

Typy proste (ang. primitive types), takie jak int, double, char, itd., nie mogą być używane jako typy generyczne. Wynika to z kilku powodów, głównie związanych z tym, jak generyki są implementowane w Javie oraz jak działają typy proste:

  1. Wymazywanie Typów (Type Erasure):
    • Generyki w Javie są implementowane za pomocą mechanizmu zwanego “wymazywaniem typów” (type erasure), co oznacza, że informacje o typach generycznych są usuwane w czasie kompilacji, a zamiast nich stosowane są ograniczenia lub typ Object.
    • Typy proste nie są obiektami i nie mogą być zastąpione przez typ Object w procesie wymazywania typów.
  2. Pudełkowanie (Boxing) i Rozpakowywanie (Unboxing):
    • Java oferuje mechanizm automatycznego pudełkowania i rozpakowywania (boxing i unboxing) dla typów prostych, co pozwala na konwersję między typami prostymi a ich odpowiednikami w postaci klas opakowujących (wrapper classes), takimi jak Integer dla int, Double dla double itp.
    • Dzięki temu, zamiast używać typów prostych w generykach, można używać ich klas opakowujących. Na przykład, zamiast List<int>, używa się List<Integer>.
  3. Kompatybilność wsteczna:
    • Generyki zostały wprowadzone do Javy w wersji 5, z zachowaniem kompatybilności wstecznej. Aby to osiągnąć, generyki musiały być zaimplementowane w sposób, który nie wymagał zmian w maszynie wirtualnej Javy (JVM). Użycie typów prostych w generykach wymagałoby głębokich zmian w JVM.
  4. Złożoność i Wydajność:
    • Włączenie typów prostych do systemu generyków znacząco zwiększyłoby złożoność języka i kompilatora. Ponadto, operacje na typach prostych wewnątrz generyków mogłyby być mniej wydajne ze względu na konieczność ciągłego pudełkowania i rozpakowywania.

Dziedziczenie a typy generyczne

Projekt W12, example22

package example22;

// Cay S. Horstmann, Java. Podstawy. Wydanie XII , Wyd. Helion, 2021.
public class Pair<T> {

    private T first;
    private T second;

    public Pair() {
        first = null;
        second = null;
    }

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public T getSecond() {
        return second;
    }

    public void setFirst(T newValue) {
        first = newValue;
    }

    public void setSecond(T newValue) {
        second = newValue;
    }
}
package example22;

public class Animal {
}
package example22;

public class Dog extends Animal{
}
package example22;

public class Test22 {

    public static void main(String[] args) {
        //Pair<Animal> obj = new Pair<Dog>(); // to nie jest możliwe
        var obj2 = new Pair<Dog>();
        // reszta kodu nie jest zalecana
        Pair obj3 = obj2;
        obj3.setFirst(new Dog());
        obj3.setSecond(new Animal());
    }
}

Typy wieloznaczne

Termin “typ wieloznaczny” (ang. wildcard type) odnosi się do typów generycznych, które nie są dokładnie określone, czyli używają symbolu zapytania ? jako zastępczego oznaczenia typu. Typy wieloznaczne pozwalają na większą elastyczność w definiowaniu i wykorzystywaniu generycznych struktur danych i metod, ponieważ mogą reprezentować szeroki zakres różnych typów.

Rodzaje Typów Wieloznacznych:

  1. Nieograniczony Typ Wieloznaczny (?):
    • Oznacza dowolny typ. Na przykład, List<?> może być listą dowolnego typu obiektów.
  2. Ograniczony Górnie Typ Wieloznaczny (? extends T):
    • Ogranicza typ do klasy T lub dowolnej jej podklasy. Na przykład, List<? extends Number> może być listą obiektów typu Number lub dowolnego typu, który jest podklasą Number (jak Integer czy Double).
  3. Ograniczony Dolnie Typ Wieloznaczny (? super T):
    • Ogranicza typ do klasy T lub dowolnej jej nadklasy. Na przykład, List<? super Integer> może być listą obiektów typu Integer lub dowolnego typu, który jest nadklasą Integer (jak Number czy Object).

Przykład prosty

Projekt W12, example23

package example23;
// Cay S. Horstmann, Java. Podstawy. Wydanie XII , Wyd. Helion, 2021.
public class Pair<T> {

    private T first;
    private T second;

    public Pair() {
        first = null;
        second = null;
    }

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public T getSecond() {
        return second;
    }

    public void setFirst(T newValue) {
        first = newValue;
    }

    public void setSecond(T newValue) {
        second = newValue;
    }
}
package example23;

public class Animal {

    @Override
    public String toString() {
        return getClass().getSimpleName();
    }
}
package example23;

public class Dog extends Animal{
}
package example23;

public class Test23 {

    public static void main(String[] args) {

        Pair<Animal> animals = new Pair<>(new Animal(), new Animal());
        printAnimals(animals);
        Pair<Dog> dogs = new Pair<>(new Dog(), new Dog());
        //printAnimals(dogs);
        printAnimalsFix(animals);
        printAnimalsFix(dogs);
        printAnimalsFix2(animals);
        printAnimalsFix2(dogs);
        printAnimalsFix3(animals);
        printAnimalsFix3(dogs);

    }

    public static void printAnimals(Pair<Animal> animals) {
        System.out.println(animals.getFirst().toString() + " " + animals.getSecond().toString());
    }

    public static void printAnimalsFix(Pair<? extends Animal> animals) {
        System.out.println(animals.getFirst().toString() + " " + animals.getSecond().toString());
    }

    public static void printAnimalsFix2(Pair<? super Dog> animals) {
        System.out.println(animals.getFirst().toString() + " " + animals.getSecond().toString());
    }

    public static void printAnimalsFix3(Pair<?> animals) {
        System.out.println(animals.getFirst().toString() + " " + animals.getSecond().toString());
    }
}

Przykład zaawansowany

Projekt W12, example24

package example24;

// Cay S. Horstmann, Java. Podstawy. Wydanie XII , Wyd. Helion, 2021.
public class Pair<T> {

    private T first;
    private T second;

    public Pair() {
        first = null;
        second = null;
    }

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public T getSecond() {
        return second;
    }

    public void setFirst(T newValue) {
        first = newValue;
    }

    public void setSecond(T newValue) {
        second = newValue;
    }
}
package example24;

public class Person implements Comparable<Person>{

    private String name;
    private int age;

    public Person(String name, int age) {
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("Name cannot be null or blank");
        }
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
        this.age = age;
    }

    public void setName(String name) {
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("Name cannot be null or blank");
        }
        this.name = name;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + ": name=" + name + ", age=" + age;
    }

    @Override
    public int compareTo(Person o) {
        int base = this.name.compareTo(o.name);
        if (base != 0) {
            return base;
        }
        return Integer.compare(this.age, o.age);
    }
}
package example24;

public class Student extends Person implements Comparable<Person>{

    private int studentId;

    public Student(String name, int age, int studentId) {
        super(name, age);
        if (studentId <10000 || studentId > 999999) {
            throw new IllegalArgumentException("Wrong student ID");
        }
        this.studentId = studentId;
    }

    public int getStudentId() {
        return studentId;
    }

    public void setStudentId(int studentId) {
        if (studentId <10000 || studentId > 999999) {
            throw new IllegalArgumentException("Wrong student ID");
        }
        this.studentId = studentId;
    }

    @Override
    public String toString() {
        return super.toString() + ", studentId=" + studentId;
    }

    @Override
    public int compareTo(Person o) {
        if (o instanceof Student) {
            Student student = (Student) o;
            int base = super.compareTo(student);
            if (base != 0) {
                return base;
            }
            return Integer.compare(this.studentId, student.studentId);
        }
        return super.compareTo(o);
    }
}
package example24;

public class Test24 {

    public static void main(String[] args) {
        Person[] people = new Person[4];
        people[0] = new Person("John", 20);
        people[1] = new Person("John", 30);
        people[2] = new Person("Adam", 20);
        people[3] = new Person("Adam", 16);
        System.out.println("Case 1");
        Pair<Person> pair = minmaxOld(people);
        System.out.println(pair.getFirst());
        System.out.println(pair.getSecond());
        Student[] students = new Student[4];
        students[0] = new Student("John", 20, 125478);
        students[1] = new Student("John", 30, 122278);
        students[2] = new Student("Adam", 20, 125433);
        students[3] = new Student("Adam", 16, 165478);
        //Pair<Student> pair2 = minmaxBad(students); // to nie jest możliwe
        System.out.println("Case 2");
        Pair<Student> pair2 = minmax(students);
        System.out.println(pair2.getFirst());
        System.out.println(pair2.getSecond());
        Person[] people2 = new Person[6];
        people2[0] = new Person("John", 20);
        people2[1] = new Person("John", 30);
        people2[2] = new Person("Adam", 20);
        people2[3] = new Student("John", 20, 125478);
        people2[4] = new Student("John", 30, 122278);
        people2[5] = new Student("Adam", 20, 125433);
        System.out.println("Case 3");
        Pair<Person> pair3 = minmaxOld(people2);
        System.out.println(pair3.getFirst());
        System.out.println(pair3.getSecond());
        System.out.println("Case 4");
        Pair<Person> pair4 = minmax(people2);
        System.out.println(pair4.getFirst());
        System.out.println(pair4.getSecond());
    }

    public static <T extends Comparable<T>> Pair<T> minmaxOld(T[] a) {
        if (a == null || a.length == 0) {
            return null;
        }
        T min = a[0];
        T max = a[0];
        for (int i=1; i<a.length; i++) {
            if (min.compareTo(a[i]) > 0) {
                min = a[i];
            }
            if (max.compareTo(a[i]) < 0) {
                max = a[i];
            }
        }
        return new Pair<>(min, max);
    }

    public static <T extends Comparable<? super T>> Pair<T> minmax(T[] a) {
        if (a == null || a.length == 0) {
            return null;
        }
        T min = a[0];
        T max = a[0];
        for (int i=1; i<a.length; i++) {
            if (min.compareTo(a[i]) > 0) {
                min = a[i];
            }
            if (max.compareTo(a[i]) < 0) {
                max = a[i];
            }
        }
        return new Pair<>(min, max);
    }
}