This is an old revision of the document!
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.
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).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ă.
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.
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.
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.