Panoramica del Java Collections Framework
Una panoramica del Java Collections Framework: interfacce, implementazioni e algoritmi in java.util.
Quasi ogni programma finisce per contenere un gruppo di elementi — ordini in attesa di spedizione, parole in un documento, utenti attualmente online, la coda di attività che un thread worker eseguirà. La risposta di Java alla domanda "dove metto quel gruppo?" è il Collections Framework: un insieme coordinato di interfacce in java.util, una dozzina o più di implementazioni dietro di esse e un'unica classe di utilità con algoritmi (Collections) che lavora sulle interfacce anziché su una specifica implementazione. Questo capitolo è la mappa — cosa c'è nel framework, perché è strutturato così e quale famiglia usare in quale situazione. Ogni capitolo di questa parte è un approfondimento su uno dei riquadri di quella mappa.
Perché un framework, non solo delle classi
Prima di Java 1.2 esistevano classi per i gruppi (Vector, Hashtable, Stack) ma nessuna interfaccia condivisa — non si poteva scrivere un metodo che accettasse "qualsiasi lista" e funzionasse con tutte e tre. Il Collections Framework ha risolto il problema separando cosa è un gruppo (l'interfaccia) da come viene memorizzato (l'implementazione). Il vantaggio è evidente ogni giorno:
// You program against the interface, not the class.
List<String> names = new ArrayList<>();
// Later, swap in a different implementation without touching the rest of the code:
List<String> names = new LinkedList<>();Ogni metodo che chiami su names è definito su List. La scelta tra ArrayList e LinkedList è una decisione di prestazioni, non di API. Puoi anche scrivere metodi come void print(List<String> xs) una sola volta e passare entrambe le implementazioni.
Le quattro interfacce radice
Il framework è organizzato attorno a quattro interfacce radice. Scegli quella il cui contratto corrisponde alla natura dei tuoi dati:
Collection<E>— la radice. Un gruppo di elementiE. Non dice nulla sull'ordine, i duplicati o l'indicizzazione.List<E>— una raccolta ordinata accessibile per indice. Duplicati consentiti. Pensa a "array che cresce" o "sequenza concatenata."ArrayList,LinkedList,Vector.Set<E>— una raccolta che vieta i duplicati. Insieme matematico. Può essere ordinata o meno.HashSet,LinkedHashSet,TreeSet.Map<K, V>— ricerca chiave→valore. Non è unaCollection(i suoi elementi sono entry, non valori singoli), ma fa parte del framework.HashMap,LinkedHashMap,TreeMap.
Altre due si affiancano a List e Set come specializzazioni di Collection:
Queue<E>— una raccolta "prossimo in fila". FIFO per default.LinkedList,ArrayDeque,PriorityQueue.Deque<E>— una coda a doppia estremità. Aggiunta e rimozione a entrambe le estremità.ArrayDeque,LinkedList.
Ogni classe di collezione nella libreria standard implementa almeno una di queste interfacce.
Scegli la famiglia in base al comportamento, poi la classe in base al costo
La decisione si articola su due livelli:
- Di cosa ha bisogno il dato? Ordinato con duplicati →
List. Senza duplicati →Set. Ricerca per chiave →Map. Produttore/consumatore →QueueoDeque. - All'interno della famiglia, quali sono i pattern di accesso? Accesso casuale per indice →
ArrayList. Inserimento/rimozione frequente in testa →LinkedListoArrayDeque. Iterazione ordinata →TreeSet/TreeMap. Semplicemente "ho bisogno di un contenitore veloce" →HashSet/HashMap.
Un rapido cheat sheet per le soluzioni più comuni:
| Necessità | Usa | Note |
|---|---|---|
| Lista ridimensionabile, accesso casuale veloce | ArrayList | La List predefinita. |
| Lista con inserimento/rimozione molto frequente alle estremità | ArrayDeque (come coda simile a una lista) | Supera LinkedList in pratica. |
| Set, contains/add/remove veloci | HashSet | Nessuna garanzia di ordine. |
| Set con iterazione prevedibile | LinkedHashSet | Iterazione in ordine di inserimento. |
| Set ordinato | TreeSet | Operazioni O(log n). |
| Map, ricerca veloce | HashMap | La Map predefinita. |
| Map con iterazione prevedibile | LinkedHashMap | Utile per cache (LRU). |
| Map ordinata | TreeMap | Chiavi in ordine ordinato. |
| Coda FIFO | ArrayDeque | Più veloce di LinkedList. |
| Estrai sempre il più piccolo | PriorityQueue | Backed da heap. |
Vector, Stack e Hashtable sono le classi pre-1.2 — ancora presenti nel JDK per compatibilità con il passato, ma non più la scelta giusta nel codice nuovo. Le loro sostituzioni moderne sono trattate nei rispettivi capitoli.
I generics rendono le collections type-safe
Ogni interfaccia e classe di collezione è generica. La parametrizzi con il tipo dell'elemento al momento della dichiarazione, e il compilatore la verifica da quel momento in poi:
List<String> names = new ArrayList<>();
names.add("Ada");
names.add(42); // ❌ compile error — Integer is not a String
String first = names.get(0); // no cast neededIl diamante <> a destra chiede al compilatore di inferire il tipo da sinistra — risparmia digitazione senza perdere la sicurezza dei tipi. La parte precedente del libro (Generics) contiene le regole alla base di tutto questo; il resto di questa parte presuppone che tu l'abbia letta.
Gli algoritmi si trovano nella classe Collections
Le operazioni non legate a una specifica implementazione — sort, shuffle, reverse, binary-search, min, max, frequency, wrapper non modificabili — si trovano nella classe di utilità statica java.util.Collections. Nota la s finale: Collection (l'interfaccia) è un elemento; Collections (la classe) è il toolkit:
List<Integer> xs = new ArrayList<>(List.of(3, 1, 4, 1, 5, 9, 2, 6));
Collections.sort(xs); // mutates xs
int idx = Collections.binarySearch(xs, 5);
List<Integer> ro = Collections.unmodifiableList(xs);Dedichiamo tre capitoli verso la fine di questa parte a Collections — ricerca, ordinamento e wrapper non modificabili — perché la classe di utilità è dove avviene molto del lavoro pratico.
Un primo tour completo
Il programma sottostante sceglie un rappresentante per ogni famiglia e mostra le operazioni che userai continuamente. Non preoccuparti di capire ogni riga ancora — ogni classe qui ha il suo capitolo. L'obiettivo è vedere la struttura del framework: stessi nomi di metodi, stessi pattern, stesso modello di iterazione.
Nota tre cose nell'output:
- Il
Setha eliminato silenziosamente il duplicato"Effective Java". Questo è il contratto — nessun codice aggiuntivo da parte tua. - La
Queueha restituito gli elementi nell'ordine in cui sono stati aggiunti.offeraccoda,peekguarda la testa senza rimuovere,pollrimuove e restituisce. - Il ciclo
for-eachfunziona su ogniCollection. Le mappe iterano tramiteentrySet(),keySet()ovalues()a seconda di cosa vuoi.
Questo è il framework in miniatura.
HashSet e HashMap non danno alcuna garanzia sull'ordine di iterazione — l'ordine che vedi nell'output sopra è un dettaglio implementativo e può variare tra versioni della JVM o esecuzioni diverse. Se hai bisogno di un ordine stabile, usa LinkedHashSet/LinkedHashMap (ordine di inserimento) o TreeSet/TreeMap (ordine ordinato). Non scrivere mai codice che dipende dall'ordine di iterazione di un semplice HashSet o HashMap.
Cosa c'è dopo
Hai visto la mappa. Il resto della Parte 11 percorre ciascuna regione. Il punto di partenza naturale è la radice da cui ogni collezione eredita — l'interfaccia Collection stessa, dove metodi come add, remove, contains, size e iterator() sono definiti una volta per tutte.