CodeGym /Kursy /JAVA 25 SELF /Generics wildcards

Generics wildcards

JAVA 25 SELF
Poziom 27 , Lekcja 4
Dostępny

1. Inwariantność generyków: List<Number> ≠ List<Integer>

W Javie generyki są rygorystyczne: są inwariantne. Oznacza to, że nawet jeśli jeden typ jest podtypem innego (na przykład Integer jest podtypem Number), kolekcje z tymi typami nie są ze sobą powiązane.

Wyobraź sobie: masz pudełko z jabłkami (List<Integer>), a ktoś mówi, że to w ogóle „pudełko z owocami” (List<Number>). Brzmi logicznie, przecież jabłka to owoce. Ale wtedy do tego pudełka można byłoby włożyć banana (Double) i wszystko by się zepsuło – to przecież tak naprawdę pudełko na jabłka.

Przykład kodu:

List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // Błąd kompilacji!

Kompilator jest tu celowo restrykcyjny: nie pozwala zamienić listy „jabłek” na listę „owoców”. W przeciwnym razie moglibyśmy do niej dodać cokolwiek i program wywróciłby się dopiero w czasie wykonywania.

Dlatego List<Number> i List<Integer> to dwa zupełnie różne typy, mimo że sam Integer jest podtypem Number.

W przeciwieństwie do tablic:

Integer[] intArr = new Integer[10];
Number[] numArr = intArr; // Dozwolone (tablice są kowariantne)
numArr[0] = 3.14; // Błąd w czasie wykonywania (ArrayStoreException)

Wniosek: generyki są inwariantne, aby zapewnić bezpieczeństwo typów na etapie kompilacji.

2. Granice parametrów typów

Czasem trzeba ograniczyć, z jakimi typami może pracować klasa lub metoda generyczna. Do tego służą ograniczenia (bounds).

Górna granica (extends)

class Stats<T extends Number> {
    private T[] nums;
    // ...
}

Teraz Stats może pracować tylko z typami, które są potomkami Number (Integer, Double, Float itd.). Próba utworzenia Stats<String> spowoduje błąd kompilacji.

Ograniczenie z interfejsem

class Sorter<T extends Comparable<T>> {
    void sort(List<T> list) { /* ... */ }
}

Teraz Sorter może pracować tylko z typami, które implementują interfejs Comparable.

Wielokrotne ograniczenia

Można wskazać kilka ograniczeń, łącząc je znakiem &:

class MyClass<T extends Number & Comparable<T>> { /* ... */ }

T musi być podtypem Number i implementować Comparable<T>.

Kolejność ma znaczenie: najpierw klasa, potem interfejsy.

3. Wprowadzenie do wildcard: ?

Wildcard to „typ podstawieniowy”, który mówi: „Tutaj może być jakiś typ, ale nie wiem, który dokładnie”.

Przykłady:

  • List<?> — lista czegokolwiek.
  • List<? extends Number> — lista dowolnego typu będącego podtypem Number (np. Integer, Double).
  • List<? super Integer> — lista dowolnego typu będącego supertypem Integer (np. Integer, Number, Object).

Po co są wildcardy?

Pozwalają pisać metody, które działają z kolekcjami różnych, ale powiązanych typów.

Przykład: tylko do odczytu (producer)

void printNumbers(List<? extends Number> list) {
    for (Number n : list) {
        System.out.println(n);
    }
    // list.add(123); // Błąd! Nie można dodawać elementów
}
  • Można czytać elementy jako Number.
  • Nie wolno dodawać elementów (poza null).

Przykład: tylko do zapisu (consumer)

void addIntegers(List<? super Integer> list) {
    list.add(42); // OK
    // Integer x = list.get(0); // Błąd! Nie wiemy, jaki typ zostanie zwrócony
}
  • Można dodawać elementy typu Integer (lub jego podtypów).
  • Nie można bezpiecznie odczytać elementów jako Integer (można tylko jako Object).

Zasada PECS

PECS – „Producer Extends, Consumer Super”:

  • Producer – Extends: jeśli kolekcja tylko dostarcza dane (producer), użyj ? extends T.
  • Consumer – Super: jeśli kolekcja tylko przyjmuje dane (consumer), użyj ? super T.

Łatwo zapamiętać: extends – do odczytu (producer), super – do zapisu (consumer).

Porównanie: tablice są kowariantne, generyki inwariantne

  • Tablice są kowariantne: Integer[] można przypisać do zmiennej typu Number[].
  • Generyki są inwariantne: List<Integer> nie można przypisać do zmiennej typu List<Number>.

Wildcardy pozwalają częściowo „złagodzić” inwariantność generyków.

5. Metody generyczne i wnioskowanie typów; ograniczenia (type erasure)

Metody generyczne

Można deklarować metody generyczne, które działają z dowolnymi typami:

public static <T> void printList(List<T> list) {
    for (T elem : list) {
        System.out.println(elem);
    }
}

Wnioskowanie typu (type inference)

Java potrafi „domyślić się” typu parametru:

List<String> strings = List.of("a", "b");
printList(strings); // T = String

Ograniczenia generyków: wymazywanie typów

  • W Javie generyki są zaimplementowane poprzez wymazywanie typów: informacja o typach parametrów jest usuwana po kompilacji.
  • W bajtkodzie nie ma różnicy między List<String> a List<Integer>.
  • Nie wolno tworzyć tablic typów generycznych: new List<String>[10] – błąd.
  • Nie wolno używać instanceof z parametrami typu: obj instanceof List<String> – błąd.

6. Praktyka na kolekcjach i Stream API

Kopiowanie elementów między listami

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T item : src) {
        dest.add(item);
    }
}
  • src — producer (? extends T)
  • dest — consumer (? super T)

Użycie:

List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new ArrayList<>();
copy(nums, ints); // OK: Number — supertyp Integer

Stream API i wildcardy

List<Integer> ints = List.of(1, 2, 3);
List<? extends Number> numbers = ints;

numbers.stream()
    .map(Number::doubleValue)
    .forEach(System.out::println);

Filtrowanie z wildcard

public static void printAll(List<?> list) {
    for (Object o : list) {
        System.out.println(o);
    }
}

7. Częste błędy

Błąd nr 1: raw types (surowe typy).
Użycie „surowych” typów wyłącza sprawdzanie typów i prowadzi do błędów w czasie wykonywania.

List list = new ArrayList(); // raw type — źle!
list.add("tekst");
list.add(123); // Można dodać cokolwiek

String s = (String) list.get(1); // ClassCastException!

Nigdy nie używaj raw types. Zawsze podawaj parametry typu: List<String>, List<Integer>.

Błąd nr 2: Niebezpieczne operacje z extends.
Próba dodawania elementów do kolekcji z ? extends ... skończy się błędem kompilacji.

List<? extends Number> nums = new ArrayList<Integer>();
nums.add(3.14); // Błąd kompilacji!

Błąd nr 3: „Zacieranie” typu przy przeciążaniu.
Nie można przeciążać metod wyłącznie różnicą w parametrach generycznych – po wymazaniu typów sygnatury są identyczne.

public void process(List<String> list) { /* ... */ }
public void process(List<Integer> list) { /* ... */ } // Błąd kompilacji!

Błąd nr 4: Tablice typów generycznych.
Nie wolno tworzyć tablic typów parametryzowanych z powodu wymazywania typów.

List<String>[] arr = new List<String>[10]; // Błąd kompilacji!
1
Zadanie
JAVA 25 SELF, poziom 27, lekcja 4
Niedostępne
Obliczanie łącznej wartości aktywów 💰
Obliczanie łącznej wartości aktywów 💰
1
Zadanie
JAVA 25 SELF, poziom 27, lekcja 4
Niedostępne
Uniwersalny system logowania zdarzeń 📜
Uniwersalny system logowania zdarzeń 📜
1
Ankieta/quiz
Interfejsy kolekcji, poziom 27, lekcja 4
Niedostępny
Interfejsy kolekcji
Interfejsy kolekcji
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION