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

TL;DR: Generics w Javie to sposób na pisanie bezpiecznego kodu który działa z różnymi typami danych. Zamiast pisać osobne klasy dla List i List, piszesz jedną klasę List która obsługuje wszystkie typy. Główne korzyści: bezpieczeństwo typów w czasie kompilacji, eliminacja cast-owania i czytelniejszy kod.

Dlaczego Generics są ważne?

Generics zostały wprowadzone w Java 1.5 i od razu stały się jedną z najważniejszych funkcji języka. Przed ich wprowadzeniem programiści musieli używać raw types i cast-ować obiekty, co prowadziło do błędów w runtime. Generics rozwiązują ten problem na poziomie kompilacji.

Co się nauczysz:

  • Czym są Generics i dlaczego zostały wprowadzone do Java
  • Jak używać Generics w Collections (List, Map, Set)
  • Jak tworzyć własne klasy i metody generyczne
  • Jakie są wildcards i kiedy ich używać
  • Najczęstsze błędy przy używaniu Generics
Wymagania wstępne: Znajomość podstaw Java (klasy, obiekty, Collections), umiejętność pracy z IDE, podstawowa znajomość ArrayList i HashMap.

Problem który rozwiązują Generics

Wyobraź sobie że musisz przechowywać listę nazwisk pracowników. Przed Java 1.5 robiłeś to tak:

// Tak wyglądał kod przed Generics (Java 1.4 i wcześniej)
ArrayList employeeNames = new ArrayList();
employeeNames.add("Jan Kowalski");
employeeNames.add("Anna Nowak");

// Problem: możesz przypadkowo dodać nieprawidłowy typ
employeeNames.add(42); // Kompilator tego nie wyłapie!

// Musisz cast-ować przy pobieraniu
String firstName = (String) employeeNames.get(0);
String secondName = (String) employeeNames.get(2); // Runtime error jeśli to Integer!

Uwaga: Kod bez Generics kompiluje się bez problemów, ale może wywolić ClassCastException w runtime – co jest jednym z najgorszych rodzajów błędów do debugowania.

Z Generics ten sam kod wygląda tak:

// Nowoczesny kod z Generics (Java 1.5+)
ArrayList employeeNames = new ArrayList();
employeeNames.add("Jan Kowalski");
employeeNames.add("Anna Nowak");

// Kompilator nie pozwoli dodać nieprawidłowego typu
// employeeNames.add(42); // Błąd kompilacji!

// Nie musisz cast-ować - typ jest znany
String firstName = employeeNames.get(0); // Bezpieczne i czytelne
Generics to jak etykiety na pudełkach. Zamiast mieć jedno wielkie pudełko „rzeczy” gdzie trzeba sprawdzać co się w nim znajduje, masz pudełko z etykietą „String” gdzie wiesz że są tylko teksty.

Podstawowe użycie Generics w Collections

Najczęściej spotkasz się z Generics w kolekcjach. Oto najważniejsze przykłady:

// ArrayList przechowujący liczby całkowite
List numbers = new ArrayList();
numbers.add(10);
numbers.add(20);
Integer sum = numbers.get(0) + numbers.get(1); // Bezpieczne

// HashMap mapujący ID użytkownika na nazwisko
Map userNames = new HashMap();
userNames.put(1L, "Jan Kowalski");
userNames.put(2L, "Anna Nowak");
String userName = userNames.get(1L); // Zwraca String, nie Object

// Set unikalnych adresów email
Set emails = new HashSet();
emails.add("jan@example.com");
emails.add("anna@example.com");
boolean hasEmail = emails.contains("jan@example.com"); // true
Od Java 7 możesz używać diamond operator <> po prawej stronie: List names = new ArrayList<>(); – kompilator automatycznie wywnioskuje typ.

Tworzenie własnych klas generycznych

Możesz tworzyć własne klasy które używają Generics. Oto prosty przykład klasy przechowującej parę wartości:

// Klasa generyczna przechowująca parę wartości
public class Pair {
    private T first;
    private U second;
    
    public Pair(T first, U second) {
        this.first = first;
        this.second = second;
    }
    
    public T getFirst() {
        return first;
    }
    
    public U getSecond() {
        return second;
    }
    
    @Override
    public String toString() {
        return "Pair{" + first + ", " + second + "}";
    }
}

// Użycie klasy generycznej
Pair userAge = new Pair<>("Jan Kowalski", 30);
String name = userAge.getFirst(); // String
Integer age = userAge.getSecond(); // Integer

Pair measurement = new Pair<>(25.5, true);
Double value = measurement.getFirst(); // Double
Boolean isValid = measurement.getSecond(); // Boolean
Type Parameter – to litera (zwykle T, U, V) która reprezentuje nieznany typ w definicji klasy lub metody generycznej. Przy tworzeniu instancji podajesz konkretny typ.

Metody generyczne

Możesz również tworzyć pojedyncze metody generyczne, nawet w klasach które nie są generyczne:

public class GenericMethods {
    
    // Metoda generyczna zamieniająca elementy w tablicy
    public static  void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    
    // Metoda generyczna znajdująca maksymalny element
    public static > T findMax(List list) {
        if (list.isEmpty()) {
            return null;
        }
        
        T max = list.get(0);
        for (T item : list) {
            if (item.compareTo(max) > 0) {
                max = item;
            }
        }
        return max;
    }
}

// Użycie metod generycznych
String[] names = {"Anna", "Jan", "Piotr"};
GenericMethods.swap(names, 0, 2); // Zamienia Anna z Piotr

List numbers = Arrays.asList(5, 2, 8, 1, 9);
Integer maxNumber = GenericMethods.findMax(numbers); // Zwraca 9
Pro tip: Metody generyczne są szczególnie przydatne w utility classes gdzie chcesz napisać jedną metodę działającą na różnych typach zamiast przeciążać ją dla każdego typu osobno.

Wildcards – elastyczność w Generics

Czasami potrzebujesz więcej elastyczności niż daje konkretny typ. Wtedy używasz wildcards:

// Unbounded wildcard - akceptuje List dowolnego typu
public static void printListSize(List list) {
    System.out.println("Lista ma " + list.size() + " elementów");
}

// Upper bounded wildcard - akceptuje Number i jego podklasy
public static double sumNumbers(List numbers) {
    double sum = 0;
    for (Number num : numbers) {
        sum += num.doubleValue();
    }
    return sum;
}

// Lower bounded wildcard - może dodawać Integer i jego nadklasy
public static void addNumbers(List numbers) {
    numbers.add(10);
    numbers.add(20);
}

// Przykłady użycia
List strings = Arrays.asList("A", "B", "C");
printListSize(strings); // Działa z List

List integers = Arrays.asList(1, 2, 3, 4, 5);
double sum = sumNumbers(integers); // Działa z List

List numbers = new ArrayList<>();
addNumbers(numbers); // Może dodawać Integer do List

WildcardSkładniaKiedy używać
UnboundedList<?>Gdy nie zależy Ci na typie, tylko na operacjach kolekcji
Upper boundedList<? extends T>Gdy chcesz czytać elementy jako typ T lub jego nadklasa
Lower boundedList<? super T>Gdy chcesz dodawać elementy typu T

Najczęstsze błędy przy używaniu Generics

Błąd początkujących: Mieszanie raw types z Generics w tym samym kodzie. Kompilator ostrzega, ale kod się kompiluje – lepiej zawsze używać Generics konsekwentnie.
// ŹLEDBLĘDNE podejście - mieszanie raw types z Generics
List strings = new ArrayList();
List rawList = strings; // Raw type - compiler warning
rawList.add(42); // Dodaje Integer do List!

// POPRAWNE podejście - konsekwentne używanie Generics
List strings = new ArrayList<>();
List anotherStringList = strings; // Type safe
Pułapka: Generics istnieją tylko w czasie kompilacji (type erasure). W runtime List<String> i List<Integer> to ta sama klasa List.

Praktyczne zastosowania w codziennym kodzie

Oto kilka praktycznych przykładów jak używać Generics w rzeczywistych projektach:

// Repository pattern z Generics
public interface Repository {
    T findById(ID id);
    List findAll();
    T save(T entity);
    void deleteById(ID id);
}

// Implementacja dla konkretnej encji
public class UserRepository implements Repository {
    private Map users = new HashMap<>();
    
    @Override
    public User findById(Long id) {
        return users.get(id);
    }
    
    @Override
    public List findAll() {
        return new ArrayList<>(users.values());
    }
    
    @Override
    public User save(User user) {
        users.put(user.getId(), user);
        return user;
    }
    
    @Override
    public void deleteById(Long id) {
        users.remove(id);
    }
}

// Użycie
Repository userRepo = new UserRepository();
User user = new User(1L, "Jan Kowalski");
userRepo.save(user);
User foundUser = userRepo.findById(1L); // Type safe, no casting needed

Czy mogę tworzyć tablice Generics jak new T[10]?

Nie, ze względu na type erasure nie możesz tworzyć tablic parametrów typu. Zamiast tego użyj Collections lub stwórz tablicę Object[] i rzutuj ją na T[].

Dlaczego List<Integer> nie może być przypisana do List<Number>?

To nie jest bezpieczne. Gdyby było możliwe, mógłbyś dodać Double do List<Integer> przez referencję List<Number>. Generics są invariant – List<A> nie jest podtypem List<B> nawet jeśli A jest podtypem B.

Kiedy używać wildcards a kiedy konkretnych typów?

Używaj wildcards gdy chcesz elastyczności – metoda ma działać z różnymi typami. Używaj konkretnych typów gdy potrzebujesz dokładnie określonego typu w całej metodzie lub klasie.

Co to jest type erasure i dlaczego istnieje?

Type erasure oznacza że informacje o typach generycznych są usuwane w czasie kompilacji. Istnieje dla zachowania kompatybilności z kodem Java sprzed wersji 1.5 który nie używał Generics.

Czy mogę używać prymitywów w Generics?

Nie bezpośrednio. Musisz używać wrapper classes: Integer zamiast int, Double zamiast double itd. Java automatycznie konwertuje między nimi (autoboxing/unboxing).

Jak sprawdzić typ w runtime gdy używam Generics?

Ze względu na type erasure nie możesz sprawdzić parametru typu w runtime. Możesz przekazać Class<T> jako parametr konstruktora lub metody.

Czy Generics wpływają na wydajność?

Minimalne wpływ. Type erasure oznacza że w runtime kod działa tak samo jak bez Generics, ale unikasz cast-owania co może być nieznacznie szybsze.

Przydatne zasoby:

🚀 Zadanie dla Ciebie

Stwórz generyczną klasę Cache<K, V> która:

  • Przechowuje pary klucz-wartość w HashMap
  • Ma metodę put(K key, V value) do dodawania
  • Ma metodę get(K key) zwracającą Optional<V>
  • Ma metodę size() zwracającą liczbę elementów

Przetestuj swoją implementację z różnymi typami: Cache<String, User> i Cache<Long, String>. Sprawdź czy kompilator właściwie sprawdza typy!

Czy używasz już Generics w swoich projektach? Jakie napotkałeś wyzwania przy ich implementacji? Podziel się swoimi doświadczeniami w komentarzach!

Zostaw komentarz

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

Przewijanie do góry