1. Problem „surowych” kolekcji (raw types)
Na chwilę zanurzymy się w historię. Do czasu Java 5 wszystkie kolekcje były „wszystkożerne”. Przechowywały obiekty typu Object, a kompilator nie kontrolował, co dokładnie do nich wkładasz. Chcesz włożyć łańcuch? Proszę bardzo. Liczbę? Czemu nie. Kota? Też można.
// Przykład "surowych" kolekcji (raw types), Java przed wersją 5
List list = new ArrayList();
list.add("Cześć");
list.add(42);
list.add(new Object());
Problem ujawniał się przy wyjmowaniu i używaniu wartości:
String s = (String) list.get(0); // OK, to jest String
String s2 = (String) list.get(1); // BUM! ClassCastException
Kompilator milczy, a w czasie wykonania dostajesz ClassCastException. To jak pudełko z napisem „jabłka”, w którym leżą kubek, banan i jeżyk.
Dlaczego to jest złe?
- Błędy ujawniają się dopiero w czasie wykonywania.
- Mylą się typy: trzeba ręcznie rzutować obiekty na właściwy typ (cast).
- Kod jest mniej czytelny i bardziej ryzykowny.
Rozwiązanie — generics (typy generyczne)
Generics (typy generyczne) to mechanizm pozwalający tworzyć klasy, interfejsy i metody z parametrami typu. Mówisz kolekcji: „Przechowuj tylko Stringi”, a kompilator tego pilnuje.
List<String> words = new ArrayList<>();
words.add("Cześć");
words.add("Świat");
// words.add(42); // Błąd kompilacji! Nie można dodać int do List<String>
Teraz kompilator nie pozwoli włożyć do listy niczego poza Stringami. Błąd zostanie wychwycony przed uruchomieniem programu.
Główna idea generics:
Zapewnić bezpieczeństwo typów kolekcji (i nie tylko), aby błędy były wychwytywane na etapie kompilacji, a nie w czasie wykonywania.
2. Składnia generics: jak to wygląda w kodzie
Wskazywanie typu w nawiasach kątowych
Tworząc kolekcję, wskaż typ elementów w <>:
List<String> names = new ArrayList<>();
names.add("Alicja");
names.add("Bob");
// names.add(123); // Błąd: nie można dodać liczby do listy Stringów
String first = names.get(0); // Nie trzeba rzutowania!
Klasyka:
- List<String> — lista Stringów
- List<Integer> — lista liczb całkowitych
- Set<Double> — zbiór liczb zmiennoprzecinkowych
- Map<String, Integer> — klucz String, wartość Integer
Dlaczego nie pisać po prostu List?
Można, ale tracisz wszystkie zalety generics i kompilator ostrzeże:
List list = new ArrayList(); // raw type — niewskazane!
list.add("Hello");
list.add(7.5);
String s = (String) list.get(1); // Witaj, ClassCastException!
Współczesny kod Java zawsze używa generics.
Operator diamentu <>
Od Java 7 nie trzeba podawać typu po prawej stronie, jeśli wynika on z kontekstu:
List<String> list = new ArrayList<>(); // Kompilator sam zrozumie, że chodzi o <String>
3. Generics dla różnych kolekcji
Przykłady dla List, Set, Map
List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
Set<String> uniqueNames = new HashSet<>();
uniqueNames.add("Alicja");
uniqueNames.add("Bob");
Map<String, Integer> ages = new HashMap<>();
ages.put("Alicja", 23);
ages.put("Bob", 31);
Przykład z własną klasą
class Student {
String name;
int age;
// ...
}
List<Student> students = new ArrayList<>();
students.add(new Student());
4. Przydatne niuanse
Zalety generics
Bezpieczeństwo typów. Kompilator pilnuje, by do kolekcji trafiały tylko elementy wymaganego typu.
Brak potrzeby rzutowania typów. Kiedyś: String s = (String) list.get(0);. Teraz: String s = list.get(0);.
Kod czytelniejszy i bardziej niezawodny. Mniej niespodzianek w czasie wykonania.
Ograniczenia generics
Nie można używać typów prymitywnych. Generics działają tylko z obiektami, nie z prymitywami (int, double, boolean). Używaj klas opakowujących: Integer, Double, Boolean.
List<Integer> numbers = new ArrayList<>();
numbers.add(10); // int automatycznie zamienia się w Integer (autoboxing)
Krótko o wymazywaniu typów (type erasure)
W Javie generics są zaimplementowane poprzez mechanizm wymazywania typów: po kompilacji informacja o parametrach typu jest wymazywana i w czasie wykonania JVM nie wie, że to był List<String>, a nie po prostu List. Zrobiono to ze względu na wsteczną kompatybilność.
Konsekwencja: nie można sprawdzić parametru typu przez instanceof z konkretnym argumentem typu.
List<String> list = new ArrayList<>();
// if (list instanceof List<String>) { ... } // Błąd kompilacji!
Próba dodania elementu innego typu — błąd kompilacji
List<String> words = new ArrayList<>();
words.add("Hello");
// words.add(123); // Błąd kompilacji: incompatible types: int cannot be converted to String
Map<String, Integer> map = new HashMap<>();
map.put("Kot", 5);
// map.put(3, "Słoń"); // Błąd: klucz musi być String, a wartość — Integer
I to świetnie: błędy są wychwytywane na etapie kompilacji.
Nie tylko kolekcje
Generics można stosować we własnych klasach i metodach. Na przykład uniwersalne „Pudełko”:
class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> stringBox = new Box<>();
stringBox.set("Cześć");
System.out.println(stringBox.get());
Box<Integer> intBox = new Box<>();
intBox.set(42);
System.out.println(intBox.get());
W kolekcjach generics to standard, ale spotkasz je też w innych miejscach, np. w Stream API i Optional.
5. Typowe błędy przy pracy z generics
Błąd nr 1: używanie „surowych” kolekcji. Zapis w stylu List list = new ArrayList(); pozbawia bezpieczeństwa typów. Zawsze podawaj parametry typu, np. List<String>.
Błąd nr 2: próba użycia typów prymitywnych. Nie można napisać List<int>, użyj List<Integer>.
Błąd nr 3: ręczne rzutowanie przy odczycie z kolekcji. Jeśli używasz generics, rzutowanie w stylu (String) list.get(i) nie jest potrzebne. Jeśli musisz — gdzieś naruszyłeś deklarowane typy.
Błąd nr 4: oczekiwanie, że parametry typów są dostępne w czasie wykonania. Z powodu wymazywania typów nie można sprawdzać ich przez instanceof w rodzaju List<String>.
Błąd nr 5: mieszanie różnych typów w jednej kolekcji. Jeśli zadeklarowano List<String>, nie próbuj dodawać Integer — kompilator na to nie pozwoli, i dobrze.
GO TO FULL VERSION