Programowanie obiektowe

Wykład 13

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

Jak to kodować? Parę zadań…

  1. Stwórz prostą klasę generyczną Box, która może przechowywać obiekt dowolnego typu. Klasa powinna zawierać metodę set, aby ustawić obiekt, oraz metodę get, aby go pobrać.

  2. Napisz generyczną metodę isEqual, która przyjmuje dwa dowolne obiekty tego samego typu i zwraca true, jeśli są one równe, w przeciwnym razie false.

  3. Stwórz klasę generyczną Counter<T>, która będzie zliczać ilość dodanych elementów określonego typu. Klasa powinna mieć metodę add(T element), która dodaje element do wewnętrznej struktury, oraz metodę getCount(), która zwraca liczbę dodanych elementów.

  4. Napisz statyczną metodę generyczną swap, która przyjmuje tablicę dowolnego typu i dwa indeksy, a następnie zamienia miejscami elementy w tej tablicy pod wskazanymi indeksami. Metoda powinna działać dla tablicy każdego typu. Przykładowe wywołanie metody: swap(myArray, 0, 2);, gdzie myArray to tablica typu Integer[] lub dowolnego innego typu. Zabezpiecz metodę tak, aby nie można było jej wywołać z indeksami spoza zakresu tablicy, jak również dla null i pustej tablicy (o zerowej liczbie elementów).

  5. Napisz statyczną metodę generyczną maxValue, która przyjmuje tablicę elementów typu generycznego T, gdzie T rozszerza Comparable<T>. Metoda powinna zwracać największy element z tablicy. Upewnij się, że metoda nie akceptuje pustej tablicy (o zerowej liczbie elementów). Przetestuj metodę na tablicach zawierających różne typy porównywalnych obiektów, jak Integer, Float, czy String. Stwórz klasę Vehicle z polami model i speed, implementującą generyczny Comparable, i przetestuj metodę maxValue na tablicy obiektów Vehicle.

Kolekcje

Definicja kolekcji

W kontekście programowania obiektowego w Javie, kolekcja (ang. ‘collection’) odnosi się do struktury danych, która grupuje obiekty w pojedynczym zbiorniku. Java oferuje szeroki zestaw klas kolekcji w ramach Java Collections Framework, które pozwalają na przechowywanie, przetwarzanie i manipulację zbiorami danych w elastyczny i efektywny sposób.

Uwaga: w internecie można znaleźć misz masz własności, w szczególności pod kątem złożoności obliczeniowej. Warygodnym źródłem jest dokumentacha.

Dobra lektura

Książka w trakcie powstawania:

Maurice Naftalin, Philip Wadler, Java Generics and Collections, O’Reilly Media, Inc. - wydanie drugie planowane na czerwiec 2024, choć można znaleźć fragemty w internecie.

Ilustacji z książki: Cay S. Horstmann, Java. Podstawy. Wydanie XII , Wyd. Helion, 2021.

Do nauki wersje generyczne:

  • interfejs Iterator
  • interfejs Collection
  • ArrayList
  • LinkedList
  • HashSet
  • TreeSet
  • HashMap
  • TreeMap

Interfejs Iterator

Generyczny interfejs Iterator<E> jest fundamentalnym elementem Java Collections Framework, umożliwiającym przeglądanie elementów kolekcji, takich jak listy, zbiory (sets) czy kolejki (queues).

Metody:

  • boolean hasNext(): Ta metoda sprawdza, czy w kolekcji są jeszcze jakieś elementy do przetworzenia. Zwraca true, jeśli kolekcja posiada kolejne elementy.
  • E next(): Zwraca następny element z kolekcji. Gdy nie ma więcej elementów, rzucany jest wyjątek NoSuchElementException.
  • void remove(): Usuwa z kolekcji ostatni element zwrócony przez metodę next(). Metoda ta może rzucić wyjątek UnsupportedOperationException, jeśli operacja usuwania nie jest wspierana przez daną kolekcję.
  • forEachRemaining(Consumer<? super E> action): Służy do wykonania danej akcji dla każdego pozostałego elementu w iteracji, zaczynając od aktualnej pozycji iteratora.

Interfejs Collection

Interfejs Collection<E> jest jednym z podstawowych interfejsów w Java Collections Framework i służy jako korzeń dla innych interfejsów kolekcji, takich jak List, Set, czy Queue. Jest to generyczny interfejs, co oznacza, że można go parametryzować różnymi typami obiektów (reprezentowanymi przez E).

Metody:

Interfejs Collection<E> w Javie zawiera wiele metod, które są fundamentalne dla manipulowania i dostępu do kolekcji danych. Oto niektóre z najpopularniejszych i najczęściej używanych metod tego interfejsu:

  1. add(E e): Dodaje określony element do kolekcji. Zwraca true, jeśli kolekcja zmieniła się w wyniku wywołania tej metody.

  2. addAll(Collection<? extends E> c): Dodaje wszystkie elementy z określonej kolekcji do bieżącej kolekcji. Zwraca true, jeśli bieżąca kolekcja zmieniła się w wyniku wywołania tej metody.

  3. clear(): Usuwa wszystkie elementy z kolekcji. Po wykonaniu tej metody kolekcja jest pusta.

  4. contains(Object o): Sprawdza, czy kolekcja zawiera określony element. Zwraca true, jeśli kolekcja zawiera przynajmniej jedno wystąpienie określonego elementu.

  5. containsAll(Collection<?> c): Sprawdza, czy kolekcja zawiera wszystkie elementy z określonej kolekcji.

  6. isEmpty(): Sprawdza, czy kolekcja jest pusta (nie zawiera żadnych elementów). Zwraca true, jeśli kolekcja nie zawiera żadnych elementów.

  7. iterator(): Zwraca iterator do przeglądania elementów w kolekcji. Iterator pozwala na sekwencyjne przechodzenie przez elementy kolekcji.

  8. remove(Object o): Usuwa jedno wystąpienie określonego elementu z kolekcji, jeśli istnieje. Zwraca true, jeśli kolekcja zmieniła się w wyniku wywołania tej metody.

  9. removeAll(Collection<?> c): Usuwa z tej kolekcji wszystkie jej elementy, które są zawarte w określonej kolekcji.

  10. retainAll(Collection<?> c): Zachowuje tylko te elementy w kolekcji, które są zawarte w określonej kolekcji. Innymi słowy, usuwa z tej kolekcji wszystkie elementy, których nie ma w określonej kolekcji.

  11. size(): Zwraca liczbę elementów w kolekcji.

  12. toArray(): Zwraca tablicę zawierającą wszystkie elementy kolekcji.

Co zyskujemy?

Możliwość tworzenie algorytmów generycznych opartych na kolekcjach.

Projekt W13, example25

LinkedList (lista powiązana)

LinkedList<E> w Javie jest implementacją listy powiązanej, która jest częścią Java Collection Framework. Jest to dynamiczna struktura danych, co oznacza, że może dynamicznie rosnąć i zmniejszać się, dodając lub usuwając elementy.

  1. Implementacja: LinkedList implementuje interfejsy List, Deque, i Queue. Może więc działać jako lista, dwustronna kolejka (deque) lub kolejka (queue).

  2. Węzły: Każdy element (węzeł) w LinkedList przechowuje dane oraz referencje do poprzedniego i następnego elementu, co umożliwia łatwe dodawanie i usuwanie elementów.

  3. Dostęp do elementów: Dostęp do elementów w LinkedList jest sekwencyjny, co oznacza, że czas dostępu do elementów jest proporcjonalny do ich położenia.

  4. Złożoność czasowa: Dodawanie/usuwanie na początku lub końcu listy ma złożoność O(1). Wyszukiwanie, dodawanie lub usuwanie elementów w środku listy ma złożoność O(n), gdzie n to liczba elementów w liście.

  5. Użycie pamięci: Każdy element listy wiązanej wymaga dodatkowej pamięci na przechowywanie referencji do następnego i poprzedniego elementu, co czyni LinkedList bardziej wymagającą pod względem pamięci w porównaniu z ArrayList.

Metody: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/LinkedList.html

  1. void add(int index, E element): Wstawia określony element na określonej pozycji na liście.

  2. boolean add(E e): Dodaje określony element na końcu listy.

  3. boolean addAll(int index, Collection<? extends E> c): Wstawia wszystkie elementy z określonej kolekcji do tej listy, zaczynając od określonej pozycji.

  4. boolean addAll(Collection<? extends E> c): Dodaje wszystkie elementy z określonej kolekcji na końcu tej listy, w kolejności zwracanej przez iterator danej kolekcji.

  5. void addFirst(E e): Wstawia określony element na początku listy.

  6. void addLast(E e): Dodaje określony element na końcu listy.

  7. void clear(): Usuwa wszystkie elementy z tej listy.

  8. Object clone(): Zwraca płytką kopię tej listy LinkedList.

  9. boolean contains(Object o): Zwraca true, jeśli lista zawiera określony element.

  10. Iterator descendingIterator(): Zwraca iterator po elementach tej dwukierunkowej kolejki w odwrotnej kolejności sekwencyjnej.

  11. E element(): Pobiera, ale nie usuwa, głowę (pierwszy element) tej listy.

  12. E get(int index): Zwraca element na określonej pozycji na liście.

  13. E getFirst(): Zwraca pierwszy element na liście.

  14. E getLast(): Zwraca ostatni element na liście.

  15. int indexOf(Object o): Zwraca indeks pierwszego wystąpienia określonego elementu na liście, lub -1, jeśli lista nie zawiera tego elementu.

  16. int lastIndexOf(Object o): Zwraca indeks ostatniego wystąpienia określonego elementu na liście, lub -1, jeśli lista nie zawiera tego elementu.

  17. ListIterator listIterator(int index): Zwraca list-iterator elementów tej listy (w odpowiedniej kolejności), zaczynając od określonej pozycji na liście.

  18. boolean offer(E e): Dodaje określony element jako ogon (ostatni element) tej listy.

  19. boolean offerFirst(E e): Wstawia określony element na początku tej listy.

  20. boolean offerLast(E e): Wstawia określony element na końcu tej listy.

  21. E peek(): Pobiera, ale nie usuwa, głowę (pierwszy element) tej listy.

  22. E peekFirst(): Pobiera, ale nie usuwa, pierwszy element tej listy lub zwraca null, jeśli lista jest pusta.

  23. E peekLast(): Pobiera, ale nie usuwa, ostatni element tej listy lub zwraca null, jeśli lista jest pusta.

  24. E poll(): Pobiera i usuwa głowę (pierwszy element) tej listy.

  25. E pollFirst(): Pobiera i usuwa pierwszy element tej listy, lub zwraca null, jeśli lista jest pusta.

  26. E pollLast(): Pobiera i usuwa ostatni element tej listy, lub zwraca null, jeśli lista jest pusta.

  27. E pop(): Usuwa i zwraca element ze stosu reprezentowanego przez tę listę.

  28. void push(E e): Wstawia element na stos reprezentowany przez tę listę.

  29. E remove(): Pobiera i usuwa głowę (pierwszy element) tej listy.

  30. E remove(int index): Usuwa element na określonej pozycji na liście.

  31. boolean remove(Object o): Usuwa pierwsze wystąpienie określonego elementu z tej listy, jeśli jest obecny.

  32. E removeFirst(): Usuwa i zwraca pierwszy element z tej listy.

  33. boolean removeFirstOccurrence(Object o): Usuwa pierwsze wystąpienie określonego elementu na tej liście (podczas przeglądania listy od głowy do ogona).

  34. E removeLast(): Usuwa i zwraca ostatni element z tej listy.

  35. boolean removeLastOccurrence(Object o): Usuwa ostatnie wystąpienie określonego elementu na tej liście (podczas przeglądania listy od głowy do ogona).

  36. LinkedList reversed(): Zwraca widok tej kolekcji w odwrotnej kolejności.

  37. E set(int index, E element): Zastępuje element na określonej pozycji na liście określonym elementem.

  38. int size(): Zwraca liczbę elementów na tej liście.

  39. Spliterator spliterator(): Tworzy późno wiążący i szybko reagujący na błędy Spliterator po elementach tej listy.

  40. Object[] toArray(): Zwraca tablicę zawierającą wszystkie elementy tej listy w odpowiedniej kolejności (od pierwszego do ostatniego elementu).

  41. T[] toArray(T[] a): Zwraca tablicę zawierającą wszystkie elementy tej listy w odpowiedniej kolejności (od pierwszego do ostatniego elementu); typ czasu wykonania zwróconej tablicy jest taki sam, jak określonej tablicy.

Przykład

Projekt W13, example26

HashSet (zbiór mieszający)

HashSet<E> to implementacja zbioru, która jest częścią Java Collections Framework. Jest to kolekcja, która przechowuje unikalne elementy, nie dopuszczając duplikatów.

  1. Unikalność: HashSet przechowuje tylko unikalne elementy. Próba dodania duplikatu nie spowoduje błędu, ale element nie zostanie dodany.

  2. Brak kolejności: Elementy w HashSet nie są przechowywane w żadnej określonej kolejności. Kolejność elementów może się różnić przy każdym uruchomieniu programu.

  3. Szybkość działania: HashSet zapewnia stały czas potrzebny do operacji dodawania, usuwania i sprawdzania, czy element istnieje, co sprawia, że jest bardzo wydajny.

  4. Null: HashSet pozwala na przechowywanie maksymalnie jednego elementu null.

  5. Implementacja: HashSet jest zaimplementowany na bazie tablicy mieszającej (HashMap), gdzie klucze to elementy zbioru, a wartości są obiektami-pustymi miejscami.

  6. Brak gwarancji kolejności: Kolejność elementów w HashSet może się zmieniać w miarę dodawania lub usuwania elementów.

Metody: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/HashSet.html

  1. boolean add(E e): Dodaje określony element do tego zbioru, jeśli jeszcze się w nim nie znajduje.

  2. void clear(): Usuwa wszystkie elementy z tego zbioru.

  3. Object clone(): Zwraca płytką kopię tej instancji HashSet: same elementy nie są klonowane.

  4. boolean contains(Object o): Zwraca true, jeśli ten zbiór zawiera określony element.

  5. boolean isEmpty(): Zwraca true, jeśli ten zbiór nie zawiera żadnych elementów.

  6. Iterator iterator(): Zwraca iterator po elementach tego zbioru.

  7. static HashSet newHashSet(int numElements): Tworzy nowy, pusty HashSet odpowiedni dla oczekiwanej liczby elementów.

  8. boolean remove(Object o): Usuwa określony element z tego zbioru, jeśli jest obecny.

  9. int size(): Zwraca liczbę elementów w tym zbiorze (jego moc).

  10. Spliterator spliterator(): Tworzy późno wiążący i szybko reagujący na błędy Spliterator po elementach tego zbioru.

  11. Object[] toArray(): Zwraca tablicę zawierającą wszystkie elementy tej kolekcji.

  12. T[] toArray(T[] a): Zwraca tablicę zawierającą wszystkie elementy tej kolekcji; typ czasu wykonania zwróconej tablicy jest taki sam, jak określonej tablicy.

Przykład

Projekt W13, example27

TreeSet (zbiór drzewiasty)

TreeSet<E> to implementacja zbioru oparta na drzewie czerwono-czarnym, będąca częścią Java Collections Framework. Jest to posortowana i automatycznie sortująca się kolekcja unikalnych elementów.

  1. Sortowanie: Elementy w TreeSet są zawsze posortowane według naturalnego porządku lub według Comparatora dostarczonego przy tworzeniu zbioru. To oznacza, że elementy są automatycznie sortowane w momencie ich dodawania.

  2. Unikalność: Podobnie jak HashSet, TreeSet przechowuje tylko unikalne elementy, nie dopuszczając duplikatów.

  3. Wykonanie: Dzięki strukturze drzewa czerwono-czarnego, operacje takie jak wyszukiwanie, dodawanie i usuwanie elementów mają złożoność czasową logarytmiczną O(log n), co jest bardziej efektywne niż liniowa złożoność niektórych innych struktur danych dla dużych zestawów danych.

  4. Null: TreeSet zwykle nie akceptuje wartości null jako elementów. Próba dodania null może skutkować NullPointerException, w zależności od użytego Comparatora.

  5. Iteracja: Iteracja przez elementy TreeSet odbywa się w uporządkowany sposób, co jest korzystne, gdy potrzebna jest uporządkowana prezentacja danych.

Metody: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/TreeSet.html

  1. boolean add(E e): Dodaje określony element do tego zbioru, jeśli jeszcze się w nim nie znajduje.

  2. boolean addAll(Collection<? extends E> c): Dodaje wszystkie elementy z określonej kolekcji do tego zbioru.

  3. void addFirst(E e): Rzuca wyjątek UnsupportedOperationException.

  4. void addLast(E e): Rzuca wyjątek UnsupportedOperationException.

  5. E ceiling(E e): Zwraca najmniejszy element w tym zbiorze większy lub równy podanemu elementowi, lub null, jeśli taki element nie istnieje.

  6. void clear(): Usuwa wszystkie elementy z tego zbioru.

  7. Object clone(): Zwraca płytką kopię tej instancji TreeSet.

  8. Comparator<? super E> comparator(): Zwraca komparator używany do porządkowania elementów w tym zbiorze, lub null, jeśli zbiór używa naturalnego porządkowania swoich elementów.

  9. boolean contains(Object o): Zwraca true, jeśli ten zbiór zawiera określony element.

  10. Iterator descendingIterator(): Zwraca iterator po elementach tego zbioru w porządku malejącym.

  11. NavigableSet descendingSet(): Zwraca widok w odwrotnej kolejności elementów zawartych w tym zbiorze.

  12. E first(): Zwraca pierwszy (najniższy) element aktualnie w tym zbiorze.

  13. E floor(E e): Zwraca największy element w tym zbiorze mniejszy lub równy podanemu elementowi, lub null, jeśli taki element nie istnieje.

  14. SortedSet headSet(E toElement): Zwraca widok części tego zbioru, którego elementy są ściśle mniejsze niż toElement.

  15. NavigableSet headSet(E toElement, boolean inclusive): Zwraca widok części tego zbioru, którego elementy są mniejsze niż (lub równe, jeśli inclusive jest true) toElement.

  16. E higher(E e): Zwraca najmniejszy element w tym zbiorze ściśle większy niż podany element, lub null, jeśli taki element nie istnieje.

  17. boolean isEmpty(): Zwraca true, jeśli ten zbiór nie zawiera żadnych elementów.

  18. Iterator iterator(): Zwraca iterator po elementach tego zbioru w porządku rosnącym.

  19. E last(): Zwraca ostatni (najwyższy) element aktualnie w tym zbiorze.

  20. E lower(E e): Zwraca największy element w tym zbiorze ściśle mniejszy niż podany element, lub null, jeśli taki element nie istnieje.

  21. E pollFirst(): Pobiera i usuwa pierwszy (najniższy) element, lub zwraca null, jeśli ten zbiór jest pusty.

  22. E pollLast(): Pobiera i usuwa ostatni (najwyższy) element, lub zwraca null, jeśli ten zbiór jest pusty.

  23. boolean remove(Object o): Usuwa określony element z tego zbioru, jeśli jest obecny.

  24. int size(): Zwraca liczbę elementów w tym zbiorze (jego moc).

  25. Spliterator spliterator(): Tworzy późno wiążący i szybko reagujący na błędy Spliterator po elementach tego zbioru.

  26. NavigableSet subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive): Zwraca widok części tego zbioru, którego elementy mieszczą się w zakresie od fromElement do toElement.

  27. SortedSet subSet(E fromElement, E toElement): Zwraca widok części tego zbioru, którego elementy mieszczą się w zakresie od fromElement, włącznie, do toElement, wyłącznie.

  28. SortedSet tailSet(E fromElement): Zwraca widok części tego zbioru, którego elementy są większe lub równe fromElement.

  29. NavigableSet tailSet(E fromElement, boolean inclusive): Zwraca widok części tego zbioru, którego elementy są większe niż (lub równe, jeśli inclusive jest true) fromElement.

Przykład

Projekt W13, example28

HashMap (mapa mieszająca)

HashMap<K,V> w Javie to kolekcja, która przechowuje pary klucz-wartość. Jest to część Java Collections Framework i oferuje efektywne sposoby przechowywania i dostępu do danych.

  1. Struktura klucz-wartość: Każdy element w HashMap składa się z klucza (unikalnego identyfikatora) i przypisanej mu wartości.

  2. Szybki dostęp do danych: Dzięki funkcji mieszającej (hashing), HashMap pozwala na bardzo szybkie wyszukiwanie wartości na podstawie klucza. Operacje takie jak wstawianie, wyszukiwanie lub usuwanie elementów mają zazwyczaj stałą złożoność czasową O(1).

  3. Unikalność kluczy: W HashMap każdy klucz musi być unikalny. Próba wstawienia duplikatu klucza spowoduje nadpisanie starej wartości przez nową.

  4. Null jako klucz i wartość: HashMap pozwala na użycie null jako klucza (ale tylko raz, ponieważ klucze są unikalne) oraz null jako wartości.

  5. Nieuporządkowana: Kolejność przechowywania par klucz-wartość w HashMap nie jest gwarantowana i może się zmieniać z czasem, zwłaszcza po operacjach, które zmieniają rozmiar mapy, np. po dodaniu nowych elementów.

Metody: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/HashMap.html

  1. void clear(): Usuwa wszystkie mapowania z tej mapy.

  2. Object clone(): Zwraca płytką kopię tej instancji HashMap: klucze i wartości same w sobie nie są klonowane.

  3. V compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction): Próbuje obliczyć mapowanie dla określonego klucza i jego aktualnie mapowanej wartości (lub null, jeśli mapowanie nie istnieje).

  4. V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction): Jeśli określony klucz nie jest jeszcze skojarzony z wartością (lub jest mapowany na null), próbuje obliczyć jego wartość za pomocą podanej funkcji mapującej i wprowadza ją do tej mapy, chyba że wynik jest null.

  5. V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction): Jeśli wartość dla określonego klucza jest obecna i nie jest null, próbuje obliczyć nowe mapowanie na podstawie klucza i jego aktualnie mapowanej wartości.

  6. boolean containsKey(Object key): Zwraca true, jeśli ta mapa zawiera mapowanie dla określonego klucza.

  7. boolean containsValue(Object value): Zwraca true, jeśli ta mapa mapuje jeden lub więcej kluczy na określoną wartość.

  8. Set<Map.Entry<K,V>> entrySet(): Zwraca widok zbioru mapowań zawartych w tej mapie.

  9. V get(Object key): Zwraca wartość, do której mapowany jest określony klucz, lub null, jeśli ta mapa nie zawiera mapowania dla klucza.

  10. boolean isEmpty(): Zwraca true, jeśli ta mapa nie zawiera żadnych mapowań klucz-wartość.

  11. Set keySet(): Zwraca widok zbioru kluczy zawartych w tej mapie.

  12. V merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction): Jeśli określony klucz nie jest już skojarzony z wartością lub jest skojarzony z null, kojarzy go z podaną wartością niebędącą null.

  13. static <K,V> HashMap<K,V> newHashMap(int numMappings): Tworzy nową, pustą HashMap odpowiednią dla oczekiwanej liczby mapowań.

  14. V put(K key, V value): Kojarzy określoną wartość z określonym kluczem w tej mapie.

  15. void putAll(Map<? extends K,? extends V> m): Kopiuje wszystkie mapowania z określonej mapy do tej mapy.

  16. V remove(Object key): Usuwa mapowanie dla określonego klucza z tej mapy, jeśli jest obecne.

  17. int size(): Zwraca liczbę mapowań klucz-wartość w tej mapie.

  18. Collection values(): Zwraca widok kolekcji wartości zawartych w tej mapie.

Przykład

Projekt W13, example29

Projekt W13, example30

TreeMap (mapa drzewiasta)

TreeMap<K,V> w Javie to posortowana mapa bazująca na czerwono-czarnym drzewie. Jest to część Java Collections Framework i działa jako mapa klucz-wartość, gdzie każdy klucz jest unikalny.

  1. Automatyczne sortowanie: Klucze w TreeMap są sortowane według naturalnego porządku (jeśli implementują interfejs Comparable) lub według Comparatora przekazanego przy tworzeniu mapy. Dzięki temu, kiedy iterujesz po kluczach, są one zwracane w posortowanej kolejności.

  2. Wykonanie: Operacje wyszukiwania, wstawiania i usuwania mają logarytmiczną złożoność czasową O(log n), co sprawia, że TreeMap jest wydajna dla dużych zbiorów danych.

  3. Null jako klucz: Standardowo TreeMap nie akceptuje null jako klucza (w przeciwieństwie do HashMap), szczególnie gdy używa naturalnego porządkowania, ponieważ null nie może być porównywany.

  4. Dostęp do pierwszego i ostatniego elementu: TreeMap zapewnia szybki dostęp do pierwszego (najmniejszego) i ostatniego (największego) klucza.

  5. Widoki: TreeMap oferuje metody takie jak headMap, tailMap i subMap, które pozwalają na tworzenie widoków określonych zakresów mapy.

Metody: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/TreeMap.html

  1. Map.Entry<K,V> ceilingEntry(K key): Zwraca mapowanie klucz-wartość związane z najmniejszym kluczem większym lub równym podanemu kluczowi, lub null, jeśli taki klucz nie istnieje.

  2. K ceilingKey(K key): Zwraca najmniejszy klucz większy lub równy podanemu kluczowi, lub null, jeśli taki klucz nie istnieje.

  3. void clear(): Usuwa wszystkie mapowania z tej mapy.

  4. Object clone(): Zwraca płytką kopię tej instancji TreeMap.

  5. Comparator<? super K> comparator(): Zwraca komparator używany do porządkowania kluczy w tej mapie, lub null, jeśli mapa używa naturalnego porządkowania swoich kluczy.

  6. V compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction): Próbuje obliczyć mapowanie dla określonego klucza i jego aktualnie mapowanej wartości (lub null, jeśli mapowanie nie istnieje).

  7. V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction): Jeśli określony klucz nie jest już skojarzony z wartością (lub jest mapowany na null), próbuje obliczyć jego wartość za pomocą podanej funkcji mapującej i wprowadza ją do tej mapy, chyba że wynik jest null.

  8. V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction): Jeśli wartość dla określonego klucza jest obecna i nie jest null, próbuje obliczyć nowe mapowanie na podstawie klucza i jego aktualnie mapowanej wartości.

  9. boolean containsKey(Object key): Zwraca true, jeśli ta mapa zawiera mapowanie dla określonego klucza.

  10. boolean containsValue(Object value): Zwraca true, jeśli ta mapa mapuje jeden lub więcej kluczy na określoną wartość.

  11. NavigableSet descendingKeySet(): Zwraca widok w odwrotnej kolejności NavigableSet kluczy zawartych w tej mapie.

  12. NavigableMap<K,V> descendingMap(): Zwraca widok w odwrotnej kolejności mapowań zawartych w tej mapie.

  13. Set<Map.Entry<K,V>> entrySet(): Zwraca widok zbioru mapowań zawartych w tej mapie.

  14. Map.Entry<K,V> firstEntry(): Zwraca mapowanie klucz-wartość związane z najmniejszym kluczem w tej mapie, lub null, jeśli mapa jest pusta.

  15. K firstKey(): Zwraca pierwszy (najniższy) klucz obecnie w tej mapie.

  16. Map.Entry<K,V> floorEntry(K key): Zwraca mapowanie klucz-wartość związane z największym kluczem mniejszym lub równym podanemu kluczowi, lub null, jeśli taki klucz nie istnieje.

  17. K floorKey(K key): Zwraca największy klucz mniejszy lub równy podanemu kluczowi, lub null, jeśli taki klucz nie istnieje.

  18. V get(Object key): Zwraca wartość, do której mapowany jest określony klucz, lub null, jeśli ta mapa nie zawiera mapowania dla klucza.

  19. SortedMap<K,V> headMap(K toKey): Zwraca widok części tej mapy, której klucze są ściśle mniejsze niż toKey.

  20. NavigableMap<K,V> headMap(K toKey, boolean inclusive): Zwraca widok części tej mapy, której klucze są mniejsze niż (lub równe, jeśli inclusive jest true) toKey.

  21. Map.Entry<K,V> higherEntry(K key): Zwraca map

owanie klucz-wartość związane z najmniejszym kluczem ściśle większym niż podany klucz, lub null, jeśli taki klucz nie istnieje.

  1. K higherKey(K key): Zwraca najmniejszy klucz ściśle większy niż podany klucz, lub null, jeśli taki klucz nie istnieje.

  2. Set keySet(): Zwraca widok zbioru kluczy zawartych w tej mapie.

  3. Map.Entry<K,V> lastEntry(): Zwraca mapowanie klucz-wartość związane z największym kluczem w tej mapie, lub null, jeśli mapa jest pusta.

  4. K lastKey(): Zwraca ostatni (najwyższy) klucz obecnie w tej mapie.

  5. Map.Entry<K,V> lowerEntry(K key): Zwraca mapowanie klucz-wartość związane z największym kluczem ściśle mniejszym niż podany klucz, lub null, jeśli taki klucz nie istnieje.

  6. K lowerKey(K key): Zwraca największy klucz ściśle mniejszy niż podany klucz, lub null, jeśli taki klucz nie istnieje.

  7. V merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction): Jeśli określony klucz nie jest już skojarzony z wartością lub jest skojarzony z null, kojarzy go z podaną wartością niebędącą null.

  8. NavigableSet navigableKeySet(): Zwraca widok NavigableSet kluczy zawartych w tej mapie.

  9. Map.Entry<K,V> pollFirstEntry(): Usuwa i zwraca mapowanie klucz-wartość związane z najmniejszym kluczem w tej mapie, lub null, jeśli mapa jest pusta.

  10. Map.Entry<K,V> pollLastEntry(): Usuwa i zwraca mapowanie klucz-wartość związane z największym kluczem w tej mapie, lub null, jeśli mapa jest pusta.

  11. V put(K key, V value): Kojarzy określoną wartość z określonym kluczem w tej mapie.

  12. void putAll(Map<? extends K,? extends V> map): Kopiuje wszystkie mapowania z określonej mapy do tej mapy.

  13. V putFirst(K k, V v): Rzuca wyjątek UnsupportedOperationException.

  14. V putLast(K k, V v): Rzuca wyjątek UnsupportedOperationException.

  15. V remove(Object key): Usuwa mapowanie dla tego klucza z tej mapy TreeMap, jeśli jest obecne.

  16. int size(): Zwraca liczbę mapowań klucz-wartość w tej mapie.

  17. NavigableMap<K,V> subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive): Zwraca widok części tej mapy, której klucze mieszczą się w zakresie od fromKey do toKey.

  18. SortedMap<K,V> subMap(K fromKey, K toKey): Zwraca widok części tej mapy, której klucze mieszczą się w zakresie od fromKey, włącznie, do toKey, wyłącznie.

  19. SortedMap<K,V> tailMap(K fromKey): Zwraca widok części tej mapy, której klucze są większe lub równe fromKey.

  20. NavigableMap<K,V> tailMap(K fromKey, boolean inclusive): Zwraca widok części tej mapy, której klucze są większe niż (lub równe, jeśli inclusive jest true) fromKey.

  21. Collection values(): Zwraca widok kolekcji wartości zawartych w tej mapie.

Przykład

Projekt W13, example31

Projekt W13, example32

Algorytmy generyczne - klasa Collections

https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Collections.html

Klasa Collections w Javie pełni rolę narzędzia pomocniczego (utility class) dla kolekcji w Java Collections Framework. Zawiera ona statyczne metody, które działają na lub zwracają kolekcje. Jest to klasa czysto statyczna, co oznacza, że nie jest przeznaczona do tworzenia instancji (obiektów), lecz służy jako zbiór narzędzi dla różnych operacji na kolekcjach.