Table of Contents

Breviar 7

Colecții, iteratori, genericitate

1. Colecții

1.1 Interfața Collection și ierarhia colecțiilor

O colecție este un obiect care grupează mai multe elemente într-o singură unitate. Prin intermediul colecțiilor avem acces la diferite structuri de date: vectori dinamici, liste înlănțuite, stive, mulțimi, tabele de dispersie ș.a.m.d.. Colecțiile sunt folosite atât pentru memorarea și manipularea datelor, cât și pentru transmiterea informațiilor între metode.

Clasele și interfețele pentru lucrul cu colecții se află în pachetul java.util. Ierarhia pornește, pentru colecții propriu-zise, de la interfața Collection, care definește operațiile de bază (adăugare, eliminare, căutare, iterare). Din Collection derivă trei ramuri principale:

Separat de Collection se află ierarhia Map, care gestionează perechi cheie - valoare. Cheile sunt unice, iar fiecare cheie mapează exact o valoare. Implementările cele mai folosite sunt HashMap (rapid, fără ordine), LinkedHashMap (menține ordinea inserării sau a accesului - util, de exemplu, pentru cache LRU - Least Recently Used) și TreeMap (chei ordonate). Interfața SortedMap extinde Map cu operații specifice ordinii - în practică, TreeMap este implementarea reprezentativă. Hashtable este o variantă veche, sincronizată, păstrată pentru compatibilitate, dar în cod modern se preferă HashMap (sau ConcurrentHashMap pentru acces concurent).

În Java modern, colecțiile sunt parametrizate: după numele colecției se declară, între <…>, tipul elementelor. De exemplu, List<String> înseamnă o listă care conține doar șiruri de caractere.

Dacă tipul este declarat astfel, compilatorul verifică la compilare să nu introducem alt tip (de ex. un Integer într-o List<String>). Orice încercare greșită este o eroare de compilare, deci problema se oprește înainte de rulare.

Colecțiile eterogene (elemente de orice fel) apar doar dacă:

  • folosim intenționat un tip general (ex. List<Object>), sau
  • folosim un raw type (ex. List fără parametrul de tip).

Raw types sunt permise, dar nerecomandate: dezactivează verificarea de tip la compilare și pot duce la ClassCastException la rulare. Dacă aveți un caz real în care elementele pot fi de tipuri diferite, folosiți List<Object> sau un tip comun (o interfață, o clasă părinte).

import java.util.*;
 
public class Main {
    public static void main(String[] args) {
        // 1) Corect și sigur - generics
        List<String> nume = new ArrayList<>();
        nume.add("Ana");
        // nume.add(10); // EROARE de compilare: 10 nu este String
        String s1 = nume.get(0); // fără cast, sigur
 
        // 2) Permis, dar nerecomandat - raw type (fără <T>)
        List nespecificata = new ArrayList(); // WARNING: unchecked/unsafe
        nespecificata.add("Ana");
        nespecificata.add(10);
        String s2 = (String) nespecificata.get(1); // EXCEPȚIE la rulare: ClassCastException
 
        // 3) Colecție heterogenă (intenționat)
        List<Object> mix = new ArrayList<>();
        mix.add("Ana");
        mix.add(10);
        Object o = mix.get(1);
        if (o instanceof Integer n) {
            System.out.println(n + 5); // sigur
        }
    }
}
1.2 Liste (List)

Interfața List, pe lângă metodele moștenite din Collection, definește colecții ordonate și indexate, care permit duplicate și ale căror elemente pot fi accesate după poziție (index). În practică, cele mai folosite implementări sunt ArrayList și LinkedList.

ArrayList oferă acces aleator foarte rapid la elemente, cu cost mai mare pentru inserări/ștergeri în interiorul listei. LinkedList stochează elementele într-o listă înlănțuită, ceea ce face inserările și ștergerile locale mai eficiente (folosind iteratorul), dar accesul la un element “din mijloc” este mai lent. LinkedList implementează și Deque, astfel că poate lucra comod cu elementele de la ambele capete (ex. addFirst, addLast). Pentru stivă/coadă se preferă ArrayDeque, iar pentru liste obișnuite ArrayList/LinkedList.

import java.util.*;
 
class Liste {
    private final List<String> list1 = new ArrayList<>();        // ordonată, acces aleator rapid
    private final LinkedList<Integer> list2 = new LinkedList<>(); // listă + deque
 
    public static void main(String[] args) {
        Liste obj = new Liste();
 
        // Operații de bază pe ArrayList<String>
        obj.list1.add("Lab POO");
        obj.list1.add("Colectii");
        obj.list1.add("Structuri de date");
        if (obj.list1.contains("Colectii")) {
            System.out.println("Lista contine cuvantul");
        }
 
        // Parcurgere și ștergere în siguranță (fără ConcurrentModificationException)
        Iterator<String> it = obj.list1.iterator();
        while (it.hasNext()) {
            String s = it.next();
            System.out.println(s);
            it.remove(); // șterge elementul tocmai citit
        }
 
        // LinkedList<Integer> ca listă + deque (ambele capete)
        obj.list2.addAll(Arrays.asList(1, 10, 20));
        obj.list2.addFirst(50); // capătul din stânga
        obj.list2.addLast(17);  // capătul din dreapta
 
        // Modificare „pe loc” cu ListIterator (ex.: înmulțește numerele pare cu 10)
        ListIterator<Integer> li = obj.list2.listIterator();
        while (li.hasNext()) {
            int x = li.next();
            if (x % 2 == 0) li.set(x * 10);
        }
 
        // Afișare elemente (for-each)
        for (Integer i : obj.list2) {
            System.out.println(i);
        }
 
        // Sortare naturală (echivalent cu Collections.sort(list2))
        obj.list2.sort(Comparator.naturalOrder());
        System.out.println(obj.list2);
    }
}

  • Declarați mereu tipul elementelor: List<String>, List<Integer>.
  • Pentru eliminări în timpul parcurgerii folosiți Iterator.remove() sau removeIf(…).
  • Alegeți ArrayList când accentul este pe citire după index și LinkedList când aveți inserări/ștergeri locale cu iteratorul sau operații la ambele capete.

1.3 Mulțimi (Set și SortedSet)

Set modelează noțiunea de mulțime în sens matematic: nu pot exista două elemente o1, o2 într-un Set pentru care o1.equals(o2) este true. </note>

Set moștenește operațiile de bază din Collection, fără a introduce metode proprii. Implementări uzuale:

SortedSet reprezintă un Set în care elementele sunt păstrate în ordine crescătoare:

Implementarea standard de `SortedSet` este TreeSet.

Într-un SortedSet, pentru orice două obiecte o1, o2 ale colecției, o1.compareTo(o2) sau comparator.compare(o1, o2) trebuie să fie valid (fără excepții), iar pentru ordinea naturală, elementele null nu sunt permise.

import java.util.*;
 
class Example {
    public static void main(String[] args) {
        // 1) HashSet - fără ordine, elimină duplicatele pe baza equals()/hashCode()
        Set<String> hs = new HashSet<>();
        Collections.addAll(hs, "Ana", "Ana", "Ion");
        System.out.println("HashSet: " + hs); // ex.: [Ana, Ion]
 
        // 2) LinkedHashSet - păstrează ordinea inserării
        Set<Integer> lhs = new LinkedHashSet<>(List.of(3, 1, 2, 1));
        System.out.println("LinkedHashSet: " + lhs); // [3, 1, 2]
 
        // 3) TreeSet - comparator: lungime, apoi lexicografic
        SortedSet<String> good = new TreeSet<>(
                Comparator.comparingInt(String::length)
                          .thenComparing(Comparator.naturalOrder())
        );
        good.addAll(List.of("aa", "b", "bb"));
        System.out.println("TreeSet ok: " + good); // [b, aa, bb]
 
        // Comparator problematic: compară DOAR lungimea -> unele elemente sunt excluse
        SortedSet<String> bad = new TreeSet<>(Comparator.comparingInt(String::length));
        bad.addAll(List.of("aa", "bb")); // "bb" e ignorat: compare("aa","bb") == 0
        System.out.println("TreeSet problematic: " + bad); // [aa]
    }
}
1.4 Dicționare (Map și SortedMap)

Map descrie structuri care asociază fiecărei chei o valoare:

Ierarhia Map este separată de Collection. Operații tipice:

În practică, cele mai folosite implementări sunt:

SortedMap este un Map cu chei păstrate în ordine crescătoare; implementarea clasică este TreeMap.

import java.util.*;
 
class MiniMapDemo {
    public static void main(String[] args) {
        // 1) HashMap - fără ordine
        Map<String, Integer> freq = new HashMap<>();
        for (String w : List.of("ana", "are", "ana", "mere")) {
            freq.merge(w, 1, Integer::sum); // new = 1, altfel +1
        }
        System.out.println("HashMap (fara ordine): " + freq);
        System.out.println("getOrDefault('banane', 0) = "
            + freq.getOrDefault("banane", 0));
 
        // 2) LinkedHashMap - păstrează ordinea inserării
        Map<Integer, String> lhm = new LinkedHashMap<>();
        lhm.put(2, "B");
        lhm.put(1, "A");
        lhm.put(3, "C");
        System.out.println("LinkedHashMap (ordine inserare): " + lhm.keySet()); // [2, 1, 3]
 
        // 3) TreeMap - chei ordonate (natural)
        Map<String, Integer> sorted = new TreeMap<>(freq); // sortează după cheia String
        System.out.println("TreeMap (chei ordonate): " + sorted);
 
        // 4) Parcurgere eficientă cu entrySet()
        for (Map.Entry<String, Integer> e : sorted.entrySet()) {
            System.out.println(e.getKey() + " => " + e.getValue());
        }
    }
}

2. Iteratori și enumerări

Enumerările și iteratorii descriu modalități de parcurgere secvențială a unei colecții. În Java, parcurgerea se face în principal cu:

2.1 Enumeration

Enumeration este o interfață veche pentru parcurgere. O mai întâlnim la Vector sau o putem obține din orice colecție prin Collections.enumeration(…). În cod modern, se preferă Iterator.

import java.util.*;
 
class DemoEnumeration {
    public static void main(String[] args) {
        List<Integer> list = List.of(3, 7, 0, 5);
        Enumeration<Integer> en = Collections.enumeration(list); // din orice Collection
 
        while (en.hasMoreElements()) {
            int x = en.nextElement(); // Integer, nu Object (datorită generics)
            System.out.println(x);
        }
    }
 
} 
2.2 Iterator

Iterator oferă metodele:

Dacă modificați colecția în timpul parcurgerii, folosiți iterator.remove() (sau, mai simplu, removeIf(…) pe colecție).

import java.util.*;
 
class DemoIterator {
    public static void main(String[] args) {
        List<String> l = new ArrayList<>(List.of("ana", "bad", "ion", "bogdan"));
 
        // Variantă modernă: removeIf
        l.removeIf(s -> s.length() == 4); // șterge elementele cu 4 litere
 
        // Echivalent cu iterator.remove()
        Iterator<String> it = l.iterator();
        while (it.hasNext()) {
            String s = it.next();
            if (s.startsWith("b")) it.remove(); // sigur
        }
 
        System.out.println(l);
    }
}
2.3 ListIterator (liste, ambele sensuri)

ListIterator extinde Iterator și oferă în plus:

Este util când trebuie să modificați lista “pe loc” sau să o parcurgeți bidirecțional.

import java.util.*;
 
class DemoListIterator {
    public static void main(String[] args) {
        List<Integer> l = new ArrayList<>(List.of(0, 1, 2, 0, 3));
 
        // 1) Înlocuire „pe loc”: 0 -> 10
        ListIterator<Integer> it = l.listIterator();
        while (it.hasNext()) {
            if (it.next() == 0) it.set(10);
        }
 
        // 2) Inserare după 1
        ListIterator<Integer> it2 = l.listIterator();
        while (it2.hasNext()) {
            if (it2.next() == 1) {  // elementul curent 1; cursorul este după 1 (între 1 și 2)
                it2.add(99);        // inserează între 1 și 2
                break;
            }
        }
 
        System.out.println(l); // [10, 1, 99, 2, 10, 3]
    }
}

Dacă parametrizăm colecția/iteratorul (ex. List<String>, Iterator<String>), metodele next()/previous() întorc direct tipul elementului și nu mai avem nevoie de cast.

Dacă folosim raw types (ex. List, Iterator), next()/previous() întorc Object și conversia devine responsabilitatea programatorului - cu risc de ClassCastException la rulare.

import java.util.*;
 
class ObservatieIterator {
    public static void main(String[] args) {
        // 1) Parametrizat (recomandat): fără cast, sigur
        List<String> l = new ArrayList<>(List.of("ana", "ion"));
        Iterator<String> it = l.iterator();
        String s1 = it.next(); // String, nu Object
        ListIterator<String> li = l.listIterator(l.size());
        String last = li.previous(); // tot String
        System.out.println("OK (generic): " + s1 + ", " + last);
 
        // 2) Neparametrizat (raw type): next()/previous() -> Object, necesită cast
        List raw = new ArrayList(); // WARNING: unchecked/raw type
        raw.add("text"); // compilează
        raw.add(10);     // compilează (amestec de tipuri!)
        Iterator itr = raw.iterator(); // WARNING: raw
        Object o1 = itr.next(); // "text" ca Object
        Object o2 = itr.next(); // 10 ca Object
 
        // Castul e responsabilitatea ta; poate eșua la rulare:
        try {
            String s2 = (String) o2; // ClassCastException (Integer -> String)
            System.out.println(s2);
        } catch (ClassCastException ex) {
            System.out.println("Eroare la rulare (raw type): " + ex);
        }
    }
}

3. Genericitate (generics)

Fără generics, o colecție raw acceptă obiecte de orice fel, iar la citire trebuie să facem conversii (cast). Codul devine greu de urmărit, iar amestecul de tipuri poate produce ușor ClassCastException la rulare.

Genericitatea rezolvă exact aceste probleme:

List<Integer> list = new ArrayList<>(); // parametrizare: lista conține DOAR Integer
list.add(5);
list.add(7);
 
int x = list.iterator().next(); // fără cast; auto-unboxing (Integer -> int)
int y = list.get(1);            // tot fără cast
 
// list.add("Text"); // eroare de compilare: tip incompatibil

Dacă aveți un caz real în care elementele pot fi de mai multe tipuri, NU reveniți la raw types. Folosiți:

  • List<Object> și verificați cu `instanceof` înainte de cast,
  • sau proiectați o ierarhie comună (o interfață, o clasă părinte) și parametrizați lista cu acest supertip.

În toate celelalte situații folosiți colecții parametrizate (List<Student>, Map<String, Integer>) pentru siguranță și claritate.

4. equals() vs hashCode()

Metoda equals(Object) stabilește egalitatea logică dintre două instanțe. Definiția egalității aparține modelului de date (ex.: doi studenți pot fi considerați egali prin CNP, sau prin combinație de câmpuri).

Metoda hashCode() returnează un rezumat numeric (int) al obiectului. Colecțiile pe bază de dispersie (HashSet, HashMap) folosesc acest rezumat pentru localizare rapidă.

  • Dacă două obiecte sunt egale prin equals, atunci trebuie să aibă același hashCode().

În HashSet și HashMap, operațiile de apartenență și unicitate funcționează astfel:

  1. colecția calculează hashCode() și alege locul elementului;
  2. confirmă prezența prin equals().
import java.util.*;
 
class EqualsHashDemo {
 
    // Varianta corectă: equals și hashCode folosesc ACELEAȘI câmpuri
    static final class GoodStudent {
        private final String id;
        GoodStudent(String id) { this.id = id; }
 
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof GoodStudent g)) return false;
            return Objects.equals(id, g.id);
        }
 
        @Override
        public int hashCode() {
            return Objects.hash(id);
        }
    }
 
    // Varianta greșită: equals suprascris, hashCode NU
    static final class BadStudent {
        private final String id;
        BadStudent(String id) { this.id = id; }
 
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof BadStudent b)) return false;
            return Objects.equals(id, b.id);
        }
        // (fără hashCode) -> folosește Object.hashCode(), diferit pentru instanțe diferite
    }
 
    public static void main(String[] args) {
        // Caz corect
        GoodStudent a1 = new GoodStudent("42");
        GoodStudent a2 = new GoodStudent("42");
 
        System.out.println("a1.equals(a2) = " + a1.equals(a2)); // true
        System.out.println("a1.hashCode = " + a1.hashCode()
                + " | a2.hashCode = " + a2.hashCode()); // egale
 
        Set<GoodStudent> good = new HashSet<>();
        good.add(a1);
        good.add(a2);
        System.out.println("HashSet<GoodStudent>.size = " + good.size()); // 1
 
        // Caz greșit
        BadStudent b1 = new BadStudent("42");
        BadStudent b2 = new BadStudent("42");
 
        System.out.println("b1.equals(b2) = " + b1.equals(b2)); // true
        System.out.println("b1.hashCode = " + b1.hashCode()
                + " | b2.hashCode = " + b2.hashCode()); // diferite
 
        Set<BadStudent> bad = new HashSet<>();
        bad.add(b1);
        bad.add(b2);
        System.out.println("HashSet<BadStudent>.size = " + bad.size()); // 2 (duplicate)
 
        System.out.println("bad.contains(new BadStudent(\"42\")) = "
                + bad.contains(new BadStudent("42"))); // false
    }
}