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!
GO TO FULL VERSION