Jeśli programujesz w Javie od kilku miesięcy, prawdopodobnie spotkałeś się z dziwną składnią jak List<String> lub Map<String, Integer>. Te zagadkowe nawiasy kątowe to Generics – jedna z najważniejszych funkcji wprowadzonych w Java 5, która na zawsze zmieniła sposób pisania kodu.
Dlaczego Generics są ważne
Przed Java 5 (2004), wszystkie kolekcje przechowywały obiekty typu Object. Oznaczało to, że mogłeś dodać do listy wszystko – String, Integer, nawet całe obiekty. Ale kiedy chciałeś coś z tej listy wyciągnąć, musiałeś ręcznie rzutować na odpowiedni typ. I tu zaczynały się problemy.
Co się nauczysz:
- Dlaczego Generics zostały wprowadzone do Javy
- Jak używać Generics z kolekcjami
- Jak tworzyć własne klasy generyczne
- Czym są wildcards i kiedy ich używać
- Jakich błędów unikać przy pracy z Generics
Wymagania wstępne:
- Podstawowa znajomość klas i obiektów w Javie
- Rozumienie kolekcji (List, Set, Map)
- Znajomość rzutowania typów (casting)
- Podstawy dziedziczenia i polimorfizmu
Problem który rozwiązują Generics
Zobaczmy jak wyglądał kod przed Generics (Java 4 i wcześniejsze):
// Kod bez Generics - tak było do Java 5 List names = new ArrayList(); names.add("Jan"); names.add("Anna"); names.add(123); // Ups! Dodałem liczba zamiast String // Pobieranie danych wymagało rzutowania String firstName = (String) names.get(0); // OK String secondName = (String) names.get(1); // OK String thirdName = (String) names.get(2); // ClassCastException!
A teraz ten sam kod z Generics:
// Kod z Generics - Java 5+ List<String> names = new ArrayList<String>(); names.add("Jan"); names.add("Anna"); names.add(123); // Błąd kompilacji! Nie można dodać Integer do List<String> // Pobieranie bez rzutowania String firstName = names.get(0); // Automatycznie String String secondName = names.get(1); // Bezpieczne i czytelne
List<String> names = new ArrayList<>();
– kompilator sam wywnioskuje typ.Generics z kolekcjami – podstawy
Najczęściej spotkasz Generics w kolekcjach. Oto najpopularniejsze zastosowania:
Lista z określonym typem
// Lista Stringów List<String> cities = new ArrayList<String>(); cities.add("Warszawa"); cities.add("Kraków"); // Lista liczb List<Integer> numbers = new ArrayList<Integer>(); numbers.add(10); numbers.add(20); // Lista własnych obiektów List<User> users = new ArrayList<User>(); users.add(new User("Jan", "Kowalski"));
Map z określonymi typami klucza i wartości
// String jako klucz, Integer jako wartość Map<String, Integer> ages = new HashMap<String, Integer>(); ages.put("Jan", 25); ages.put("Anna", 30); // Pobieranie bez rzutowania Integer janAge = ages.get("Jan"); // Automatycznie Integer // Iterowanie po Map z Generics for (Map.Entry<String, Integer> entry : ages.entrySet()) { String name = entry.getKey(); // String Integer age = entry.getValue(); // Integer System.out.println(name + " ma " + age + " lat"); }
Tworzenie własnych klas generycznych
Generics nie są ograniczone tylko do kolekcji. Możesz tworzyć własne klasy generyczne:
// Prosta klasa generyczna public class Box<T> { private T content; public void put(T item) { this.content = item; } public T get() { return content; } public boolean isEmpty() { return content == null; } }
Użycie takiej klasy:
// Box przechowujący String Box<String> stringBox = new Box<String>(); stringBox.put("Hello World"); String message = stringBox.get(); // Automatycznie String // Box przechowujący Integer Box<Integer> numberBox = new Box<Integer>(); numberBox.put(42); Integer number = numberBox.get(); // Automatycznie Integer // Box przechowujący własny obiekt Box<User> userBox = new Box<User>(); userBox.put(new User("Jan", "Kowalski")); User user = userBox.get(); // Automatycznie User
Klasa z wieloma parametrami typu
public class Pair<K, V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } } // Użycie Pair<String, Integer> nameAge = new Pair<String, Integer>("Jan", 25); String name = nameAge.getKey(); // String Integer age = nameAge.getValue(); // Integer
Wildcards – elastyczność w Generics
Czasami potrzebujesz więcej elastyczności niż konkretny typ. Do tego służą wildcards:
Wildcard ? (unknown type)
// Lista dowolnego typu List<?> unknownList = new ArrayList<String>(); unknownList = new ArrayList<Integer>(); // Też działa // Przydatne w metodach które przyjmują różne typy public void printListSize(List<?> list) { System.out.println("Lista ma " + list.size() + " elementów"); } // Działa z każdym typem printListSize(new ArrayList<String>()); printListSize(new ArrayList<Integer>()); printListSize(new ArrayList<User>());
Bounded wildcards
// Tylko Number i jego podklasy List<? extends Number> numbers = new ArrayList<Integer>(); numbers = new ArrayList<Double>(); // Też działa // Tylko Number i jego nadklasy List<? super Integer> integers = new ArrayList<Number>(); integers = new ArrayList<Object>(); // Też działa
List<? extends Number>
możesz tylko czytać, nie możesz dodawać elementów (poza null). Z List<? super Integer>
możesz dodawać Integer, ale czytanie zwraca Object.Praktyczny przykład – Repository pattern
Zobaczmy jak Generics używane są w praktyce w wzorcu Repository:
// Generyczne repository public interface Repository<T, ID> { void save(T entity); T findById(ID id); List<T> findAll(); void delete(ID id); } // Konkretna implementacja dla User public class UserRepository implements Repository<User, Long> { @Override public void save(User user) { // Logika zapisu użytkownika } @Override public User findById(Long id) { // Logika wyszukiwania po ID return new User(); // Placeholder } @Override public List<User> findAll() { return new ArrayList<User>(); } @Override public void delete(Long id) { // Logika usuwania } }
JpaRepository<Entity, ID>
i automatycznie otrzymuje metody CRUD.Ograniczenia i rzeczy które warto wiedzieć
Type Erasure
Generics w Javie są implementowane przez „type erasure” – informacje o typach są usuwane podczas kompilacji:
List<String> stringList = new ArrayList<String>(); List<Integer> intList = new ArrayList<Integer>(); // W runtime oba to po prostu List System.out.println(stringList.getClass() == intList.getClass()); // true! // Nie możesz sprawdzić typu parametru w runtime // if (stringList instanceof List<String>) {} // Błąd kompilacji
Nie można tworzyć tablic Generics
// Nie działa // List<String>[] arrays = new List<String>[10]; // Błąd kompilacji // Zamiast tego użyj List lub wildcards List<String>[] arrays = new List[10]; // Unchecked warning List<List<String>> listOfLists = new ArrayList<List<String>>(); // Lepiej
Common mistakes – błędy początkujących
List<String>
zamiast List
.List<String>
to podklasa List<Object>
. To nie prawda – nie ma między nimi związku dziedziczenia.(List<String>) rawList
zamiast sprawdzenia każdego elementu osobno.List<Object>
gdy chcesz przechowywać różne typy. Lepiej użyj konkretnej hierarchii klas lub union types.Nie bezpośrednio. Musisz użyć wrapper classes: List<Integer>
zamiast List<int>
. Java automatycznie konwertuje (autoboxing/unboxing).
Kompilator ostrzega że nie może sprawdzić bezpieczeństwa typów, zwykle gdy mieszasz kod z Generics i bez. Dodaj @SuppressWarnings(„unchecked”) jeśli jesteś pewien bezpieczeństwa.
Reguła PECS: Producer-Extends, Consumer-Super. Jeśli tylko czytasz z kolekcji – extends. Jeśli dodajesz do kolekcji – super.
Tak: class StringBox extends Box<String>
lub class GenericBox<T> extends Box<T>
. Możesz konkretyzować typ lub przekazać parametr dalej.
To różne typy dla kompilatora, nawet jeśli w runtime to te same klasy. Generics zapewniają type safety w compile time.
To gdy typy są dostępne w runtime. Java ma type erasure, więc Generics nie są reified – informacja o typie znika po kompilacji.
Nie. Po kompilacji kod z Generics działa tak samo szybko jak bez nich. Zyskujesz bezpieczeństwo bez straty wydajności.
Przydatne zasoby:
🚀 Zadanie dla Ciebie
Stwórz własną klasę generyczną Cache<K, V>
która:
- Przechowuje pary klucz-wartość w Map
- Ma metodę
put(K key, V value)
- Ma metodę
get(K key)
zwracającą V lub null - Ma metodę
contains(K key)
zwracającą boolean - Ma maksymalny rozmiar i usuwa najstarsze elementy
Przetestuj z różnymi typami: Cache<String, Integer>
, Cache<Integer, String>
, Cache<String, User>
.
Masz pytania o Generics w Javie? Podziel się swoimi doświadczeniami w komentarzach – często spotykanym problemem są wildcards, chętnie pomogę!