Documentație: Tipuri Generice (Generics) în Java
Cuprins
Introducere
Tipurile generice (generics) reprezintă o facilitate a limbajului Java introdusă în versiunea 5.0 care permite crearea de clase, interfețe și metode parametrizate cu tipuri. Acestea oferă un mecanism pentru a defini structuri și algoritmi independenți de tip, păstrând în același timp siguranța tipurilor la compilare.
Genericele permit scrierea codului o singură dată, dar utilizarea acestuia cu diferite tipuri de date, fără a face conversii explicite (casting) și fără a pierde siguranța tipurilor.
Motivație și Avantaje
Înainte de introducerea genericelor, colecțiile din Java stocau elemente de tip Object
, ceea ce necesita conversii explicite la tipul dorit și permitea introducerea erorilor de tip în timpul rulării.
Avantajele genericelor:
Siguranța tipurilor la compilare: Erorile de tip sunt detectate în timpul compilării, nu la rulare.
Eliminarea conversiilor explicite (casting): Nu mai este necesar să facem conversii explicite când recuperăm elemente.
Posibilitatea implementării de algoritmi generici: Aceleași metode pot fi aplicate pe diferite tipuri de date.
Cod mai curat și mai ușor de întreținut: Codul este mai expresiv și mai puțin predispus la erori.
Exemplu fără generice (Java pre-5.0):
List lista = new ArrayList();
lista.add("Hello");
lista.add(123); // Legal, dar conceptual greșit dacă lista ar trebui să conțină doar șiruri
String s = (String) lista.get(0); // Conversie explicită necesară
String s2 = (String) lista.get(1); // Aruncă ClassCastException la rulare
Exemplu cu generice:
List<String> lista = new ArrayList<>();
lista.add("Hello");
// lista.add(123); // Eroare la compilare - tipul este verificat
String s = lista.get(0); // Nu este necesară conversie explicită
Sintaxa de Bază
Clase Generice
O clasă generică este definită cu unul sau mai mulți parametri de tip între paranteze unghiulare (<>
):
public class Container<T> {
private T element;
public Container(T element) {
this.element = element;
}
public T getElement() {
return element;
}
public void setElement(T element) {
this.element = element;
}
}
Utilizare:
Container<String> stringContainer = new Container<>("Hello");
String str = stringContainer.getElement(); // Nu este necesară conversie
Container<Integer> intContainer = new Container<>(42);
int value = intContainer.getElement(); // Auto-unboxing
Începând cu Java 7, putem folosi diamond operator (<>
) pentru a simplifica declarațiile:
Container<String> stringContainer = new Container<>("Hello");
Interfețe Generice
Similar cu clasele, interfețele pot fi și ele generice:
public interface Pair<K, V> {
K getKey();
V getValue();
void setKey(K key);
void setValue(V value);
}
public class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;
public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}
@Override
public K getKey() { return key; }
@Override
public V getValue() { return value; }
@Override
public void setKey(K key) { this.key = key; }
@Override
public void setValue(V value) { this.value = value; }
}
Metode Generice
Metodele pot fi și ele generice, chiar și când sunt definite în clase non-generice:
public class Utils {
// Metodă generică
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
// Metodă generică cu mai mulți parametri de tip
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
Utilizare:
Integer[] intArray = {1, 2, 3, 4, 5};
String[] stringArray = {"Hello", "World"};
Utils.printArray(intArray);
Utils.printArray(stringArray);
Parametri de Tip
Convenții de Denumire
În Java, există convenții standard pentru denumirea parametrilor de tip:
T
- Tip (Type)E
- Element (Element)K
- Cheie (Key)V
- Valoare (Value)N
- Număr (Number)S
- Al doilea tip/parametruU
,V
, etc. - Tipuri suplimentare
Aceste convenții nu sunt obligatorii, dar sunt larg utilizate pentru a face codul mai ușor de înțeles.
Parametri de Tip Multipli
Clasele și metodele generice pot avea mai mulți parametri de tip:
public class Mapping<K, V> {
private K key;
private V value;
public Mapping(K key, V value) {
this.key = key;
this.value = value;
}
// Getteri și setteri
}
// Utilizare
Mapping<String, Integer> mapare = new Mapping<>("vârstă", 30);
Wildcard Types (Tipuri Joker)
Wildcards (caracterul joker ?
) permit crearea de tipuri generice mai flexibile.
Unbounded Wildcards (?)
Reprezintă "orice tip" și se folosește când operațiile sunt independente de tipul specificat:
public static void printList(List<?> list) {
for (Object elem: list) {
System.out.println(elem);
}
}
Acest cod poate procesa orice fel de listă, indiferent de tipul elementelor.
Upper Bounded Wildcards (? extends T)
Limitează wildcards la un anumit tip sau orice subtip al acestuia:
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number n : list) {
sum += n.doubleValue();
}
return sum;
}
Acest cod poate procesa liste de Integer
, Double
, sau orice alt subtip al lui Number
.
Lower Bounded Wildcards (? super T)
Limitează wildcards la un anumit tip sau orice supertip al acestuia:
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
Acest cod poate adăuga Integer
la o listă de Integer
, Number
, sau Object
.
Principiul PECS (Producer Extends, Consumer Super)
O regulă importantă pentru utilizarea wildcards este principiul PECS:
Utilizează
? extends T
când colecția este un producer (citești din ea)Utilizează
? super T
când colecția este un consumer (scrii în ea)Utilizează tipuri exacte (
T
) când colecția este atât producer cât și consumer
// Producer - citim din sursă
public static <T> void copy(List<? extends T> sursă, List<? super T> destinație) {
for (T element : sursă) {
destinație.add(element); // Adaugă în destinație
}
}
Type Erasure
La compilare, compilatorul Java efectuează un proces numit "type erasure" (ștergere de tip), care:
Înlocuiește parametrii de tip generici cu limitele lor sau cu
Object
dacă nu există limiteInserează conversii (casting) unde e necesar
Generează metode bridge pentru a păstra polimorfismul în moștenire
Acest proces există pentru a asigura compatibilitatea cu codul pre-generic și pentru a implementa generics fără a modifica Java Virtual Machine (JVM).
Exemplu:
// Cod scris
public class Container<T> {
private T element;
public void setElement(T element) { this.element = element; }
public T getElement() { return element; }
}
// După type erasure
public class Container {
private Object element;
public void setElement(Object element) { this.element = element; }
public Object getElement() { return element; }
}
Acest proces explică unele limitări ale genericelor în Java.
Restricții și Limitări
Genericele în Java au unele restricții importante:
Nu se pot crea instanțe ale parametrilor de tip:
public class Container<T> { private T element; public Container() { // element = new T(); // Eroare de compilare } }
Nu se pot crea array-uri de tipuri generice parametrizate:
// Eroare de compilare List<Integer>[] arrayOfLists = new List<Integer>[10];
Nu se pot folosi primitivele ca parametri de tip:
// Eroare de compilare Container<int> container = new Container<>(5); // Corect - folosim wrapper class Container<Integer> container = new Container<>(5);
Nu se pot folosi operatorii instanceof cu tipuri generice:
public static <T> boolean isTypeMatch(Object obj, List<T> list) { // Eroare de compilare // return obj instanceof T; // sau // return obj instanceof List<T>; }
Nu se pot crea, captura sau arunca obiecte de tip generic:
// Eroare de compilare // public class MathException<T> extends Exception { ... } // Incorect // public static <T extends Exception> void doWork() throws T { ... }
Aceste limitări vin din cauza implementării prin type erasure.
Exemple Practice
Container Generic
Un container simplu pentru orice tip de obiect:
public class Box<T> {
private T content;
public Box() { }
public Box(T content) {
this.content = content;
}
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
public boolean hasContent() {
return content != null;
}
@Override
public String toString() {
if (content == null) {
return "Empty Box";
}
return "Box containing " + content.toString();
}
}
Metode Utilitare Generice
Metode utilitare pentru manipularea colecțiilor generice:
public class CollectionUtils {
// Găsește elementul maxim dintr-o colecție
public static <T extends Comparable<T>> T findMax(Collection<T> collection) {
if (collection == null || collection.isEmpty()) {
return null;
}
Iterator<T> iterator = collection.iterator();
T max = iterator.next();
while (iterator.hasNext()) {
T current = iterator.next();
if (current.compareTo(max) > 0) {
max = current;
}
}
return max;
}
// Convertește o colecție într-o altă colecție folosind un transformator
public static <T, R> List<R> transform(Collection<T> collection, Function<T, R> transformer) {
List<R> result = new ArrayList<>(collection.size());
for (T element : collection) {
result.add(transformer.apply(element));
}
return result;
}
// Filtrează o colecție folosind un predicat
public static <T> List<T> filter(Collection<T> collection, Predicate<T> predicate) {
List<T> result = new ArrayList<>();
for (T element : collection) {
if (predicate.test(element)) {
result.add(element);
}
}
return result;
}
}
Perechi și Tuple
Implementarea generică a unei perechi și a unui tuplu:
// Pereche generică (key-value)
public class Pair<K, V> {
private final K key;
private final V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Pair<?, ?> pair = (Pair<?, ?>) o;
return Objects.equals(key, pair.key) &&
Objects.equals(value, pair.value);
}
@Override
public int hashCode() {
return Objects.hash(key, value);
}
@Override
public String toString() {
return "(" + key + ", " + value + ")";
}
// Factory method
public static <K, V> Pair<K, V> of(K key, V value) {
return new Pair<>(key, value);
}
}
// Triplet generic
public class Triplet<T, U, V> {
private final T first;
private final U second;
private final V third;
public Triplet(T first, U second, V third) {
this.first = first;
this.second = second;
this.third = third;
}
public T getFirst() { return first; }
public U getSecond() { return second; }
public V getThird() { return third; }
// Metodele equals, hashCode, toString și factory method similar cu Pair
}
Design Patterns cu Generics
Genericele sunt folosite frecvent în design patterns pentru a crea soluții mai flexibile și reutilizabile:
Builder Pattern Generic
public class GenericBuilder<T> {
private final Supplier<T> instantiator;
private final List<Consumer<T>> modifiers = new ArrayList<>();
public GenericBuilder(Supplier<T> instantiator) {
this.instantiator = instantiator;
}
public <V> GenericBuilder<T> with(BiConsumer<T, V> consumer, V value) {
Consumer<T> modifier = instance -> consumer.accept(instance, value);
modifiers.add(modifier);
return this;
}
public T build() {
T instance = instantiator.get();
modifiers.forEach(modifier -> modifier.accept(instance));
return instance;
}
public static <T> GenericBuilder<T> of(Supplier<T> instantiator) {
return new GenericBuilder<>(instantiator);
}
}
// Utilizare
Person person = GenericBuilder.of(Person::new)
.with(Person::setName, "John")
.with(Person::setAge, 30)
.with(Person::setEmail, "john@example.com")
.build();
Repository Pattern Generic
public interface GenericRepository<T, ID> {
Optional<T> findById(ID id);
List<T> findAll();
T save(T entity);
void delete(T entity);
void deleteById(ID id);
long count();
boolean exists(ID id);
}
public abstract class JpaGenericRepository<T, ID> implements GenericRepository<T, ID> {
protected final Class<T> entityClass;
protected final EntityManager entityManager;
public JpaGenericRepository(Class<T> entityClass, EntityManager entityManager) {
this.entityClass = entityClass;
this.entityManager = entityManager;
}
@Override
public Optional<T> findById(ID id) {
return Optional.ofNullable(entityManager.find(entityClass, id));
}
@Override
public List<T> findAll() {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<T> cq = cb.createQuery(entityClass);
Root<T> rootEntry = cq.from(entityClass);
CriteriaQuery<T> all = cq.select(rootEntry);
return entityManager.createQuery(all).getResultList();
}
// Implementarea celorlalte metode
}
Bune Practici
Câteva bune practici pentru utilizarea genericelor în Java:
Prefă parametrizarea fara limitarea:
// Mai bine public <T extends Comparable<T>> T findMax(List<T> list) // Decât public Comparable findMax(List<? extends Comparable> list)
Utilizează paramteri de tip pentru expresivitate:
// Mai expresiv public <K, V> V getValue(Map<K, V> map, K key) // Decât public Object getValue(Map map, Object key)
Ține cont de principiul PECS:
? extends T
pentru producători (read)? super T
pentru consumatori (write)
Evită utilizarea raw types (tipuri brute):
// Evită List lista = new ArrayList(); // Preferă List<Object> lista = new ArrayList<>();
Nu crea array-uri de tipuri generice:
// Evită List<String>[] arrayOfLists = new List<String>[10]; // Nu compilează // Preferă List<List<String>> listOfLists = new ArrayList<>();
Limitează scopul parametrilor de tip:
// Mai bine public <T extends Comparable<T>> int compareTo(T o1, T o2) // Decât public <T> int compareTo(T o1, T o2) // Eroare dacă T nu implementează Comparable
Folosește factory methods pentru a simplifica crearea de obiecte generice:
public static <K, V> Pair<K, V> of(K key, V value) { return new Pair<>(key, value); } // Utilizare Pair<String, Integer> pair = Pair.of("Cheie", 42);
Preferă interfețe generice pentru API-uri publice:
public interface Repository<T, ID> { T findById(ID id); List<T> findAll(); // ... }
Documentează parametrii de tip în Javadoc:
/** * Găsește elementul maxim dintr-o colecție. * * @param <T> tipul elementelor din colecție * @param collection colecția de elemente * @return elementul maxim sau null dacă colecția este goală */ public static <T extends Comparable<T>> T findMax(Collection<T> collection) { // ... }
Last updated