Generics w Javie – po co i jak używać

TL;DR: Generics w Javie to sposób na pisanie bezpieczniejszego kodu, który eliminuje ClassCastException i czyni kod bardziej czytelnym. Zamiast List przechowującej Object, masz List<String> która przechowuje tylko String. Kompilator sprawdza typy za Ciebie.

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.

W 2016 roku Generics to standard w każdym nowoczesnym projekcie Java. Firmy wymagają znajomości tej technologii, ponieważ znacznie redukuje błędy w kodzie produkcyjnym i czyni aplikacje bardziej niezawodnymi.

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

Analogia: Generics to jak etykiety na pudełkach. Zamiast mieć pudełko „różne rzeczy” gdzie nie wiesz co znajdziesz, masz pudełko „tylko książki” gdzie gwarantujesz sobie że zawsze wyciągniesz książkę.

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!
Problem: Kompilator nie sprawdzał typów. Błąd pojawiał się dopiero w runtime jako ClassCastException – w najgorszym momencie, gdy aplikacja już działała u klienta.

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
Pro tip: Od Java 7 możesz używać „diamond operator” <> po prawej stronie: 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
T – to konwencja nazewnictwa dla „type parameter”. Możesz użyć dowolnej nazwy, ale T (Type), E (Element), K (Key), V (Value) to standardy.

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
Pułapka: Z 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
    }
}
Ten wzorzec jest używany w Spring Data JPA – każdy repository dziedziczy po 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

Błąd #1: Używanie raw types (bez Generics) w nowym kodzie. Zawsze określaj typ: List<String> zamiast List.
Błąd #2: Myślenie że List<String> to podklasa List<Object>. To nie prawda – nie ma między nimi związku dziedziczenia.
Błąd #3: Próba rzutowania (List<String>) rawList zamiast sprawdzenia każdego elementu osobno.
Błąd #4: Używanie List<Object> gdy chcesz przechowywać różne typy. Lepiej użyj konkretnej hierarchii klas lub union types.
Czy mogę użyć primitives w Generics?

Nie bezpośrednio. Musisz użyć wrapper classes: List<Integer> zamiast List<int>. Java automatycznie konwertuje (autoboxing/unboxing).

Co oznacza warning „unchecked”?

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.

Kiedy używać extends a kiedy super?

Reguła PECS: Producer-Extends, Consumer-Super. Jeśli tylko czytasz z kolekcji – extends. Jeśli dodajesz do kolekcji – super.

Czy mogę dziedziczyć po klasie generycznej?

Tak: class StringBox extends Box<String> lub class GenericBox<T> extends Box<T>. Możesz konkretyzować typ lub przekazać parametr dalej.

Dlaczego nie mogę porównać List<String> z List<Integer>?

To różne typy dla kompilatora, nawet jeśli w runtime to te same klasy. Generics zapewniają type safety w compile time.

Co to jest reification?

To gdy typy są dostępne w runtime. Java ma type erasure, więc Generics nie są reified – informacja o typie znika po kompilacji.

Czy Generics wpływają na wydajność?

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ę!

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry