El CatalegArray amb què vas tancar la lliçó anterior funciona, però tres de cada quatre línies són infraestructura: un comptador n paral·lel al length, un Arrays.copyOf per doblar la capacitat, un System.arraycopy per tapar forats, un null manual per no filtrar memòria. Cap d'aquestes línies no parla de biblioteques, de préstecs ni d'empleats. I fins i tot amb tot aquest esforç, el catàleg continua sense poder garantir que no hi hagi ISBN repetits sense recórrer-lo sencer, ni trobar un material per la seva referència en menys de O(n).
El Java Collections Framework és la resposta del JDK a aquest problema, i és una de les millors peces de disseny de la plataforma. No són "unes quantes classes útils": és una arquitectura d'interfícies, implementacions i algorismes que fa vint-i-cinc anys que és el vocabulari comú de tot programador Java. Aprendre-la bé significa dues coses. La primera, que deixaràs d'escriure infraestructura. La segona, i més important, que aprendràs a triar l'estructura de dades correcta, que és una de les decisions que més impacte té en el rendiment i en la claredat d'un programa real.
Aquesta lliçó és el mapa. No entra a fons en cap implementació —això són les set lliçons següents—, sinó que et dona la vista aèria: quines interfícies existeixen i com es relacionen, quina implementació triar per a cada problema, com funciona el recorregut per dins i quines operacions comparteixen totes les col·leccions. La taula mestra de l'apartat 4 és el cor de tot el mòdul; hi tornaràs una vegada i una altra.
Contingut
- Què és el Framework de Col·leccions
- La jerarquia d'interfícies
- Declara per la interfície, instancia la implementació
- La taula mestra d'implementacions
- Els genèrics a les col·leccions
Iterable,Iteratori elfor-eachper dinsConcurrentModificationException: fail-fast explicat- Els mètodes comuns de
Collection - Col·leccions immutables i de conveniència
- Com triar la col·lecció correcta
- Primera refactorització de BiblioTech
- Errors Habituals i Consells
- Exercicis
- Què és el Framework de Col·leccions
Una col·lecció és un objecte que agrupa diversos elements i els sap gestionar: afegir, treure, buscar, comptar, recórrer. Enfront de l'array, una col·lecció té tres virtuts decisives:
- Creix i minva sola. No hi ha capacitat que administrar.
- Té semàntica. Un
Setgaranteix que no hi ha duplicats; unaQueuegaranteix l'ordre FIFO. L'array no garanteix res: només guarda. - És intercanviable. Com que totes implementen les mateixes interfícies, canviar
ArrayListperLinkedListés canviar una paraula.
El Framework s'organitza en tres capes que convé distingir des del principi:
| Capa | Què és | Exemples |
|---|---|---|
| Interfícies | El contracte: quines operacions ofereix un tipus de col·lecció | Collection, List, Set, Queue, Deque, Map |
| Implementacions | El com: l'estructura de dades concreta | ArrayList, LinkedList, HashSet, TreeMap, ArrayDeque |
| Algorismes | Operacions genèriques sobre qualsevol implementació | Collections.sort, Collections.max, Collections.shuffle |
Aquesta separació és l'aplicació directa de tot el que vas aprendre al mòdul 4. List és una interfície en el sentit exacte de 04-01: un contracte que diu què es pot fer sense dir com. ArrayList i LinkedList són dues implementacions diferents del mateix contracte, i el polimorfisme de 03-06 permet intercanviar-les sense tocar el codi que les fa servir. El Framework de Col·leccions és, probablement, el millor exemple de disseny orientat a interfícies que trobaràs.
Tot viu a java.util, així que gairebé qualsevol classe que escriguis començarà amb algun d'aquests imports:
- La jerarquia d'interfícies
Aquesta és l'estructura que cal tenir gravada:
classDiagram
Iterable <|-- Collection
Collection <|-- List
Collection <|-- Set
Collection <|-- Queue
Set <|-- SortedSet
SortedSet <|-- NavigableSet
Queue <|-- Deque
class Iterable {
<<interface>>
+iterator() Iterator
+forEach(Consumer)
}
class Collection {
<<interface>>
+add(e) boolean
+remove(o) boolean
+contains(o) boolean
+size() int
+isEmpty() boolean
}
class List {
<<interface>>
ORDRE per posicio
DUPLICATS permesos
+get(int)
+set(int, e)
+indexOf(o)
}
class Set {
<<interface>>
SENSE duplicats
+add() retorna false si ja hi era
}
class Queue {
<<interface>>
Ordre de PROCES (FIFO)
+offer(e)
+poll()
+peek()
}
class Deque {
<<interface>>
Doble extrem: cua I pila
+addFirst(e)
+addLast(e)
}
I a part, sense cap relació d'herència amb tot això:
classDiagram
Map <|-- SortedMap
SortedMap <|-- NavigableMap
class Map {
<<interface>>
Parells clau to valor
Claus UNIQUES
+put(k, v)
+get(k)
+containsKey(k)
+keySet()
+values()
+entrySet()
}
Llegeix les dues jerarquies així, de dalt a baix:
Iterableés la interfície més general de totes i només promet una cosa: "et puc donar un iterador per recórrer-me un per un". Qualsevol cosa que siguiIterablefunciona ambfor-each. Fixa't que un array no ésIterable—no és una classe, no implementa res—; elfor-eachsobre arrays funciona perquè el compilador el tracta com un cas especial (ho veuràs a l'apartat 6).Collectionafegeix el vocabulari bàsic de "grup d'elements":add,remove,contains,size,isEmpty,clear. És el mínim comú a llistes, conjunts i cues.List: elements ordenats per posició, amb índex i duplicats permesos. És l'equivalent natural de l'array.Set: sense duplicats, sense accés per índex. Modela el concepte matemàtic de conjunt.Queue: elements en un ordre de procés, normalment FIFO. S'afegeix per un extrem i es consumeix per l'altre.Deque: cua de doble extrem; serveix alhora com a cua i com a pila.
Per què Map no és una Collection
És la pregunta que fa tothom la primera vegada, i té una resposta precisa: una Collection guarda elements solts; un Map guarda parells clau→valor. Els mètodes de Collection no tindrien sentit en un mapa:
- Què faria
map.add(x)?xés una clau, un valor, un parell? - Què retornaria
map.iterator()? Claus, valors o parells? - Què comprovaria
map.contains(x)? El mapa necessita dues preguntes diferents:containsKeyicontainsValue.
Forçar Map extends Collection hauria produït una interfície plena de mètodes ambigus o no suportats. La solució de disseny és més neta: Map és una jerarquia independent, i ofereix tres vistes que sí que són col·leccions:
Map<String, Material> index = new HashMap<>();
Set<String> claus = index.keySet(); // les claus, sense duplicats: un Set
Collection<Material> valors = index.values(); // els valors, es poden repetir
Set<Map.Entry<String, Material>> parells = index.entrySet(); // els parellsI aquestes tres vistes sí que són Iterable, així que sí que es recorren amb for-each. És el millor dels dos mons: Map conserva la seva API pròpia i coherent, i continua connectat a la resta del Framework a través de les seves vistes. A 05-05 ho veuràs en detall.
- Declara per la interfície, instancia la implementació
Aquesta és la regla d'or del Framework, i probablement l'hàbit professional més rendible d'aquesta lliçó:
List<Llibre> cataleg = new ArrayList<>(); // CORRECTE
ArrayList<Llibre> malament = new ArrayList<>(); // funciona, pero es pitjor dissenyA l'esquerra del =, el tipus més general que cobreixi el que necessites (la interfície). A la dreta, la implementació concreta. Els motius:
1. Pots canviar d'implementació tocant una paraula. Si demà descobreixes que necessites comportament de cua, canvies new ArrayList<>() per new LinkedList<>() i res més del programa no se n'assabenta. Si havies declarat ArrayList<Llibre>, has de revisar cada declaració, cada paràmetre i cada tipus de retorn.
2. Les signatures dels teus mètodes es tornen flexibles. Compara:
public void processar(ArrayList<Llibre> llibres) { ... } // nomes accepta ArrayList
public void processar(List<Llibre> llibres) { ... } // accepta ArrayList, LinkedList, List.of(...)
public void processar(Collection<Llibre> llibres) { ... } // accepta a mes Set i Queue
public void processar(Iterable<Llibre> llibres) { ... } // accepta QUALSEVOL cosa recorribleLa regla pràctica a les signatures: demana el tipus més general que et serveixi. Si només recorres, Iterable o Collection. Si necessites índexs, List. En canvi, en retornar convé ser una mica més específic per no amagar garanties útils (si retornes alguna cosa sense duplicats, declara-la Set).
3. Comuniques la intenció, no el mecanisme. List<Llibre> diu "una seqüència ordenada amb possible repetició". ArrayList<Llibre> diu a més "implementada sobre un array", un detall intern que a qui crida no li importa.
Les excepcions són poques i explícites: quan necessites un mètode que només existeix a la implementació. ArrayList té trimToSize(), LinkedList té addFirst() heretat de Deque. En aquests casos, declara pel tipus que ofereixi el mètode que realment fas servir:
I una nota sobre l'operador diamant <>: des de Java 7, el tipus del costat dret es dedueix de l'esquerre, així que new ArrayList<Llibre>() s'escriu new ArrayList<>(). Escriu-ho sempre així.
- La taula mestra d'implementacions
Aquesta taula és el cor del mòdul. Cada fila és una lliçó o un apartat de les set següents, però tenir-les juntes et dona una cosa que cap lliçó aïllada no dona: criteri d'elecció.
| Implementació | Interfície | Ordre | Duplicats | null |
Accés per índex/clau | Afegir | Eliminar | Cercar | Quan triar-la |
|---|---|---|---|---|---|---|---|---|---|
ArrayList |
List |
Inserció | Sí | Sí | O(1) | O(1)* al final | O(n) | O(n) | Llista per defecte. El 90 % dels casos |
LinkedList |
List, Deque |
Inserció | Sí | Sí | O(n) | O(1) als extrems | O(1) amb iterador | O(n) | Cua/pila; insercions massives als extrems |
HashSet |
Set |
Cap | No | Un null |
— | O(1) | O(1) | O(1) | Conjunt per defecte: unicitat i pertinença |
LinkedHashSet |
Set |
Inserció | No | Un null |
— | O(1) | O(1) | O(1) | Com HashSet però amb ordre reproduïble |
TreeSet |
NavigableSet |
Ordenat | No | No | — | O(log n) | O(log n) | O(log n) | Necessites els elements sempre ordenats o rangs |
HashMap |
Map |
Cap | Claus no | 1 clau, N valors | O(1) | O(1) | O(1) | O(1) | Mapa per defecte: índex clau→valor |
LinkedHashMap |
Map |
Inserció o accés | Claus no | Sí | O(1) | O(1) | O(1) | O(1) | Ordre reproduïble; memòria cau LRU |
TreeMap |
NavigableMap |
Claus ordenades | Claus no | Clau no | O(log n) | O(log n) | O(log n) | O(log n) | Claus ordenades, rangs, firstKey/floorKey |
ArrayDeque |
Deque, Queue |
Inserció | Sí | No | — | O(1) extrems | O(1) extrems | O(n) | Cua i pila per defecte |
PriorityQueue |
Queue |
Per prioritat | Sí | No | — | O(log n) | O(log n) el cap | O(n) | Processar sempre "el més urgent" primer |
* Amortitzat: gairebé sempre O(1), però quan l'array intern s'omple hi ha una còpia O(n). Veuràs per què la mitjana continua sent O(1) a 05-03.
Llegeix la taula per columnes i en sortiran els patrons que de debò importen:
- La columna "Ordre" té tres valors. Cap significa que l'ordre de recorregut és impredictible i pot canviar entre execucions o en afegir elements: no en depenguis mai. Inserció significa que es recorre en l'ordre en què vas posar els elements. Ordenat significa per
ComparableoComparator(05-09). - La columna "O(1) enfront de O(log n)" resumeix la diferència entre les estructures de hash (
HashXxx) i les d'arbre (TreeXxx): el hash és més ràpid, l'arbre manté l'ordre. Triar és decidir si necessites aquest ordre. - La columna "
null" és una font enorme deNullPointerExceptioninesperades. Les estructures "modernes" (ArrayDeque,PriorityQueue) els rebutgen a propòsit, perquè fan servirnullcom a senyal interna de "buit". Les d'arbre els rebutgen perquè no es pot compararnullambcompareTo. - Les files en negreta del "quan triar-la" són les quatre respostes per defecte:
ArrayList,HashSet,HashMap,ArrayDeque. Comença sempre per una d'elles i canvia-la només si tens un motiu concret.
- Els genèrics a les col·leccions
Totes les col·leccions són genèriques: porten entre angles el tipus d'element que contenen.
List<Llibre> cataleg = new ArrayList<>();
Set<String> isbnVistos = new HashSet<>();
Map<String, Material> index = new HashMap<>();De moment, llegeix-ho així: <...> és el tipus del que hi ha a dins. List<Llibre> és "una llista de llibres". Map<String, Material> és "un mapa amb claus que són cadenes i valors que són materials". Res més.
El que hi guanyes és seguretat en temps de compilació:
List<Llibre> cataleg = new ArrayList<>();
cataleg.add(new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018));
// cataleg.add("Java Eficac"); // ERROR DE COMPILACIO, i aixo es bo
Llibre primer = cataleg.get(0); // sense casting: el compilador ja sap que es un LlibreAbans de Java 5 les col·leccions guardaven Object, i cada lectura requeria un casting que podia fallar en execució:
List llistaVella = new ArrayList(); // "raw type": NO l'escriguis mai
llistaVella.add(new Llibre(...));
llistaVella.add("una cadena qualsevol"); // compila sense protestar
Llibre l = (Llibre) llistaVella.get(1); // ClassCastException en EXECUCIOEls genèrics mouen aquest error del temps d'execució al temps de compilació, que és exactament on vols els errors. No facis servir mai tipus crus (sense <>); el compilador t'avisarà amb unchecked warning i amb raó.
Autoboxing i el seu cost
Els genèrics només accepten tipus referència, no primitius. List<int> no compila; cal escriure List<Integer>. L'autoboxing de 01-04 fa la conversió automàticament i el codi sembla natural:
List<Integer> diesRetard = new ArrayList<>();
diesRetard.add(5); // autoboxing: 5 -> Integer.valueOf(5)
int primer = diesRetard.get(0); // unboxing: Integer -> intPerò el cost és real i convé conèixer-lo:
| Aspecte | int[] dades |
List<Integer> dades |
|---|---|---|
| Memòria per element | 4 bytes | ~16 bytes d'objecte + 4-8 de referència |
| Localitat de memòria cau | Excel·lent (contigu) | Dolenta (objectes dispersos) |
| Cost de llegir | Directe | Seguir referència + unboxing |
Per a un milió d'enters, la diferència és d'uns 4 MB enfront de més de 20 MB, i unes quantes vegades més lent en recórrer. En una aplicació de gestió tant se val; en càlcul numèric intensiu, no. La regla: fes servir col·leccions llevat que treballis amb molts primitius i el rendiment sigui crític, cas en què l'array guanya.
Hi ha a més un parany de comparació amb Integer que ja vas veure a 01-05 i que reapareix aquí:
Integer a = 127, b = 127;
Integer c = 128, d = 128;
System.out.println(a == b); // true (memoria cau de -128 a 127)
System.out.println(c == d); // false (objectes diferents)
System.out.println(c.equals(d)); // true <- SEMPRE equalsLa teoria completa dels genèrics —paràmetres de tipus propis (class Caixa<T>), comodins (? extends, ? super), límits (<T extends Comparable<T>>) i l'esborrat de tipus que explica per què List<int> és impossible— és la lliçó 10-01. En aquest mòdul et basta amb fer servir els genèrics existents.
Iterable, Iterator i el for-each per dins
Iterable, Iterator i el for-each per dinsEl for-each que vas aprendre a 05-01 no és màgia: és sucre sintàctic. El compilador el tradueix a una altra cosa, i saber a què t'explicarà de cop uns quants comportaments que si no semblen arbitraris.
Sobre un array, el compilador genera un for clàssic:
for (Material m : cataleg) { System.out.println(m.getTitol()); }
// el compilador genera alguna cosa equivalent a:
for (int i = 0; i < cataleg.length; i++) {
Material m = cataleg[i];
System.out.println(m.getTitol());
}Sobre un Iterable (qualsevol col·lecció), genera un bucle amb un iterador:
for (Material m : llistaCataleg) { System.out.println(m.getTitol()); }
// el compilador genera alguna cosa equivalent a:
Iterator<Material> it = llistaCataleg.iterator();
while (it.hasNext()) {
Material m = it.next();
System.out.println(m.getTitol());
}Un Iterator és un objecte que representa una posició dins d'un recorregut i ofereix tres mètodes:
| Mètode | Què fa |
|---|---|
boolean hasNext() |
Queda algun element per visitar? |
E next() |
Retorna el següent i avança. Si no en queda cap, NoSuchElementException |
void remove() |
Elimina l'últim element retornat per next(), de manera segura |
El disseny és una aplicació preciosa del principi de 04-01: Iterator és un contracte que desacobla el recorregut de l'estructura. Un ArrayList l'implementa movent un índex; una LinkedList, saltant de node en node; un HashSet, recorrent cubells. El teu bucle no s'assabenta de cap d'aquestes diferències.
flowchart LR
A["Comenca el for-each"] --> B["colleccio.iterator()"]
B --> C{"it.hasNext()"}
C -- si --> D["e = it.next()"]
D --> E["cos del bucle"]
E --> C
C -- no --> F["Fi del bucle"]
Normalment no escriuràs iteradors a mà. Hi ha una excepció important, i és el motiu que existeixin: eliminar elements mentre recorres.
Iterator<Material> it = cataleg.iterator();
while (it.hasNext()) {
Material m = it.next();
if (!m.estaDisponible()) {
it.remove(); // segur: la colleccio sap que ha estat l'iterador
}
}I, com que Iterable és la interfície més general de totes, un mètode que només recorre la pot acceptar i funcionar amb qualsevol origen de dades:
public static double sumarTarifes(Iterable<Material> materials) {
double total = 0;
for (Material m : materials) { total += m.getTarifaDiaria(); }
return total;
}
// val per a List, Set, Queue, TreeSet, keySet()... per a qualsevol cosa recorribleIterable també ofereix forEach, que rep un Consumer dels que vas dominar a 04-06:
cataleg.forEach(m -> System.out.println(m.descriure()));
cataleg.forEach(System.out::println); // referencia a metodeÉs equivalent a un for-each i de vegades més llegible, sobretot quan ja tens la referència a mètode escrita. I al mòdul 10 veuràs que stream() obre una tercera via, molt més expressiva, per filtrar, transformar i agrupar en una sola expressió; en aquest mòdul recorrerem sempre amb for-each, Iterator i els mètodes propis de les col·leccions.
ConcurrentModificationException: fail-fast explicat
ConcurrentModificationException: fail-fast explicatAra, l'error que produeix més cerques a Internet de tot el Framework:
List<Material> cataleg = new ArrayList<>(...);
for (Material m : cataleg) {
if (!m.estaDisponible()) {
cataleg.remove(m); // ConcurrentModificationException
}
}El nom enganya: no cal cap fil concurrent. "Concurrent" aquí significa "durant el recorregut". El que passa és això.
Cada col·lecció porta un comptador intern anomenat modCount (modification count) que s'incrementa cada vegada que la seva estructura canvia: cada add, cada remove, cada clear. Quan demanes un iterador, aquest desa una còpia del modCount actual. I a cada next(), l'iterador compara:
// dins de l'iterador d'ArrayList, simplificat:
final void checkForComodification() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}Si la col·lecció ha canviat per darrere de l'iterador, els dos comptadors difereixen i l'iterador es nega a continuar.
flowchart TD
A["cataleg.iterator()<br/>expectedModCount = modCount = 3"] --> B["it.next() → retorna m0"]
B --> C["cataleg.remove(m0)<br/>modCount passa a 4"]
C --> D["it.next()"]
D --> E{"modCount 4<br/>diferent de<br/>expectedModCount 3"}
E -- si --> F["ConcurrentModificationException"]
A aquesta política se l'anomena fail-fast: falla aviat i de manera sorollosa. I és una virtut, no un caprici. Si l'iterador continués endavant, el recorregut es desincronitzaria i el resultat seria pitjor que una excepció: elements saltats en silenci. Comprova-ho amb aquest exemple, que no llança excepció i tot i així està malament:
List<String> noms = new ArrayList<>(List.of("Marta Ruiz", "Diego Alonso", "Nuria Vidal"));
for (String n : noms) {
if (n.startsWith("Marta")) { noms.remove(n); }
}
System.out.println(noms); // [Diego Alonso, Nuria Vidal] ... i sense excepcioEn eliminar l'element 0 d'una llista de 3, la mida baixa a 2 i l'índex intern de l'iterador passa a 1, amb la qual cosa hasNext() (que compara 1 != 2) dona true, es retorna "Nuria Vidal", l'índex passa a 2 i hasNext() dona false: el bucle acaba abans d'hora i "Diego Alonso" no es visita mai. Si l'element que cal esborrar és el penúltim, el recorregut acaba sense excepció i sense haver mirat l'últim. Aquest és l'escenari que el fail-fast intenta evitar; que de vegades no salti és una casualitat del recompte, no una garantia.
Les quatre solucions
1. Iterator.remove() — la solució clàssica, vàlida en qualsevol versió de Java:
Iterator<Material> it = cataleg.iterator();
while (it.hasNext()) {
if (!it.next().estaDisponible()) {
it.remove(); // l'iterador ACTUALITZA el seu expectedModCount
}
}2. removeIf(Predicate) — des de Java 8, la solució preferida per llegibilitat:
Una línia, sense iterador visible, i amb el Predicate de 04-06. És el que has d'escriure per defecte.
3. Recórrer una còpia — quan necessites modificar l'original mentre la llegeixes sencera:
for (Material m : new ArrayList<>(cataleg)) { // recorres la COPIA
if (!m.estaDisponible()) { cataleg.remove(m); } // modifiques l'ORIGINAL
}Costa memòria (duplica la llista) però és imprescindible quan l'operació dins del bucle pot afegir i treure elements.
4. Recórrer cap enrere amb índexs — útil si necessites la posició:
for (int i = cataleg.size() - 1; i >= 0; i--) {
if (!cataleg.get(i).estaDisponible()) { cataleg.remove(i); }
}Cap enrere, perquè en eliminar l'element i els següents es desplacen; recorrent en sentit invers, els ja visitats no es mouen.
| Situació | Solució recomanada |
|---|---|
| Eliminar segons un criteri simple | removeIf |
| Eliminar amb lògica complexa o efectes col·laterals | Iterator.remove() |
| Afegir i treure durant el recorregut | Recórrer una còpia |
| Necessites l'índex de l'element eliminat | for clàssic cap enrere |
| Modificar l'estat dels objectes (no la col·lecció) | for-each normal: no hi ha cap problema |
Aquesta última fila importa: for (Material m : cataleg) { m.prestar(); } és perfectament legal. modCount compta canvis estructurals a la col·lecció, no canvis als objectes que conté.
- Els mètodes comuns de
Collection
CollectionTot el que sigui Collection —qualsevol List, Set o Queue— entén aquest vocabulari:
| Mètode | Què fa | Retorna |
|---|---|---|
add(E e) |
Afegeix l'element | boolean: false si la col·lecció el rebutja (duplicat en un Set) |
remove(Object o) |
Elimina la primera aparició, fent servir equals |
boolean: true si hi havia alguna cosa a eliminar |
contains(Object o) |
Hi és?, fent servir equals |
boolean |
size() |
Quants elements hi ha | int |
isEmpty() |
És buida? | boolean |
clear() |
Buida la col·lecció | void |
addAll(Collection c) |
Afegeix tots els d'una altra | boolean: si ha canviat alguna cosa |
removeAll(Collection c) |
Elimina tots els que estiguin a l'altra | boolean |
retainAll(Collection c) |
Conserva només els que estiguin a l'altra | boolean |
containsAll(Collection c) |
Hi són tots els de l'altra? | boolean |
forEach(Consumer) |
Aplica una acció a cada element | void |
removeIf(Predicate) |
Elimina els que compleixin el criteri | boolean |
toArray(T[] a) |
Aboca a un array | T[] |
Dues observacions sobre el valor retornat que resolen problemes reals:
add retorna boolean, i en un Set aquest booleà val or. false significa "ja hi era", així que detectar duplicats és una línia:
Set<String> isbnVistos = new HashSet<>();
if (!isbnVistos.add(llibre.getIsbn())) {
System.out.println("AVIS: ISBN duplicat, alta rebutjada -> " + llibre.getIsbn());
}remove i contains fan servir equals, no ==. D'aquí que 03-09 insistís tant a implementar-lo bé: si la teva classe no sobreescriu equals, contains compararà referències i un objecte "igual" però diferent no es trobarà mai.
List<Fitxa> fitxes = new ArrayList<>();
fitxes.add(new Fitxa("Java Eficac", "Joshua Bloch", 2018));
// funciona perque Fitxa es un record: equals generat i correcte
System.out.println(fitxes.contains(new Fitxa("Java Eficac", "Joshua Bloch", 2018))); // trueI toArray tanca el cercle amb la lliçó anterior:
List<Material> llista = new ArrayList<>(...);
Material[] array = llista.toArray(new Material[0]); // l'idioma estandardEl new Material[0] no és cap malbaratament: li diu al mètode quin tipus d'array ha de crear. Passar new Material[llista.size()] també val i sol ser una mica més lent a les JVM modernes, per curiós que sembli. Fes servir sempre new Material[0].
- Col·leccions immutables i de conveniència
Des de Java 9 hi ha mètodes de fàbrica que creen col·leccions immutables en una expressió:
List<String> empleats = List.of("Marta Ruiz", "Diego Alonso", "Nuria Vidal");
Set<String> tipus = Set.of("Llibre", "Revista", "DVD");
Map<String, Integer> terminis = Map.of("Llibre", 15, "Revista", 7, "DVD", 3);Les seves propietats, que cal conèixer al detall perquè sorprenen:
- Totalment immutables: qualsevol
add,remove,setoclearllançaUnsupportedOperationException. - No admeten
null, ni com a element, ni com a clau, ni com a valor: llancenNullPointerExceptionen crear-les. Set.ofiMap.ofrebutgen duplicats en construir, ambIllegalArgumentException. És intencionat: si escriusSet.of("a", "a")gairebé segur que és un error teu.Map.ofadmet fins a 10 parells. Per a més,Map.ofEntries(Map.entry(k, v), ...).- El seu ordre de recorregut no està garantit i pot canviar entre execucions (a
Set.ofiMap.ofestà deliberadament aleatoritzat perquè ningú no en depengui).List.ofsí que conserva l'ordre, perquè una llista és ordenada per definició.
Són perfectes per a constants, taules fixes i valors de configuració:
public static final Set<String> TIPUS_PRESTABLES = Set.of("Llibre", "Revista", "DVD");
public static final Map<String, Integer> DIES_PER_TIPUS = Map.of("Llibre", 15, "Revista", 7, "DVD", 3);unmodifiableList i la diferència que importa
Collections.unmodifiableList fa una cosa semblant però no igual: crea una vista de només lectura sobre una llista existent.
List<String> original = new ArrayList<>(List.of("Marta Ruiz", "Diego Alonso"));
List<String> vista = Collections.unmodifiableList(original);
vista.add("Nuria Vidal"); // UnsupportedOperationException, com esperaves
original.add("Nuria Vidal"); // permes... i la VISTA tambe canvia
System.out.println(vista); // [Marta Ruiz, Diego Alonso, Nuria Vidal]List.of(...) |
Collections.unmodifiableList(llista) |
List.copyOf(llista) |
|
|---|---|---|---|
| Es pot modificar per la seva pròpia API? | No | No | No |
| Canvia si algú modifica l'original? | No hi ha original | Sí | No: és una còpia |
Admet null |
No | Sí (els que hi hagués) | No |
| Cost en crear-la | O(n) | O(1): no copia res | O(n) |
Per a una còpia defensiva de debò en un getter (el patró de 03-07), unmodifiableList no basta si qui crida pot arribar a la llista original. El correcte és List.copyOf(llista) o Collections.unmodifiableList(new ArrayList<>(llista)).
I hi ha tres utilitats més per a casos freqüents:
List<Material> buida = Collections.emptyList(); // buida, immutable, sense cost
List<String> un = Collections.singletonList("Marta Ruiz"); // exactament un element
Map<String, Integer> buit = Collections.emptyMap();El seu valor és en un consell de disseny que val la pena adoptar ja: no retornis mai null on s'espera una col·lecció. Retorna una col·lecció buida. Qui cridi podrà escriure for (Material m : cataleg.cercar(...)) sense comprovar res, i desapareix tota una família de NullPointerException.
- Com triar la col·lecció correcta
Aquest arbre resol la immensa majoria de les decisions reals:
flowchart TD
A["Necessito guardar diversos elements"] --> B{"Cada element te<br/>una CLAU per buscar-lo?"}
B -- si --> C{"Necessito les claus<br/>en ordre?"}
C -- "no" --> C1["HashMap"]
C -- "ordre d'insercio" --> C2["LinkedHashMap"]
C -- "ordre natural o Comparator" --> C3["TreeMap"]
B -- no --> D{"Hi pot haver<br/>DUPLICATS?"}
D -- no --> E{"Necessito ordre?"}
E -- "no" --> E1["HashSet"]
E -- "ordre d'insercio" --> E2["LinkedHashSet"]
E -- "ordenat" --> E3["TreeSet"]
D -- si --> F{"Com accedeixo<br/>als elements?"}
F -- "per posicio, recorro tot" --> F1["ArrayList"]
F -- "nomes pels extrems" --> G{"L'ordre de sortida es...?"}
G -- "FIFO o LIFO" --> G1["ArrayDeque"]
G -- "per prioritat" --> G2["PriorityQueue"]
I en cinc preguntes, per si prefereixes el format llista:
- Busco per clau? →
Map. (HashMapllevat que necessitis ordre.) - Els duplicats són un error? →
Set. (HashSetllevat que necessitis ordre.) - Necessito posicions i índexs? →
List. (ArrayListgairebé sempre.) - Només afegeixo per un extrem i trec per l'altre? →
Deque/Queue. (ArrayDeque.) - Sempre vull atendre primer el més urgent? →
PriorityQueue.
Un advertiment sobre l'optimització prematura: per a col·leccions de desenes o centenars d'elements, la diferència de rendiment entre implementacions és irrellevant. Tria per semàntica: Set quan els duplicats siguin un error conceptual, Map quan existeixi de debò una clau. Un Set<String> d'ISBN documenta que els ISBN són únics millor que qualsevol comentari. El rendiment importa a partir de desenes de milers d'elements, i allà la taula de l'apartat 4 et donarà la resposta correcta.
- Primera refactorització de BiblioTech
Apliquem ja el que hem après al CatalegArray de la lliçó anterior. Recorda com era:
public class CatalegArray {
private Material[] materials;
private int n;
public void afegir(Material m) {
if (m == null) { return; }
if (n == materials.length) {
materials = Arrays.copyOf(materials, materials.length * 2);
}
materials[n] = m;
n++;
}
public boolean eliminar(String referencia) {
for (int i = 0; i < n; i++) {
if (materials[i].getReferencia().equals(referencia)) {
System.arraycopy(materials, i + 1, materials, i, n - i - 1);
materials[n - 1] = null;
n--;
return true;
}
}
return false;
}
// ...
}I així queda amb col·leccions:
package com.nexussoftware.bibliotech.servei;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import com.nexussoftware.bibliotech.domini.Material;
/** Cataleg de BiblioTech, primera versio sobre el Framework de Colleccions. */
public class Cataleg {
// Declarats per la INTERFICIE, instanciats amb la implementacio per defecte
private final List<Material> materials = new ArrayList<>();
private final Set<String> referencies = new HashSet<>(); // control de duplicats O(1)
/** Afegeix si la referencia no hi era ja. Retorna false si era duplicada. */
public boolean afegir(Material m) {
if (m == null) { return false; }
if (!referencies.add(m.getReferencia())) { // add retorna false si ja hi era
return false;
}
materials.add(m); // sense capacitat, sense comptador, sense copyOf
return true;
}
/** Elimina per referencia. Sense arraycopy i sense nulls per netejar. */
public boolean eliminar(String referencia) {
if (!referencies.remove(referencia)) { return false; }
return materials.removeIf(m -> m.getReferencia().equals(referencia));
}
/** Cerca amb qualsevol criteri, fent servir el Predicate de 04-06. */
public List<Material> cercar(Predicate<Material> criteri) {
List<Material> resultat = new ArrayList<>();
for (Material m : materials) {
if (criteri.test(m)) { resultat.add(m); }
}
return resultat; // sense copyOf(resultat, n): ja te la mida justa
}
public int comptar(Predicate<Material> criteri) {
int n = 0;
for (Material m : materials) {
if (criteri.test(m)) { n++; }
}
return n;
}
/** Ordena el cataleg al lloc amb qualsevol Comparator. */
public void ordenar(Comparator<Material> criteri) {
materials.sort(criteri); // metode propi de List des de Java 8
}
/** Vista de nomes lectura: ningu de fora no pot alterar el cataleg. */
public Collection<Material> llistar() {
return List.copyOf(materials);
}
public int mida() { return materials.size(); }
public boolean buit() { return materials.isEmpty(); }
}Ús:
Cataleg cataleg = new Cataleg();
cataleg.afegir(new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018));
cataleg.afegir(new Llibre("Patrons de Disseny", "Erich Gamma", "978-0000000002", 1994));
cataleg.afegir(new Llibre("Refactoritzacio", "Martin Fowler", "978-0000000003", 1999));
cataleg.afegir(new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"));
if (!cataleg.afegir(new Llibre("Java Eficac (2a ed.)", "Joshua Bloch", "978-0000000001", 2018))) {
System.out.println("AVIS: ISBN 978-0000000001 ja catalogat. Alta rebutjada.");
}
cataleg.ordenar(Comparator.comparing(Material::getTitol));
cataleg.llistar().forEach(m -> System.out.println(" " + m.descriure()));
System.out.println("Materials prestats: " + cataleg.comptar(m -> !m.estaDisponible()));AVIS: ISBN 978-0000000001 ja catalogat. Alta rebutjada. [Llibre] Java Eficac - Joshua Bloch (978-0000000001) [Revista] Java Magazine num. 42 (REV-2024-03) [Llibre] Patrons de Disseny - Erich Gamma (978-0000000002) [Llibre] Refactoritzacio - Martin Fowler (978-0000000003) Materials prestats: 0
Compta el que ha desaparegut: el camp n, la comprovació de capacitat, l'Arrays.copyOf per créixer, el System.arraycopy per esborrar, el null manual, el retall final de l'array de resultats. I compta el que ha aparegut: un Set<String> que garanteix en O(1) que no hi ha ISBN repetits, una cosa que la versió amb arrays no tenia i que hauria costat un bucle O(n) a cada alta.
Continua havent-hi una debilitat, i és deliberada: eliminar i qualsevol cerca per referència recorren la llista sencera, O(n). Amb un Map<String, Material> seria O(1). Això és 05-05.
Errors Habituals i Consells
Declarar per la implementació. ArrayList<Llibre> llista = new ArrayList<>() funciona, però et lliga. Escriu List<Llibre> llista = new ArrayList<>(). Als paràmetres dels teus mètodes, demana el tipus més general que et serveixi: Collection o Iterable si només recorres.
Fer servir tipus crus. List llista = new ArrayList(); compila amb un avís i retorna Object, obligant a castings que fallen en execució. Posa sempre el <Tipus>.
Modificar la col·lecció dins d'un for-each. És la causa número u de ConcurrentModificationException. Fes servir removeIf per a criteris simples, Iterator.remove() per a lògica complexa o recorre una còpia si a més afegeixes.
Confiar en l'ordre d'un HashSet o HashMap. No hi ha ordre garantit i pot canviar en afegir elements o entre versions de Java. Si necessites ordre reproduïble, fes servir les variants Linked o Tree. Un test que depengui de l'ordre d'un HashSet és un test que fallarà algun dia.
Posar en un Set o fer servir com a clau de Map objectes sense equals/hashCode correctes. L'objecte es desarà però no es trobarà mai. A 05-05 en veuràs la demostració exacta i el perquè; de moment, si un objecte va a un Set o a una clau de Map, repassa 03-09.
Esperar que List.of sigui modificable. Retorna una llista immutable. Si necessites modificar-la: new ArrayList<>(List.of(...)).
Confondre unmodifiableList amb una còpia. És una vista: si algú modifica la llista original, la vista canvia. Per a una còpia defensiva real, List.copyOf(llista).
Retornar null en lloc d'una col·lecció buida. Obliga qui crida a comprovar null a cada ús i garanteix NullPointerException el dia que algú se n'oblidi. Retorna List.of() o Collections.emptyList().
Comparar Integer amb == dins de col·leccions. Funciona fins a 127 per la memòria cau d'enters i falla a partir de 128. Fes servir sempre equals, o compara amb intValue().
Consell de rendiment: no optimitzis sense mesurar. Per a 200 elements, ArrayList i LinkedList són indistingibles. Tria per semàntica; el rendiment apareix a partir de desenes de milers, i llavors ja tindràs la taula de l'apartat 4.
Exercicis
Exercici 1: triar la col·lecció correcta
Per a cada requisit de BiblioTech, indica quina interfície i quina implementació faries servir i per què. Escriu també la declaració de la variable.
- El catàleg complet, recorregut sovint i mostrat en l'ordre en què es va donar d'alta.
- Els ISBN ja catalogats, per rebutjar altes duplicades a l'instant.
- Buscar un material per la seva referència sense recórrer el catàleg.
- Els préstecs pendents de processar, atesos per ordre d'arribada.
- Els avisos de venciment, atenent sempre primer el de més dies de retard.
- Els tipus de material que existeixen, sempre llistats alfabèticament.
- Les últimes 10 operacions, per poder desfer la més recent.
- Els dies de préstec per tipus, com a constant del programa.
Exercici 2: purga del catàleg sense ConcurrentModificationException
Escriu una classe PurgaCataleg amb tres mètodes estàtics que rebin un List<Material> i eliminin els materials el títol dels quals estigui buit o la referència dels quals sigui null:
purgarAmbIterator(List<Material>), fent servirIteratorexplícit.purgarAmbRemoveIf(List<Material>), en una línia.purgarSobreCopia(List<Material>), recorrent una còpia.
Afegeix un mètode demostrarError(List<Material>) que provoqui a propòsit la ConcurrentModificationException amb un comentari que expliqui en quin moment exacte saltarà.
Exercici 3: recórrer qualsevol cosa
Escriu una classe d'utilitats Recorreguts amb mètodes que acceptin el tipus més general possible:
int comptar(Iterable<?> origen): compta elements de qualsevol cosa recorrible.String unir(Iterable<Material> materials, String separador): concatena els títols.double sumarTarifes(Collection<Material> materials).List<Material> disponibles(Collection<Material> materials): els que estiguin disponibles, retornant sempre una llista (buida si no n'hi ha cap, mainull).
Demostra que tots funcionen igual amb un ArrayList, un HashSet, un List.of(...) i el keySet() d'un mapa quan escaigui.
Solucions
Solució 1
| # | Requisit | Declaració | Per què |
|---|---|---|---|
| 1 | Catàleg complet | List<Material> cataleg = new ArrayList<>(); |
Ordre d'inserció, duplicats possibles (dos exemplars), recorregut freqüent: accés O(1) i la millor localitat de memòria cau |
| 2 | ISBN catalogats | Set<String> isbnVistos = new HashSet<>(); |
La unicitat és el requisit. add retorna false si ja hi era: detecció en O(1) |
| 3 | Buscar per referència | Map<String, Material> index = new HashMap<>(); |
Existeix una clau natural (la referència). Cerca O(1) enfront de O(n) |
| 4 | Préstecs per ordre d'arribada | Deque<Prestec> pendents = new ArrayDeque<>(); |
FIFO pur: addLast per encuar, pollFirst per atendre. ArrayDeque és la implementació per defecte de cua |
| 5 | Avisos per retard | Queue<Prestec> avisos = new PriorityQueue<>(Comparator.comparingInt(Prestec::diesRetard).reversed()); |
"Sempre el més urgent primer" és exactament la semàntica de PriorityQueue |
| 6 | Tipus alfabètics | SortedSet<String> tipus = new TreeSet<>(); |
Sense duplicats i sempre ordenat, sense ordenar a mà després de cada alta |
| 7 | Últimes 10 operacions | Deque<OperacioCataleg> historial = new ArrayDeque<>(); |
LIFO: push en registrar, pop en desfer. Per limitar a 10, pollLast() quan size() > 10 |
| 8 | Dies per tipus (constant) | static final Map<String, Integer> DIES = Map.of("Llibre", 15, "Revista", 7, "DVD", 3); |
Taula fixa: immutable, expressada en una línia i a prova de modificacions accidentals |
La lliçó transversal de l'exercici: la col·lecció correcta es dedueix de l'enunciat. "Sense repetits" diu Set. "Buscar per" diu Map. "Per ordre d'arribada" diu Queue. "Desfer" diu pila. Si t'ho has de pensar molt, probablement el requisit no està ben formulat.
Solució 2
package com.nexussoftware.bibliotech.servei;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import com.nexussoftware.bibliotech.domini.Material;
public final class PurgaCataleg {
private PurgaCataleg() { }
/** Un material es invalid si no te titol util o no te referencia. */
private static boolean esInvalid(Material m) {
return m == null
|| m.getTitol() == null || m.getTitol().isBlank()
|| m.getReferencia() == null;
}
/** Versio 1: Iterator explicit. Funciona en qualsevol versio de Java. */
public static void purgarAmbIterator(List<Material> cataleg) {
Iterator<Material> it = cataleg.iterator();
while (it.hasNext()) {
Material m = it.next(); // cal desar el que retorna next()
if (esInvalid(m)) {
it.remove(); // elimina l'ULTIM retornat per next()
// it.remove() actualitza expectedModCount: per aixo es segur
}
}
}
/** Versio 2: removeIf. Java 8+. Es la que has d'escriure per defecte. */
public static void purgarAmbRemoveIf(List<Material> cataleg) {
cataleg.removeIf(PurgaCataleg::esInvalid); // referencia a metode estatic (04-06)
}
/**
* Versio 3: recorrer una copia. Necessaria quan dins del bucle
* tambe s'AFEGEIXEN elements a la colleccio original.
*/
public static void purgarSobreCopia(List<Material> cataleg) {
for (Material m : new ArrayList<>(cataleg)) { // es recorre la COPIA
if (esInvalid(m)) {
cataleg.remove(m); // es modifica l'ORIGINAL
}
}
}
/** Versio incorrecta, a proposit, per veure la fallada. */
public static void demostrarError(List<Material> cataleg) {
for (Material m : cataleg) {
if (esInvalid(m)) {
cataleg.remove(m);
// El remove incrementa modCount. A la SEGUENT crida a next()
// l'iterador compara modCount amb el seu expectedModCount, veu que
// difereixen i llanca ConcurrentModificationException.
//
// Cas especial: si l'element eliminat es el PENULTIM, despres de
// l'esborrat hasNext() retorna false, el bucle acaba sense cridar
// next() i NO salta l'excepcio... pero l'ultim element no s'ha
// visitat. Aixo es pitjor que l'excepcio: una fallada silenciosa.
}
}
}
}Les tres versions vàlides fan el mateix amb costos diferents: removeIf i Iterator.remove operen sobre la llista original sense memòria extra; purgarSobreCopia duplica la llista, i només compensa quan el bucle també afegeix elements. La versió de removeIf és a més l'única que es llegeix d'un cop d'ull.
Solució 3
package com.nexussoftware.bibliotech.servei;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import com.nexussoftware.bibliotech.domini.Material;
public final class Recorreguts {
private Recorreguts() { }
/**
* Iterable<?> es el tipus MES GENERAL possible: serveix per a List, Set, Queue,
* keySet(), values()... El '?' significa "de qualsevol tipus d'element":
* no ens importa QUE hi ha a dins perque nomes comptem. La teoria dels
* comodins es veu a 10-01.
*/
public static int comptar(Iterable<?> origen) {
if (origen == null) { return 0; }
int n = 0;
for (Object ignorat : origen) { n++; }
return n;
}
/** Iterable perque nomes recorrem: no necessitem size(), get() ni add(). */
public static String unir(Iterable<Material> materials, String separador) {
if (materials == null) { return ""; }
StringBuilder sb = new StringBuilder();
for (Material m : materials) {
if (m == null) { continue; }
if (sb.length() > 0) { sb.append(separador); }
sb.append(m.getTitol());
}
return sb.toString();
}
/** Collection perque, a mes de recorrer, ens interessa isEmpty(). */
public static double sumarTarifes(Collection<Material> materials) {
if (materials == null || materials.isEmpty()) { return 0.0; }
double total = 0.0;
for (Material m : materials) {
if (m != null) { total += m.getTarifaDiaria(); }
}
return total;
}
/** Retorna SEMPRE una llista, mai null: qui crida pot recorrer sense comprovar. */
public static List<Material> disponibles(Collection<Material> materials) {
if (materials == null) { return List.of(); } // buida i immutable, cost zero
List<Material> resultat = new ArrayList<>();
for (Material m : materials) {
if (m != null && m.estaDisponible()) { resultat.add(m); }
}
return resultat;
}
}Demostració que el tipus general és el que dona la flexibilitat:
Material llibre = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
Material revista = new Revista("Java Magazine", "REV-2024-03", 42, "Mensual");
List<Material> llista = new ArrayList<>(List.of(llibre, revista));
Set<Material> conjunt = new HashSet<>(llista);
List<Material> fixa = List.of(llibre, revista);
Map<String, Material> index = new HashMap<>();
index.put(llibre.getReferencia(), llibre);
index.put(revista.getReferencia(), revista);
System.out.println(Recorreguts.comptar(llista)); // 2
System.out.println(Recorreguts.comptar(conjunt)); // 2
System.out.println(Recorreguts.comptar(fixa)); // 2
System.out.println(Recorreguts.comptar(index.keySet())); // 2 <- un Set<String>
System.out.println(Recorreguts.comptar(index.values())); // 2 <- una Collection<Material>
System.out.println(Recorreguts.unir(index.values(), " | "));
System.out.printf("Suma de tarifes: %.2f EUR%n", Recorreguts.sumarTarifes(conjunt));
System.out.println("Disponibles: " + Recorreguts.disponibles(fixa).size());Un sol mètode, comptar, funciona sobre cinc orígens diferents —incloses les vistes d'un mapa— perquè demana Iterable, el contracte mínim que tots compleixen. És la regla d'or de l'apartat 3 portada a les signatures dels mètodes, i és el que separa una API rígida d'una de reutilitzable. Quan al mòdul 10 coneguis els Streams, aquesta mateixa flexibilitat s'expressarà de manera encara més compacta, però el principi serà el mateix.
Conclusió
Ja tens el mapa complet. Saps que el Framework de Col·leccions s'organitza en tres capes —interfícies que defineixen contractes, implementacions que trien l'estructura de dades i algorismes que operen sobre qualsevol d'elles— i que aquesta separació és el millor exemple de disseny orientat a interfícies del JDK. Coneixes la jerarquia: Iterable → Collection → List/Set/Queue, amb Deque estenent Queue i les variants Sorted/Navigable; i saps per què Map va a part: guarda parells, no elements solts, i add, contains i iterator hi serien ambigus. També saps que es reconnecta a la resta del Framework mitjançant les seves tres vistes: keySet, values i entrySet.
Has adoptat la regla d'or: List<Llibre> cataleg = new ArrayList<>(). Declara per la interfície, instancia la implementació, i demana a les teves signatures el tipus més general que et serveixi. Amb això, canviar d'implementació costa una paraula i els teus mètodes accepten orígens que ni havies previst.
Tens la taula mestra de les deu implementacions que faràs servir la resta de la teva carrera, amb el seu ordre, els seus duplicats, els seus nuls i les seves complexitats, i les quatre respostes per defecte gravades: ArrayList, HashSet, HashMap, ArrayDeque. I tens l'arbre de decisió per arribar a l'elecció correcta en cinc preguntes.
Entens els genèrics com a usuari: <...> és el tipus del que hi ha a dins, dona seguretat en compilació, elimina castings i obliga a Integer en lloc d'int amb un cost de memòria que convé conèixer —la teoria arriba a 10-01—. Saps que el for-each és sucre sintàctic: sobre arrays es tradueix a un for amb índex, i sobre col·leccions a un Iterator amb hasNext/next. I entens a fons la ConcurrentModificationException: el modCount, la política fail-fast, per què és una virtut, per què de vegades no salta i falla pitjor, i les quatre solucions amb el seu criteri d'elecció, començant per removeIf.
Domines el vocabulari comú de Collection —add amb el seu boolean revelador, remove i contains recolzats en equals, addAll, retainAll, forEach, removeIf, toArray(new T[0])— i les col·leccions immutables, amb la diferència precisa entre List.of, Collections.unmodifiableList (una vista) i List.copyOf (una còpia), més el consell que elimina tota una família de fallades: no retornis mai null on s'espera una col·lecció.
I BiblioTech ja ho nota. El Cataleg ha perdut el comptador, la capacitat, el copyOf, l'arraycopy i els null manuals, i ha guanyat una cosa que abans no tenia: un Set<String> que garanteix en temps constant que no hi ha ISBN repetits. Queda una debilitat evident: buscar per referència continua recorrent la llista sencera.
A la lliçó següent, ArrayList, entraràs en la implementació que faràs servir més que cap altra. Veuràs com funciona per dins —l'array intern, la diferència entre capacitat i mida, el redimensionament i per què afegir continua sent O(1) "amortitzat" encara que de vegades copiï un milió d'elements—, la seva API completa amb el parany clàssic de remove(int) enfront de remove(Object) en una List<Integer>, les conversions entre array i llista amb els paranys de cadascuna, i la refactorització definitiva del catàleg de BiblioTech, amb l'abans i el després d'un mètode real.
Curs de Programació en Java
Mòdul 1: Introducció a Java
- Introducció a Java
- Configuració de l'entorn de desenvolupament
- Sintaxi i estructura bàsica
- Variables i tipus de dades
- Operadors
- Entrada i sortida per consola
- El teu primer programa complet: BiblioTech
Mòdul 2: Flux de control
- Sentències condicionals
- Bucles
- Sentències switch
- Break i continue
- Depuració i traces d'execució
- Projecte: menú interactiu de BiblioTech
Mòdul 3: Programació orientada a objectes
- Introducció a la POO
- Classes i objectes
- Mètodes
- Constructors
- Herència
- Polimorfisme
- Encapsulament
- Abstracció
- La classe Object: equals, hashCode i toString
Mòdul 4: Programació orientada a objectes avançada
- Interfícies
- Classes abstractes
- Classes internes
- Classes anònimes
- Expressions lambda
- Interfícies funcionals i referències a mètodes
- Enumeracions i registres
Mòdul 5: Estructures de dades i col·leccions
- Arrays
- El framework de col·leccions
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cua i Deque
- Pila
- Ordenació i cerca en col·leccions
Mòdul 6: Gestió d'excepcions
- Introducció a les excepcions
- Bloc try-catch
- Throw i throws
- Excepcions personalitzades
- Bloc finally
- Try-with-resources i AutoCloseable
- Estratègies de gestió d'errors i logging
Mòdul 7: Entrada/sortida de fitxers
- Lectura de fitxers
- Escriptura de fitxers
- Fluxos de fitxers
- BufferedReader i BufferedWriter
- Serialització
- L'API NIO.2: Path i Files
- Formats d'intercanvi: CSV i Properties
Mòdul 8: Multifil i concurrència
- Introducció al multifil
- Creació de fils
- Cicle de vida d'un fil
- Sincronització
- Utilitats de concurrència
- Col·leccions concurrents i variables atòmiques
- Tasques asíncrones amb CompletableFuture
Mòdul 9: Xarxes
- Introducció a les xarxes
- Sockets
- ServerSocket
- DatagramSocket i DatagramPacket
- URL i HttpURLConnection
- El client HTTP modern
Mòdul 10: Temes avançats
- Genèrics
- Anotacions
- Reflexió
- Característiques de Java 8: Streams i Optional
- Dates i hores amb java.time
- Java 9 i més enllà
- Memòria, recol·lecció de brossa i rendiment
Mòdul 11: Frameworks i llibreries de Java
- Introducció als frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Proves avançades amb Mockito
- Llibreries essencials de l'ecosistema
