This is an old revision of the document!


Laboratorul 10: Genericitate și Tipuri Parametrizate

Obiective

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:

  • prezentarea structurilor generice simple.
  • conceptele de wildcard și bounded wildcards.
  • utilitatea genericității în design-ul unui sistem.

Aspectele bonus urmărite sunt:

  • compilarea cu warning-uri pentru raw types și dangerous casts
  • glosar pentru denumirea tipurilor generice.

Genericics

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:

  • colecțiile pierdeau informatia de tip
  • compilatorul nu mai știa ce tip returnează list.get()
  • se foloseau casturi manuale (periculoase și fragile)

Acest compromis ducea des la erori runtime.

  • Reamintim că erorile de tip (type errors) captate la runtime sunt mult mai costisitoare decât cele descoperite la compilare.
  • Generics mută 90% din probleme la compilare, acolo unde este locul lor.

Raw Types

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.

Costume Party Analogy:

  1. Toate obiectele intră în colecție „purtând aceeași mască” (Object).
  2. Odată masca pusă, compilatorul nu mai știe cine este cine.
  3. Programatorul trebuie să dea jos masca folosind casturi, care pot eșua.

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:

  • nu există garanția că lista conține obiecte compatibile între ele,
  • tipurile sunt verificate abia în runtime,
  • necesită conversii (cast-uri) deseori periculoase și greu de întreținut.

Compilerul îți spune că faci “unchecked operations”, dar nu te poate opri.

De ce nu putem corecta colecțiile fără generics?

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.

Varianta 1: Suprascrierea metodei add() cu un tip mai specific

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:

  • metoda originală add(Object) rămâne disponibilă,
  • lista ta tot va accepta orice tip.

Exemplu:

DateList list = new DateList();
list.add("Hello");  // Perfect valid → deoarece add(Object) există încă

  • Polimorfismul permite doar expansiunea tipurilor, nu îngustarea lor.
  • Vom vedea mai jos că generics au fost introduse tocmai pentru a putea “îngusta” contractele în mod sigur.

Varianta 2: Scriem o clasă nouă, complet separată

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ță:

  • nu poate fi folosită cu Collections.sort(), Collections.shuffle(), addAll(), subList()
  • nu poate fi trecută în metode care cer List
  • nu poate fi folosită în librăriile Java
  • nu poate fi combinată cu alte colecții
  • rupe total ecosistemul API

Tipuri parametrizate - baza genericității

Prin 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:

  1. o singură clasă poate fi folosită pentru orice tip,
  2. compilatorul verifică automat corectitudinea tipurilor,
  3. nu există casturi la extragere.

Definirea claselor generice

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.

Definirea metodelor generice

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:

  • nu dorim ca întreaga clasă să fie generică,
  • metoda trebuie să funcționeze cu mai multe tipuri,
  • avem logici generale (ex. transformări, validări, utilități),
  • vrem să evităm duplicarea codului.

De exemplu, putem avea o metodă generică care returnează același tip primit:

public static <T> T identity(T value) {
    return value;
}

Observați poziționarea lui <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.

Tipuri generice multiple

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.

Tipuri generice cu limitări

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:

  • garantează că putem apela metodele definite în Number,
  • previne utilizarea unor tipuri incompatibile (ex. 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ă.

Wildcards – flexibilitate în genericitate

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 (?).

Wildcard neîngrădit (?)

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.

Bounded wildcards – PECS (Producer Extends, Consumer Super)

Acesta este unul dintre cele mai importante concepte în genericitate.

Upper bounded wildcards

? 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
}

Lower bounded wildcards

? 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
}

Regula PECS pe scurt:

  • Producer Extends → folosește extends când lista doar oferă valori.
  • Consumer Super → folosește super când lista primește valori.

Type erasure – cum funcționează generics sub capotă

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:

  1. Nu putem instanția generic types direct:
    // T value = new T(); // invalid
  2. Nu putem crea array-uri generice:
    // T[] array = new T[10]; // invalid
  3. Nu putem folosi operatorul instanceof cu tipuri generice:
    // if (obj instanceof List<String>) { ... } // invalid
  4. Tipurile sunt transformate în Object sau limitele lor (extends).

Bridge Methods

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:

  • asigură suprascrierea corectă a metodelor generice,
  • păstrează compatibilitatea cu interfețele raw,
  • evită erori legate de polimorfism.

Type erasure oferă compatibilitate cu versiunile vechi ale Java, dar limitează unele scenarii care în alte limbaje (ex. C#) sunt posibile.

Puteți aprofunda diferența la Type Erasure dintre C# și Java în acest articol.

Din cauza mecanismului de type erasure, la runtime avem următoarea relație de adevăr:

new ArrayList<String>().getClass() ==
new ArrayList<Integer>().getClass()  // true

Practic, tipul fiecărui ArrayList este eliminat la runtime, deci intern ambele clase sunt identice.

Invarianță, Covarianță și Contravarianță

Java are trei moduri principale de a trata relațiile dintre tipurile generice.

Invarianță (comportamentul implicit)

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

Covarianță (arrays)

Array-urile sunt covariante:

Object[] arr = new String[10];
arr[0] = 42; // ArrayStoreException

Generics nu sunt covariante.

Contravarianță (? super T)

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.

Cast-uri în Generics

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

Acest lucru se întâmplă tocmai pentru că tipurile generice sunt invariante.

Arrays și Generics

Arrays sunt reified, adică își cunosc tipul la runtime. Generics sunt erased. Aceste două modele diferite sunt incompatibile.

Consecințe importante:

  • Nu putem crea array-uri de tip parametrizat:
    List<String>[] arr = new List<String>[10]; // Eroare
  • Putem crea doar array-uri de wildcard:
    List<?>[] arr = new List<?>[10]; // permis, dar riscant

Arrays verifică tipurile la runtime, în schimb generics nu o fac.

[Optional] Warning-uri de compilare

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.

IntelliJ va rula automat această comandă pentru voi.

[Nice to know] Glosar – Litere folosite în Generics

Literă Semnificație
T Type
E Element (în special în colecții)
K Key
V Value
R Result
? wildcard, tip necunoscut

Exerciții

  • Exercițiile vor fi făcute pe platforma Devmind Code. Găsiți exercițiile din acest laborator în contestul aferent.
  • Vă recomandăm să copiați scheletul și să faceți exercițiile mai întâi în IntelliJ, deoarece acolo aveți acces la o serie de instrumente specifice unui IDE. După ce ați terminat exercițiile puteți să le copiați pe Devmind Code.

Task 1 (6p)

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:

  1. (1 punct) 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.
  2. (1 puncte) void addAll(K key, List<V> values) - adaugă valorile din lista de valori dată ca parametru la lista asociată cheii.
  3. (1 puncte) void addAll(MultiMapValue<K, V> map) - adaugă intrările din obiectul MultiMapValue dat ca parametru în obiectul curent (this).
  4. (0.5 puncte) V getFirst(K key) - întoarce prima valoare asociată cheii (dacă nu există, se întoarce null).
  5. (0.5 puncte) List<V> getValues(K key) - se întoarce lista de valori asociată cheii.
  6. (0.5 puncte) boolean containsKey(K key) - se verifică faptul dacă este prezentă cheia în MultiMapValue.
  7. (0.5 puncte) boolean isEmpty() - se verifică dacă MultiMapValue este gol.
  8. (0.5 puncte) List<V> remove(K key) - se șterge cheia, împreună cu valorile asociate ei, din MultiMapValue.
  9. (0.5 puncte) int size() - se întoarce mărimea MultiMapValue.

Task 2 (4p)

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:

  1. (1 puncte) void addValue(T value) - adaugă o valoare în arborele binar de căutare.
  2. (0.5 puncte) void addAll(List<T> values) - adaugă valorile dintr-o listă în arborele binar de căutare.
  3. (1.5 puncte) HashSet<T> getValues(T inf, T sup) - colectează valorile din arbore între o limită inferioară și superioară într-o colecție de tipul HashSet.
  4. (0.5 puncte) int size() - se întoarce numărul de elemente inserate în arbore.
  5. (0.5 puncte) boolean isEmpty() - se întoarce dacă există vreun element inserat în arborele binar sau nu.

Resurse și link-uri utile

poo-ca-cd/laboratoare/genericitate-si-tipuri-parametrizate.1765176674.txt.gz · Last modified: 2025/12/08 08:51 by florian_luis.micu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0