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
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!
Z Generics ten sam kod wygląda tak:
// Nowoczesny kod z Generics (Java 1.5+) ArrayListemployeeNames = 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
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 Listnumbers = 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
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
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 staticvoid 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
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 extends Number> 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 super Integer> numbers) { numbers.add(10); numbers.add(20); } // Przykłady użycia Liststrings = 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
Wildcard | Składnia | Kiedy używać |
---|---|---|
Unbounded | List<?> | Gdy nie zależy Ci na typie, tylko na operacjach kolekcji |
Upper bounded | List<? extends T> | Gdy chcesz czytać elementy jako typ T lub jego nadklasa |
Lower bounded | List<? super T> | Gdy chcesz dodawać elementy typu T |
Najczęstsze błędy przy używaniu Generics
// ŹLEDBLĘDNE podejście - mieszanie raw types z Generics Liststrings = 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
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
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[].
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.
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.
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.
Nie bezpośrednio. Musisz używać wrapper classes: Integer zamiast int, Double zamiast double itd. Java automatycznie konwertuje między nimi (autoboxing/unboxing).
Ze względu na type erasure nie możesz sprawdzić parametru typu w runtime. Możesz przekazać Class<T> jako parametr konstruktora lub metody.
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:
- Oracle Java Generics Tutorial
- Java 8 Collections API Documentation
- Java Generics FAQ by Angelika Langer
🚀 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!