Scopul acestui laborator este prezentarea conceptului de genericitate și modalitățile de creare și folosire a claselor, metodelor și interfețelor generice în Java.
Aspectele urmărite sunt:
Aspectele bonus urmărite sunt:
Genericitatea este unul dintre cele mai elegante mecanisme introduse în Java pentru a ajuta la scrierea unui cod general, sigur și reutilizabil.
Înainte de Java 5, colecțiile acceptau orice tip de obiect:
List list = new ArrayList(); list.add(new Date()); list.add("Hello");
Din cauza acestui lucru:
list.get()Acest compromis ducea des la erori runtime.
Java este orientată pe obiecte, ceea ce înseamnă că polimorfismul permite referirea obiectelor ca tipuri generice (toate obiectele moștenesc Object). Chiar și după Java 5, este posibil să folosim o colecție fără a specifica un parametru de tip. Acest stil se numește raw type.
Dar o colecție de Object este, filosofic vorbind, atât o colecție de orice, cât și o colecție de nimic.
Object).
Considerăm un ArrayList fără genericitate:
ArrayList list = new ArrayList(); list.add("Hello"); list.add(10); // logic problematic, DAR poate fi adăugat String text = (String) list.get(0); // OK String another = (String) list.get(1); // ClassCastException runtime
Acest stil prezintă mai multe probleme:
Problema fundamentală nu este implementarea colecțiilor, ci contractul lor public.
Colecțiile înainte de generics acceptau orice obiect prin metoda:
public boolean add(Object o)
Să încercăm să rezolvăm această problemă fără generics.
Mulți începători gândesc instinctiv:
“Dacă vreau o listă doar de Date, suprascriu add() ca să accepte doar Date.”
Exemplu:
class DateList extends ArrayList { public boolean add(Date d) { ... } // încercare de override }
Problema: Acest lucru nu este overriding, ci overloading.
Metoda originală din ArrayList este:
public boolean add(Object o)
Semnătura nu coincide, deci metoda nu este suprascrisă. Rezultatul:
add(Object) rămâne disponibilă,Exemplu:
DateList list = new DateList(); list.add("Hello"); // Perfect valid → deoarece add(Object) există încă
Un alt instinct este: “Ok, nu pot modifica ArrayList, atunci scriu eu o clasă List doar pentru Date.”
Exemplu:
class DateList { private ArrayList inner = new ArrayList(); public void add(Date d) { inner.add(d); } public Date get(int i) { return (Date) inner.get(i); } }
Practic, am delegat aceste operații unei clase specializate. Inițial pare că funcționează, deoarece lista acceptă doar Date.
Problema: Această clasă nu mai este un List real, pentru că nu implementează List, drept consecință:
Collections.sort(), Collections.shuffle(), addAll(), subList()ListPrin introducerea type parameters, colecțiile devin sigure:
ArrayList<String> words = new ArrayList<>(); words.add("Hello"); // words.add(10); // Eroare de compilare String word = words.get(0); // Fără cast
Compilatorul garantează că doar obiecte de tipul String pot fi adăugate. Aceasta demonstrează că generics mută erorile de tip din runtime în compile time, un concept fundamental în design-ul limbajelor moderne.
Totodată, mai avem următoarele avantaje:
Putem crea propriile clase generice folosind operatorul diamant (<>) la definirea clasei:
// Clasa Box va lucra cu obiecte de tip "T" public class Box<T> { private T value; // folosim tipul "T" ca și câmp // Folosim tipul "T" ca parametru public void set(T value) { this.value = value; } // Folosim tipul "T" ca valoare de return public T get() { return value; } }
Utilizare:
// Tipul "T" va fi înlocuit cu "Integer" peste tot în clasă Box<Integer> intBox = new Box<>(); intBox.set(10); Box<String> stringBox = new Box<>(); stringBox.set("Hello");
Observăm cum o singură clasă devine reutilizabilă pentru mai multe tipuri, eliminând redundanța și crescând flexibilitatea designului.
Pe lângă clase generice, Java permite definirea metodelor generice, adică metode care introduc propriii parametri de tip independent de parametrii de tip ai clasei.
Această funcționalitate este extrem de utilă atunci când:
De exemplu, putem avea o metodă generică care returnează același tip primit:
public static <T> T identity(T value) { return value; }
<T> în semnătura metodei. Prin definirea lui în acel loc marcăm metoda ca fiind generică.
Utilizare:
String s = identity("Hello"); Integer n = identity(42); Date d = identity(new Date());
Compilatorul deduce automat tipul T în fiecare apel.
O clasă poate avea mai mulți parametri:
public class Pair<K, V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } }
Aceasta este baza map-urilor, dicționarelor și tuplurilor.
Interfețele pot fi la rândul lor generice:
public interface Transformer<T, R> { R apply(T input); }
Generics pot participa la lanțuri de moștenire exact ca orice alte clase, dar cu câteva detalii importante.
class MyList<T> extends ArrayList<T> { // Moștenește întreaga funcționalitate și rămâne generică }
Clasa derivată continuă să fie generică și poate funcționa exact ca tipul original.
class IntegerList extends ArrayList<Integer> { }
Clasa nu mai este generică. Acceptă doar valori de tip Integer.
Acest lucru este util pentru a crea alias-uri semantice:
class UserIdList extends ArrayList<UserId> { }
class Bad<T> extends Box<String> { }
Parametrul generic T există, dar nu mai este folosit. Asta înseamnă că următorul cod compilează:
Bad<Integer> b = new Bad<>();
Deși clasa funcționează doar cu String intern. Aceasta creează confuzie și design ineficient.
class SortedBox<T extends Comparable<T>> extends Box<T> { }
Clasa poate fi instanțiată doar cu tipuri care sunt comparabile între ele.
Acest tip de moștenire apare des în structuri de date (liste ordonate, heap-uri, arbori etc.).
extends în cadrul generics.
Există situații în care dorim ca un parametru generic să fie restricționat la un anumit tip (sau subclase ale acestuia).
public class NumberBox<T extends Number> { private T value; }
Această constrângere oferă două avantaje:
Number,String).
extends în cadrul generics.
De asemenea, Java permite bounded type parameters multiple:
public <T extends Number & Comparable<T>> void process(T value) { ... }
Această combinație este utilă în algoritmi de sortare sau agregare numerică.
Un alt exemplu destul de des folosit cu limitări în tipuri generice este acesta:
public static <T extends Comparable<T>> T max(T a, T b) { return a.compareTo(b) > 0 ? a : b; }
Practic, am făcut o metodă care întoarce maximul dintre două obiecte, doar dacă acel obiect implementează interfața Comparable pentru a putea folosi metoda compareTo.
Există momente în care nu cunoaștem exact tipul colecției, dar dorim totuși să operăm asupra ei. Pentru aceste cazuri folosim wildcard-uri (?).
Folosim ? când tipul nu este relevant, ci doar existența lui:
public void printList(List<?> list) { for (Object o : list) System.out.println(o); }
Poate primi orice listă: List<String>, List<Integer>, etc.
Acesta este unul dintre cele mai importante concepte în genericitate.
? extends T – valorile permise includ tipul “T” și copiii lui.
Se mai poate spune și că lista produce valori de tip “T”. Se folosește când vrem să citim obiecte:
public void processNumbers(List<? extends Number> list) { Number n = list.get(0); // OK // list.add(10); // nerecomandat, nu știm exact subtipul }
? super T – valorile permise includ tipul “T” și părinții lui.
Se mai poate spune și că lista consumă valori de tip “T”. Se folosește când vrem să adăugăm obiecte:
public void addNumbers(List<? super Integer> list) { list.add(10); // OK // Integer x = list.get(0); // invalid, nu știm tipul exact }
extends când lista doar oferă valori.super când lista primește valori.
extends, acesta se poate referi atât la clase cât și la interfețe (vezi exemplul de mai sus despre Comparable).
Wildcard-urile (?) sunt utile pentru flexibilitate, dar nu pot fi folosite ca tipuri reale în operații care necesită consistență de tip.
De exemplu, următoarea metodă nu compilează:
void resetFirst(List<?> list) { list.set(0, list.get(0)); // Eroare }
Motivul:
list.get(0) întoarce un Objectlist.set necesită un tip exact, pe care ? nu îl reprezintăWildcard-ul înseamnă “nu cunosc tipul”, deci nu poate fi folosit ca tip concret.
Pentru a rezolva această problemă, folosim Wildcard Capture. Definim o metodă generică auxiliară care introduce un parametru de tip real:
<T> void resetHelper(List<T> list) { list.set(0, list.get(0)); // Aici merge } void resetFirst(List<?> list) { resetHelper(list); // Compilatorul captează wildcard-ul ca tip T }
Ce se întâmplă aici?
<?> este “capturat” și tratat ca un tip T concretget() și set()
Metoda Collections.swap folosește exact acest mecanism:
static void swap(List<?> list, int i, int j) { swapHelper(list, i, j); } private static <T> void swapHelper(List<T> list, int i, int j) { T tmp = list.get(i); list.set(i, list.get(j)); list.set(j, tmp); }
List<?>).Java implementează genericitatea printr-un mecanism numit type erasure, adică tipurile generice există doar în timpul compilării, fiind eliminate în codul de bytecode.
Consecințe:
// T value = new T(); // invalid
// T[] array = new T[10]; // invalid
instanceof cu tipuri generice:// if (obj instanceof List<String>) { ... } // invalid
Generics funcționează prin type erasure, ceea ce înseamnă că semnătura metodelor generice dispare în bytecode. Pentru a păstra compatibilitatea cu codul pre-generic, compilatorul creează automat metode suplimentare numite bridge methods.
Pentru codul:
class MyList implements List<String> { public String get(int index) { ... } }
Compilatorul generează automat:
public Object get(int index) { return get(index); }
Aceste metode:
Type erasure oferă compatibilitate cu versiunile vechi ale Java, dar limitează unele scenarii care în alte limbaje (ex. C#) sunt posibile.
new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass() // true
Practic, tipul fiecărui ArrayList este eliminat la runtime, deci intern ambele clase sunt identice.
Java are trei moduri principale de a trata relațiile dintre tipurile generice.
Deși moștenirea pare că am putea face asta, List<String> nu este un List<Object>.
List<Object> list = new ArrayList<String>(); // Eroare
Altfel, am putea avea:
List<Object> x = new ArrayList<String>(); x.add(10); // risc major
Array-urile sunt covariante:
Object[] arr = new String[10]; arr[0] = 42; // ArrayStoreException
Generics nu sunt covariante.
Pentru a defini un scenariu de moștenire ca în exemplul despre invarianță, putem folosi bounded wildcards, astfel listelor li se poate permite să accepte tipuri părinte.
List<? super Integer> list;
Aceasta permite scrierea în colecție (add(Integer)), dar nu permite citirea unui tip specific.
În mod natural, ai putea crede că dacă String este un subtip al lui Object, atunci și List<String> ar trebui să fie un subtip al lui List<Object>. Dar nu este așa.
Exemplu:
List<String> stringList = new ArrayList<>(); // Deși pare logic, nu este permis: List<Object> objects = stringList; // Eroare de compilare!
De ce? Dacă ar fi fost permis, atunci:
objects.add(new Object()); // ar fi permis... String s = stringList.get(0); // ...dar aici am obține un Object, nu un String!
Acesta este motivul pentru care generics în Java sunt invariante.
Din cauza type erasure, anumite casturi sunt permise, iar altele nu.
Permis:
List raw = new ArrayList(); List<String> s = (List<String>) raw; // unchecked, dar permis
Interzis:
List<Date> d = new ArrayList<>(); List<Object> o = (List<Object>) d; // eroare de compilare
Arrays sunt reified, adică își cunosc tipul la runtime. Generics sunt erased. Aceste două modele diferite sunt incompatibile.
Consecințe importante:
List<String>[] arr = new List<String>[10]; // Eroare
List<?>[] arr = new List<?>[10]; // permis, dar riscant
Pentru a vedea exact unde apar erorile de tip puteți rula următoarea comandă:
javac -Xlint:unchecked Program.java
Aceasta va indica toate locurile în care se folosesc raw types sau casturi riscante.
| Literă | Semnificație |
|---|---|
| T | Type |
| E | Element (în special în colecții) |
| K | Key |
| V | Value |
| R | Result |
| ? | wildcard, tip necunoscut |
Implementați o structură de date de tipul MultiMapValue<K, V>, pe baza scheletului, care reprezintă un HashMap<K, ArrayList<V», unde o cheie este asociată cu mai multe valori. Modalitatea de stocare a datelor este la alegere (moștenire sau agregare) și să folosiți funcționalitățile din HashMap. În schelet aveți următoarele metode de implementat:
add(K key, V value) - adaugă o valoare la o cheie dată (valoarea este adăugate în lista de valori asociate cheii, dacă cheia și lista nu există, atunci lista va fi creată și asociată cheii.void addAll(K key, List<V> values) - adaugă valorile din lista de valori dată ca parametru la lista asociată cheii.void addAll(MultiMapValue<K, V> map) - adaugă intrările din obiectul MultiMapValue dat ca parametru în obiectul curent (this).V getFirst(K key) - întoarce prima valoare asociată cheii (dacă nu există, se întoarce null).List<V> getValues(K key) - se întoarce lista de valori asociată cheii.boolean containsKey(K key) - se verifică faptul dacă este prezentă cheia în MultiMapValue.boolean isEmpty() - se verifică dacă MultiMapValue este gol.List<V> remove(K key) - se șterge cheia, împreună cu valorile asociate ei, din MultiMapValue.int size() - se întoarce mărimea MultiMapValue.Implementați o structură de date de tipul Tree<T> (Arbore binar de căutare) pe baza scheletului. Analizați modalitatea de utilizare a bounded wildcards, explicați necesitatea lor laborantului (fie în cadrul orei de laborator, fie la nivel de comentariu în cod). În schelet aveți următoarele metode de implementat:
void addValue(T value) - adaugă o valoare în arborele binar de căutare.void addAll(List<T> values) - adaugă valorile dintr-o listă în arborele binar de căutare.HashSet<T> getValues(T inf, T sup) - colectează valorile din arbore între o limită inferioară și superioară într-o colecție de tipul HashSet.int size() - se întoarce numărul de elemente inserate în arbore.boolean isEmpty() - se întoarce dacă există vreun element inserat în arborele binar sau nu.