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).
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.
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 } } }
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); } }
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.
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] } }
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()); } } }
Enumerările și iteratorii descriu modalități de parcurgere secvențială a unei colecții. În Java, parcurgerea se face în principal cu:
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); } } }
Iterator oferă metodele:
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); } }
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ă 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); } } }
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
În toate celelalte situații folosiți colecții parametrizate (List<Student>, Map<String, Integer>) pentru siguranță și claritate.
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ă.
În HashSet și HashMap, operațiile de apartenență și unicitate funcționează astfel:
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 } }