LinkedList és la segona implementació de List del JDK i probablement la més malinterpretada de tot el Framework. La frase que es repeteix als tutorials, a les entrevistes de feina i als comentaris de codi és sempre la mateixa: "fes servir ArrayList per accedir per índex i LinkedList per inserir i esborrar, que és O(1)". Aquesta frase és, tal com sona, falsa, i creure-se-la porta a escriure codi considerablement més lent pensant que s'està optimitzant.
En aquesta lliçó entendràs exactament què és una llista doblement enllaçada, quines operacions són realment O(1) i sota quina condició precisa, i per què a la pràctica —fins i tot en els casos que semblen el seu terreny— ArrayList acostuma a guanyar. També veuràs on LinkedList sí que té sentit: no tant com a List, sinó com a Deque, és a dir, com a cua de doble extrem. I acabaràs amb l'únic cas de BiblioTech on la seva semàntica encaixa de debò: la cua de reserves pendents, que es consumeix per un extrem i creix per l'altre.
Aquesta lliçó no va només d'una classe. Va d'aprendre a raonar honestament sobre estructures de dades, distingint el que diu la teoria del que fa el processador.
Contingut
- L'estructura: nodes doblement enllaçats
- Què implica per a la memòria i per a la memòria cau
- El matís que gairebé tothom explica malament
ArrayListenfront deLinkedList, operació per operació- La conclusió honesta i actual
LinkedListcom aDequeListIterator: recorregut bidireccional i inserció- Mesurar el rendiment de manera honesta
- Quan
LinkedListsí que és l'elecció correcta: la cua de reserves - Errors Habituals i Consells
- Exercicis
- L'estructura: nodes doblement enllaçats
Un ArrayList guarda els elements en un bloc contigu de memòria. Una LinkedList no guarda res contigu: guarda una cadena de nodes, cadascun al seu propi racó del heap, units per referències.
Aquest és el node real del JDK, i cap en cinc línies:
private static class Node<E> {
E item; // l'element que transporta
Node<E> next; // referencia al node SEGUENT (null si es l'ultim)
Node<E> prev; // referencia al node ANTERIOR (null si es el primer)
}I la llista en si guarda només tres camps:
transient int size = 0;
transient Node<E> first; // primer node
transient Node<E> last; // ultim nodeflowchart LR
LL["LinkedList<br/>size = 4<br/>first, last"]
N1["null ← prev<br/><b>Java Eficac</b><br/>next →"]
N2["← prev<br/><b>Patrons de Disseny</b><br/>next →"]
N3["← prev<br/><b>Refactoritzacio</b><br/>next →"]
N4["← prev<br/><b>Java Magazine</b><br/>next → null"]
LL -.->|first| N1
LL -.->|last| N4
N1 --> N2
N2 --> N3
N3 --> N4
N4 -.-> N3
N3 -.-> N2
N2 -.-> N1
Les fletxes contínues són els next; les puntejades, els prev. Que hi hagi enllaços en tots dos sentits és el que la fa doblement enllaçada, i té dues conseqüències importants: es pot recórrer cap enrere, i des d'un node es pot eliminar aquest node sense conèixer l'anterior.
D'aquesta estructura se'n dedueixen immediatament les seves dues característiques fonamentals:
No hi ha índexs. Els nodes no estan numerats ni col·locats en posicions calculables. Per arribar a l'element 500 cal començar a first i saltar 500 vegades. No hi ha cap manera de fer-ho més ràpid, i aquí hi ha la debilitat estructural de LinkedList.
No hi ha capacitat. Cada node es crea quan cal i es descarta quan s'elimina. No hi ha redimensionaments, no hi ha còpies, no hi ha array intern per omplir. Afegir l'element un milió costa exactament el mateix que afegir el segon.
Veure com s'insereix un node al mig ho explica tot la resta:
flowchart LR
subgraph abans["ABANS"]
direction LR
A1["Node A"] --> A2["Node B"]
end
subgraph despres["DESPRES d'inserir X entre A i B"]
direction LR
B1["Node A"] --> BX["Node X"]
BX --> B2["Node B"]
end
L'operació consisteix a crear el node X i canviar quatre referències: A.next = X, X.prev = A, X.next = B, B.prev = X. Quatre assignacions. Cap element no es mou, ni el primer ni el milionèsim. Això és el que vol dir "inserció O(1)". I ara ve el matís.
- Què implica per a la memòria i per a la memòria cau
Abans del matís, dos costos que les taules de complexitat no mostren i que a la pràctica pesen més que la mateixa O().
Cost de memòria
En un ArrayList, cada element ocupa una referència a l'array intern: 4 bytes amb compressió de punters, 8 sense.
En una LinkedList, cada element necessita un objecte Node complet:
| Component del node | Bytes aproximats (JVM de 64 bits amb punters comprimits) |
|---|---|
| Capçalera de l'objecte | 12 |
item (referència a l'element) |
4 |
next (referència) |
4 |
prev (referència) |
4 |
| Farciment d'alineació | 4 |
| Total per element | ~28 bytes |
Enfront dels ~4 bytes per element d'un ArrayList ben dimensionat, això són unes set vegades més memòria només en infraestructura, sense comptar els objectes apuntats. Per a un milió d'elements: uns 4 MB enfront d'uns 28 MB. I cada node és un objecte que el recol·lector de brossa ha de rastrejar (mòdul 10-07).
Cost de localitat de memòria cau
Aquest és el factor decisiu i el que més gent ignora. Com explicava 05-01, el processador no llegeix memòria byte a byte: porta blocs de 64 bytes a la seva memòria cau. En un ArrayList, llegir l'element 0 porta també els 15 següents de franc; recórrer la llista és pràcticament lectura seqüencial, el patró que més agrada al maquinari.
En una LinkedList, cada node pot ser a qualsevol lloc del heap. Si els vas crear en ordre potser són a prop, però després d'un temps d'altes i baixes, i després de diverses passades del recol·lector, queden dispersos. Recórrer significa llavors un salt impredictible de memòria per element, i cada salt que falla a la memòria cau costa de l'ordre de cent vegades més que un accés en memòria cau.
flowchart TB
subgraph AL["ArrayList: recorregut sequencial"]
direction LR
X0["e0"] --- X1["e1"] --- X2["e2"] --- X3["e3"] --- X4["e4"]
end
subgraph LL["LinkedList: salts per tot el heap"]
direction LR
Y0["node0"] -.-> Y3["node1"]
Y3 -.-> Y1["node2"]
Y1 -.-> Y4["node3"]
Y4 -.-> Y2["node4"]
end
El resultat mesurat una vegada i una altra: recórrer una LinkedList sol ser entre 2 i 10 vegades més lent que recórrer un ArrayList de la mateixa mida, encara que totes dues operacions siguin O(n). La notació O() ignora les constants, i aquí la constant importa moltíssim.
- El matís que gairebé tothom explica malament
Repetim l'afirmació popular: "inserir i esborrar en LinkedList és O(1)".
El correcte és: inserir i esborrar és O(1) SI JA TENS LA POSICIÓ, és a dir, si ja tens a la mà una referència al node (o ets en un extrem). Arribar a aquesta posició costa O(n).
Mira-t'ho al codi real del JDK:
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size) { linkLast(element); }
else { linkBefore(element, node(index)); } // node(index) es el problema
}
Node<E> node(int index) {
if (index < (size >> 1)) { // si es a la primera meitat
Node<E> x = first;
for (int i = 0; i < index; i++) { x = x.next; } // O(n) salts
return x;
} else { // si es a la segona meitat
Node<E> x = last;
for (int i = size - 1; i > index; i--) { x = x.prev; }
return x;
}
}linkBefore és O(1): quatre assignacions. Però node(index) recorre fins a la meitat de la llista. El mètode complet és O(n). L'optimització de començar per l'extrem més proper només divideix entre dos: continua sent O(n).
Conseqüència directa: aquest bucle, que sembla raonable, és un desastre.
List<Integer> llista = new LinkedList<>();
for (int i = 0; i < 100_000; i++) { llista.add(i); }
// "Insereixo al mig, que en LinkedList es O(1)"... NO
for (int i = 0; i < 1000; i++) {
llista.add(llista.size() / 2, i); // cada crida recorre 50.000 nodes: O(n)
}Mil crides × 50 000 salts = 50 milions de salts de punter. El mateix bucle amb ArrayList faria mil System.arraycopy de 50 000 referències, que és la mateixa O() però executada per una instrucció de còpia de bloc optimitzada pel maquinari: a la pràctica, unes quantes vegades més ràpid.
Quan sí que és O(1) de debò
Només en tres situacions:
1. Als extrems. addFirst, addLast, removeFirst, removeLast accedeixen directament a first i last. Genuïnament O(1), sense recorregut.
LinkedList<String> cua = new LinkedList<>();
cua.addLast("Marta Ruiz"); // O(1) real
cua.removeFirst(); // O(1) real2. Amb un iterador que ja és a la posició.
ListIterator<Material> it = llista.listIterator();
while (it.hasNext()) {
Material m = it.next();
if (m.getTitol().startsWith("Java")) {
it.add(new Nota("Revisar")); // O(1) REAL: l'iterador ja hi es
}
}El recorregut complet és O(n), però cada inserció individual és O(1), i no hi ha desplaçament d'elements. Aquí LinkedList sí que és teòricament superior a ArrayList, on cada add(i, e) desplaçaria la resta.
3. Iterator.remove() durant un recorregut. Mateix raonament: l'iterador ja té el node.
La regla resumida, que convé memoritzar:
A
LinkedListl'operació és barata; arribar al lloc és car. AArrayListarribar al lloc és gratis; l'operació és cara.
I com que "arribar al lloc" és el que fa qualsevol mètode indexat, el resultat net gairebé sempre afavoreix ArrayList.
ArrayList enfront de LinkedList, operació per operació
ArrayList enfront de LinkedList, operació per operació| Operació | ArrayList |
LinkedList |
Qui guanya a la pràctica |
|---|---|---|---|
get(i) / set(i, e) |
O(1) | O(n) | ArrayList, sense discussió |
add(e) al final |
O(1) amortitzat | O(1) | Empat; ArrayList sol ser més ràpid per memòria cau |
add(0, e) al principi |
O(n) | O(1) | LinkedList en teoria; ArrayDeque millor que totes dues |
add(i, e) al mig |
O(n) desplaçament | O(n) recorregut + O(1) | ArrayList: l'arraycopy és més ràpid que saltar nodes |
remove(size()-1) |
O(1) | O(1) | Empat |
remove(0) |
O(n) | O(1) | LinkedList; ArrayDeque millor que totes dues |
remove(i) |
O(n) | O(n) | ArrayList |
remove(Object) |
O(n) | O(n) | ArrayList |
contains / indexOf |
O(n) | O(n) | ArrayList (molt millor memòria cau) |
Recórrer amb for-each |
O(n) ràpid | O(n) lent | ArrayList, 2-10× |
Recórrer amb for indexat |
O(n) | O(n²) | ArrayList; a LinkedList és un error greu |
Inserir amb ListIterator |
O(n) per inserció | O(1) per inserció | LinkedList |
addFirst / pollFirst |
No existeixen a List |
O(1) | LinkedList (o ArrayDeque) |
| Memòria per element | ~4-8 bytes | ~28 bytes | ArrayList |
sort |
O(n log n) directe | O(n log n) + abocar a array i reconstruir | ArrayList |
Presta especial atenció a la fila del for indexat, perquè és un error de rendiment que s'hi cola amb facilitat:
// Sobre una LinkedList de 100.000 elements: cada get(i) recorre fins a i nodes.
// Total: 1 + 2 + 3 + ... + 100.000 = uns 5.000 milions de salts.
for (int i = 0; i < llista.size(); i++) {
processar(llista.get(i)); // O(n^2) DISFRESSAT
}
// Correcte en totes dues implementacions:
for (Material m : llista) { // O(n): l'iterador avanca de node en node
processar(m);
}Si el paràmetre del teu mètode és List i no saps quina implementació t'arribarà, recorre sempre amb for-each. Aquesta és una de les raons per les quals existeix la interfície marcadora RandomAccess (05-03): permet a un algorisme genèric preguntar si l'accés indexat és barat.
if (llista instanceof RandomAccess) {
for (int i = 0; i < llista.size(); i++) { processar(llista.get(i)); }
} else {
for (Material m : llista) { processar(m); }
}Al teu codi no escriuràs això gairebé mai —fes servir for-each i au—, però és útil saber per què existeix.
- La conclusió honesta i actual
És la part de la lliçó que més s'aparta del discurs habitual, així que va amb noms i arguments.
A la pràctica, ArrayList guanya gairebé sempre. Fins i tot en escenaris que semblen afavorir LinkedList, les mesures repetides en JVM modernes donen la victòria a l'array, perquè:
- La localitat de memòria cau domina. La diferència entre un accés a la memòria cau L1 i una fallada que va a memòria principal és de dos ordres de magnitud. Cap avantatge algorísmic no sobreviu a això quan l'O() és la mateixa.
System.arraycopyés una instrucció nativa que copia blocs de memòria a velocitat de maquinari. Desplaçar 10 000 referències contigües és sorprenentment barat; seguir 10 000 punters dispersos, no.- Els nodes pressionen el recol·lector. Un milió d'elements són un milió d'objectes
Nodeper rastrejar, enfront d'un sol array. - Les insercions "al mig" gairebé mai no són al mig de debò. Al codi real s'insereix al final, o s'elimina per criteri amb
removeIf, o s'ordena. Totes aquestes operacions afavoreixen l'ArrayList.
Aquesta és també l'opinió pública de Joshua Bloch, autor de Java Eficaç i coautor del mateix Framework de Col·leccions, que ha arribat a dir que a dia d'avui LinkedList no aporta valor suficient per justificar-ne l'ús general.
Llavors LinkedList no serveix per a res? Serveix, però no com a List: com a Deque. Quan el que vols és una cua o una pila —afegir per un extrem, treure per l'altre— LinkedList compleix amb O(1) real i sense capacitat que gestionar.
I tot i així, en aquest terreny té un competidor millor: ArrayDeque, que veuràs a 05-07, i que és més ràpid i consumeix menys memòria perquè fa servir un array circular. La recomanació oficial del JDK per a cues i piles és ArrayDeque, no LinkedList.
El resum pràctic, sense embuts:
| Necessito... | Fes servir |
|---|---|
| Una llista | ArrayList |
| Una cua o una pila | ArrayDeque (05-07, 05-08) |
Inserir molt al mig recorrent amb ListIterator |
LinkedList (cas rar però legítim) |
Una List que a més sigui Deque a la mateixa variable |
LinkedList |
Una cua que admeti null |
LinkedList (ArrayDeque els rebutja) |
Que continuïs aprenent LinkedList té tres motius sòlids: te la trobaràs en codi existent, és l'exemple canònic de llista enllaçada —una estructura que apareix en mil llocs— i entendre per què no és la resposta t'ensenya a raonar sobre rendiment millor que qualsevol regla memoritzada.
LinkedList com a Deque
LinkedList com a DequeLinkedList implementa dues interfícies alhora, i aquí hi ha el seu tret distintiu:
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.SerializableÉs l'única classe del JDK que és List i Deque simultàniament. Això li dona accés a tota la família d'operacions pels extrems, totes genuïnament O(1):
| Mètode | Què fa | Si és buida |
|---|---|---|
addFirst(e) / offerFirst(e) |
Insereix al principi | — |
addLast(e) / offerLast(e) |
Insereix al final | — |
getFirst() / getLast() |
Consulta sense treure | NoSuchElementException |
peekFirst() / peekLast() |
Consulta sense treure | Retorna null |
removeFirst() / removeLast() |
Treu i retorna | NoSuchElementException |
pollFirst() / pollLast() |
Treu i retorna | Retorna null |
peek() / poll() |
Àlies de peekFirst/pollFirst (semàntica de cua) |
null |
push(e) / pop() |
Àlies d'addFirst/removeFirst (semàntica de pila) |
pop: NoSuchElementException |
La distinció entre les dues famílies —llançar excepció o retornar null— és una decisió de disseny important de Queue i Deque, i s'explica a fons a 05-07. De moment queda't amb la regla: fes servir peek/poll/offer quan la col·lecció buida sigui una situació normal; fes servir getFirst/removeFirst/addFirst quan que estigui buida signifiqui que alguna cosa va malament.
Exemple amb les dues semàntiques sobre la mateixa classe:
LinkedList<String> cua = new LinkedList<>();
// Com a CUA (FIFO): entra pel final, surt pel principi
cua.addLast("Marta Ruiz");
cua.addLast("Diego Alonso");
cua.addLast("Nuria Vidal");
System.out.println(cua.pollFirst()); // Marta Ruiz (la primera que va arribar)
System.out.println(cua); // [Diego Alonso, Nuria Vidal]
// Com a PILA (LIFO): entra i surt pel mateix extrem
LinkedList<String> pila = new LinkedList<>();
pila.push("alta");
pila.push("baixa");
pila.push("modificacio");
System.out.println(pila.pop()); // modificacio (l'ultima que va entrar)
System.out.println(pila); // [baixa, alta]Fixa't en el tipus de la variable: aquí està declarada com a LinkedList perquè necessitem mètodes que List no té. Si només la faràs servir com a cua, el correcte segons la regla d'or de 05-02 és declarar-la per la interfície que reflecteixi el seu ús:
L'ús de cua i pila en detall són les lliçons 05-07 i 05-08.
ListIterator: recorregut bidireccional i inserció
ListIterator: recorregut bidireccional i insercióListIterator és una extensió d'Iterator exclusiva de les llistes, i és la forma correcta d'inserir o substituir elements mentre recorres. La seva API:
| Mètode | Què fa |
|---|---|
hasNext() / next() |
Avançar, com a Iterator |
hasPrevious() / previous() |
Retrocedir |
nextIndex() / previousIndex() |
Posició del següent / anterior |
add(E e) |
Insereix a la posició actual, abans del que retornaria next() |
set(E e) |
Substitueix l'últim retornat per next() o previous() |
remove() |
Elimina l'últim retornat |
La clau conceptual: un ListIterator no assenyala un element, assenyala un forat entre elements (un cursor). next() salta l'element que té a la dreta i en retorna el valor; previous() salta el de la seva esquerra.
flowchart LR
P0["^0"] --- A["A"] --- P1["^1"] --- B["B"] --- P2["^2"] --- C["C"] --- P3["^3"]
Els ^ són les posicions possibles del cursor. Amb el cursor a ^1, next() retorna B i deixa el cursor a ^2; previous() retornaria A i el deixaria a ^0.
Inserir mentre recorres
Aquest és l'ús que justifica la seva existència:
List<String> operacions = new LinkedList<>(
List.of("alta:978-0000000001", "baixa:978-0000000002", "alta:978-0000000003"));
ListIterator<String> it = operacions.listIterator();
while (it.hasNext()) {
String op = it.next();
if (op.startsWith("baixa:")) {
it.add("avis:revisar-" + op.substring(6)); // s'insereix DESPRES de l'actual
}
}
System.out.println(operacions);Tres detalls que cal entendre:
it.add(x)insereix a la posició del cursor, que després denext()és just darrere de l'element retornat. Per això l'avís apareix darrere de la baixa.- L'element inserit no es torna a visitar.
addavança el cursor per damunt del que s'ha inserit, així que no hi ha bucle infinit. Comprova-ho: siaddno ho fes, el nou element s'examinaria i en podria generar un altre, indefinidament. - No hi ha
ConcurrentModificationException. L'iterador és qui modifica, així que actualitza el seuexpectedModCount(05-02).
Intentar el mateix amb un for-each i llista.add(...) donaria l'excepció immediatament. I fer-ho amb índexs sobre un ArrayList obligaria a recalcular la posició després de cada inserció, un generador clàssic de fallades de límit.
Substituir mentre recorres
ListIterator<String> it = noms.listIterator();
while (it.hasNext()) {
String n = it.next();
if (n.isBlank()) {
it.set("(sense nom)"); // substitueix l'ultim retornat per next()
}
}Equivalent a replaceAll quan la condició és simple, però permet lògica arbitrària i decidir element a element.
Recórrer cap enrere
// listIterator(size) colloca el cursor al FINAL
ListIterator<Material> it = cataleg.listIterator(cataleg.size());
while (it.hasPrevious()) {
Material m = it.previous();
System.out.println(m.getTitol());
}Sobre una LinkedList això és eficient gràcies als enllaços prev; sobre un ArrayList també, perquè l'accés indexat és O(1). En tots dos casos és més clar que un for decreixent quan a més necessites inserir o eliminar.
Avís important: barrejar un ListIterator amb modificacions directes de la llista ho trenca tot. Mentre un iterador estigui viu, tots els canvis han de passar per ell.
- Mesurar el rendiment de manera honesta
Compararem les dues implementacions amb un experiment senzill. Abans, un advertiment que cal prendre's seriosament.
Els microbenchmarks en Java són traïdors. La JVM compila el codi a mesura que s'executa (JIT), així que les primeres iteracions són molt més lentes que les següents; el recol·lector de brossa pot saltar enmig de la mesura; i el compilador pot eliminar per complet un bucle el resultat del qual no es fa servir, donant-te temps de zero. L'eina correcta és JMH (Java Microbenchmark Harness), la llibreria oficial d'OpenJDK, que gestiona l'escalfament, les iteracions i el consum de resultats. El que ve a continuació és una aproximació il·lustrativa, útil per veure ordres de magnitud, no per publicar xifres.
package com.nexussoftware.bibliotech.presentacio;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class ComparativaLlistes {
private static final int N = 100_000;
public static void main(String[] args) {
// Escalfament: deixem que el JIT compili abans de mesurar
for (int i = 0; i < 3; i++) { mesurarTot(false); }
System.out.println("=== Mesura (N = " + N + ") ===");
mesurarTot(true);
}
private static void mesurarTot(boolean imprimir) {
List<Integer> array = new ArrayList<>();
List<Integer> enllacada = new LinkedList<>();
long t1 = mesurar(() -> { for (int i = 0; i < N; i++) { array.add(i); } });
long t2 = mesurar(() -> { for (int i = 0; i < N; i++) { enllacada.add(i); } });
long t3 = mesurar(() -> { long s = 0; for (int i = 0; i < N; i++) { s += array.get(i); } consumir(s); });
long t4 = mesurar(() -> { long s = 0; for (Integer v : array) { s += v; } consumir(s); });
long t5 = mesurar(() -> { long s = 0; for (Integer v : enllacada) { s += v; } consumir(s); });
List<Integer> a2 = new ArrayList<>(array);
List<Integer> l2 = new LinkedList<>(array);
long t6 = mesurar(() -> { for (int i = 0; i < 20_000; i++) { a2.add(0, i); } });
long t7 = mesurar(() -> { for (int i = 0; i < 20_000; i++) { l2.add(0, i); } });
if (imprimir) {
System.out.printf("add al final ArrayList %6d ms | LinkedList %6d ms%n", t1, t2);
System.out.printf("get(i) indexat ArrayList %6d ms | LinkedList (NO es mesura: O(n^2))%n", t3);
System.out.printf("recorrer foreach ArrayList %6d ms | LinkedList %6d ms%n", t4, t5);
System.out.printf("add(0, e) x20000 ArrayList %6d ms | LinkedList %6d ms%n", t6, t7);
}
}
private static long mesurar(Runnable tasca) {
long inici = System.nanoTime();
tasca.run();
return (System.nanoTime() - inici) / 1_000_000; // a millisegons
}
/** Impedeix que el JIT elimini el bucle per considerar el seu resultat inutil. */
private static void consumir(long valor) {
if (valor == Long.MIN_VALUE) { System.out.print(""); }
}
}Resultats típics en una màquina d'escriptori, amb l'ordre de magnitud com a única informació fiable:
| Operació (N = 100 000) | ArrayList |
LinkedList |
Comentari |
|---|---|---|---|
add al final |
~3 ms | ~5 ms | Empat pràctic; l'array guanya per memòria cau |
get(i) en bucle indexat |
~1 ms | minuts | O(n²): ni es mesura |
Recórrer amb for-each |
~1 ms | ~6 ms | 6× més lent, mateixa O() |
add(0, e) × 20 000 |
~120 ms | ~2 ms | Aquí sí que guanya LinkedList... |
addFirst × 20 000 en ArrayDeque |
~1 ms | — | ...però ArrayDeque les guanya totes dues |
L'última fila resumeix la lliçó: l'únic cas clar a favor de LinkedList és inserir pel principi, i per a això existeix una estructura millor.
I una conclusió metodològica igual de valuosa: mesura abans d'optimitzar. Si algú canvia un ArrayList per un LinkedList "perquè insereix més ràpid" sense haver mesurat, hi ha moltes probabilitats que hagi empitjorat el programa.
- Quan
LinkedList sí que és l'elecció correcta: la cua de reserves
LinkedList sí que és l'elecció correcta: la cua de reservesArriba el cas real de BiblioTech. Quan un material està prestat, un empleat el pot reservar. Les reserves formen una cua: s'atenen per ordre d'arribada, s'afegeixen pel final i es consumeixen pel principi. És el patró exacte en què una llista enllaçada és adequada: zero accés per índex, tot pels extrems.
Primer, la classe Reserva. Com que és una dada amb identitat pròpia i estat (atesa), la modelem com a classe, no com a record (04-07):
package com.nexussoftware.bibliotech.domini;
/** Sollicitud de reserva d'un material ja prestat. */
public class Reserva {
public static final int PRIORITAT_NORMAL = 5;
private final Empleat empleat;
private final Material material;
private final int diaSollicitud;
private final int prioritat; // 1 = maxima urgencia, 10 = minima
private boolean atesa;
public Reserva(Empleat empleat, Material material, int diaSollicitud, int prioritat) {
this.empleat = empleat;
this.material = material;
this.diaSollicitud = Math.max(diaSollicitud, 0);
this.prioritat = (prioritat < 1 || prioritat > 10) ? PRIORITAT_NORMAL : prioritat;
this.atesa = false;
}
public Reserva(Empleat empleat, Material material, int diaSollicitud) {
this(empleat, material, diaSollicitud, PRIORITAT_NORMAL);
}
public Empleat getEmpleat() { return empleat; }
public Material getMaterial() { return material; }
public int getDiaSollicitud() { return diaSollicitud; }
public int getPrioritat() { return prioritat; }
public boolean estaAtesa() { return atesa; }
public void marcarAtesa() { this.atesa = true; }
/** Dies que fa que espera aquesta reserva. */
public int diesEnEspera(int diaActual) {
return Math.max(0, diaActual - diaSollicitud);
}
@Override
public String toString() {
return String.format("Reserva[%s -> %s, dia %d, prioritat %d%s]",
empleat.getNom(), material.getTitol(), diaSollicitud, prioritat,
atesa ? ", ATESA" : "");
}
}I la cua de reserves d'un material:
package com.nexussoftware.bibliotech.servei;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import com.nexussoftware.bibliotech.domini.Empleat;
import com.nexussoftware.bibliotech.domini.Material;
import com.nexussoftware.bibliotech.domini.Reserva;
/**
* Cua FIFO de reserves d'UN material.
*
* LinkedList es aqui una eleccio justificada: nomes s'opera als extrems
* (addLast en reservar, pollFirst en atendre) i mai per index. Tot i aixi,
* un ArrayDeque (05-07) seria mes rapid; es fa servir LinkedList perque a mes
* necessitem recorrer i eliminar per criteri, i perque illustra el cas.
*/
public class CuaReserves {
private final Material material;
private final LinkedList<Reserva> pendents = new LinkedList<>();
public CuaReserves(Material material) {
this.material = material;
}
/** Encuar pel final. O(1) real: es toca el node 'last'. */
public void reservar(Empleat empleat, int dia) {
pendents.addLast(new Reserva(empleat, material, dia));
}
/** Consultar qui es el seguent SENSE treure'l. Retorna null si no hi ha ningu. */
public Reserva seguent() {
return pendents.peekFirst();
}
/**
* Aten la primera reserva de la cua. O(1) real: es toca el node 'first'.
* Retorna null si no n'hi havia cap de pendent.
*/
public Reserva atendreSeguent(int dia) {
Reserva r = pendents.pollFirst(); // poll: null si es buida, no excepcio
if (r != null) {
r.marcarAtesa();
material.prestar();
}
return r;
}
/** Cancella la reserva mes recent d'un empleat. Recorre des del final. */
public boolean cancellarUltimaDe(Empleat empleat) {
Iterator<Reserva> it = pendents.descendingIterator(); // de last a first
while (it.hasNext()) {
if (it.next().getEmpleat().equals(empleat)) {
it.remove(); // O(1) REAL: l'iterador ja te el node
return true;
}
}
return false;
}
/** Caduca les reserves que fa massa dies que esperen. */
public int caducar(int diaActual, int diesMaxims) {
int abans = pendents.size();
pendents.removeIf(r -> r.diesEnEspera(diaActual) > diesMaxims);
return abans - pendents.size();
}
/** Avanca una reserva urgent al principi de la cua. O(1) real. */
public void avancar(Reserva urgent) {
pendents.remove(urgent); // O(n): cal localitzar-la
pendents.addFirst(urgent); // O(1): al principi
}
public List<Reserva> llistar() { return new ArrayList<>(pendents); }
public int enEspera() { return pendents.size(); }
public boolean hiHaPendents() { return !pendents.isEmpty(); }
/** Posicio a la cua (1 = el seguent). 0 si l'empleat no te reserva. */
public int posicioDe(Empleat empleat) {
int posicio = 1;
for (Reserva r : pendents) { // for-each, MAI get(i) en una LinkedList
if (r.getEmpleat().equals(empleat)) { return posicio; }
posicio++;
}
return 0;
}
}Ús complet:
Empleat marta = new Empleat("Marta Ruiz", "EMP-001");
Empleat diego = new Empleat("Diego Alonso", "EMP-002");
Empleat nuria = new Empleat("Nuria Vidal", "EMP-003");
Material javaEficac = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
javaEficac.prestar(); // ja esta prestat a una altra persona
CuaReserves cua = new CuaReserves(javaEficac);
cua.reservar(marta, 100);
cua.reservar(diego, 101);
cua.reservar(nuria, 103);
System.out.println("En espera: " + cua.enEspera());
System.out.println("Seguent: " + cua.seguent());
System.out.println("Posicio de Nuria: " + cua.posicioDe(nuria));
javaEficac.retornar();
Reserva atesa = cua.atendreSeguent(110);
System.out.println("Atesa: " + atesa);
System.out.println("Ara es el torn de: " + cua.seguent().getEmpleat().getNom());
int caducades = cua.caducar(130, 20); // mes de 20 dies esperant
System.out.println("Reserves caducades: " + caducades);
System.out.println("Queden en espera: " + cua.enEspera());En espera: 3 Seguent: Reserva[Marta Ruiz -> Java Eficac, dia 100, prioritat 5] Posicio de Nuria: 3 Atesa: Reserva[Marta Ruiz -> Java Eficac, dia 110, prioritat 5, ATESA] Ara es el torn de: Diego Alonso Reserves caducades: 1 Queden en espera: 1
Fixa't en tres decisions deliberades d'aquest codi:
posicioDerecorre ambfor-each, mai ambget(i). Sobre unaLinkedList, el bucle indexat seria O(n²).cancellarUltimaDefa servirdescendingIteratoriit.remove(): l'iterador ja té el node, així que l'eliminació és O(1) real. És el cas legítim de l'apartat 3.avancarés honest sobre el seu cost: elremove(Object)és O(n) perquè cal localitzar la reserva; només l'addFirstés O(1). Aquest mètode seria el primer candidat a revisar si la cua creixés molt, i unaPriorityQueue(05-07) resoldria millor el problema de les urgències.
I una nota final de disseny: encara que LinkedList funciona bé aquí, la declaració canònica d'una cua és Deque<Reserva> pendents = new ArrayDeque<>(). S'ha fet servir LinkedList perquè a més necessitem removeIf i recorreguts, i perquè aquest era el seu cas natural; a 05-07 veuràs la versió amb ArrayDeque i per què sol ser millor.
Errors Habituals i Consells
Recórrer una LinkedList amb for indexat. for (int i = 0; i < llista.size(); i++) llista.get(i) és O(n²): cada get recorre la cadena des d'un extrem. Amb 100 000 elements passa de mil·lisegons a minuts. Fes servir sempre for-each o un iterador.
Creure que add(i, e) és O(1) a LinkedList. La inserció sí que ho és; arribar a l'índex i no. El mètode complet és O(n). Només els extrems i les insercions via iterador són O(1) de debò.
Triar LinkedList "perquè insereix més ràpid" sense mesurar. A la pràctica perd gairebé sempre per localitat de memòria cau i per consum de memòria. Comença per ArrayList i canvia només amb dades a la mà.
Fer servir LinkedList com a cua quan existeix ArrayDeque. ArrayDeque és més ràpid i ocupa menys. L'única raó per preferir LinkedList és necessitar List i Deque a la mateixa variable, o admetre elements null.
Confondre get/remove amb peek/poll. Sobre una llista buida, getFirst() i removeFirst() llancen NoSuchElementException; peekFirst() i pollFirst() retornen null. Tria segons si la cua buida és normal o és un error.
Modificar la llista mentre hi ha un ListIterator viu. Qualsevol llista.add, llista.remove o llista.clear directe invalida l'iterador i provoca ConcurrentModificationException a l'operació següent. Mentre l'iterador existeixi, tot hi passa per ell.
Oblidar que it.add(x) no torna a visitar el que s'ha inserit. És el que evita el bucle infinit, i és el correcte; però si esperaves que el nou element s'avalués, no passarà.
Ordenar una LinkedList sovint. sort aboca a un array, ordena i reconstrueix tota la cadena de nodes. Si ordenaràs amb freqüència, fes servir ArrayList; si necessites ordre permanent, TreeSet o PriorityQueue (05-06, 05-07).
Consell: declara per la interfície que reflecteixi l'ús. Si és una llista, List<X> l = new ArrayList<>(). Si és una cua, Deque<X> d = new ArrayDeque<>(). Declarar LinkedList<X> només es justifica quan necessites les dues cares alhora.
Consell: LinkedList és un magnífic exercici mental. Implementar-la a mà —amb nodes, prev i next— ensenya més sobre punters i estructures de dades que molts llibres. Però en producció, ArrayList.
Exercicis
Exercici 1: historial d'operacions amb mida màxima
Escriu HistorialOperacions que guardi les últimes N operacions del catàleg (cadenes com ara "alta:978-0000000001"), fent servir una LinkedList com a estructura interna:
void registrar(String operacio): afegeix al final i, si se supera el màxim, elimina la més antiga (la primera).String ultima()iString mesAntiga(): sense eliminar-les, retornantnullsi és buit.List<String> enOrdreInvers(): de la més recent a la més antiga, ambdescendingIterator.int eliminarPerPrefix(String prefix): elimina totes les que comencin per aquest prefix i retorna quantes n'ha tret.
Justifica en comentaris quines operacions són O(1) reals i quines no.
Exercici 2: inserir separadors amb ListIterator
Escriu un mètode estàtic void inserirSeparadors(List<Material> cataleg) que recorri un catàleg ja ordenat per tipus i insereixi, abans del primer material de cada tipus, un Material especial de tipus separador (fes servir un Llibre amb títol "--- LLIBRES ---" o crea una petita classe Separador extends Material).
Ha de funcionar amb ListIterator i sense provocar ConcurrentModificationException ni bucles infinits. Explica per què la inserció no es torna a visitar.
Afegeix després una versió inserirSeparadorsMalament que intenti el mateix amb un for-each i documenta què falla exactament.
Exercici 3: comparativa honesta
Escriu BancDeProves que compari ArrayList i LinkedList en quatre escenaris, amb escalfament previ i amb protecció contra l'eliminació de codi pel JIT:
- Afegir N elements al final.
- Recórrer amb
for-eachsumant. - Inserir 10 000 elements pel principi.
- Eliminar per criteri amb
removeIf.
Imprimeix una taula amb els temps i escriu, en un comentari final, la teva conclusió raonada i l'advertiment sobre JMH.
Solucions
Solució 1
package com.nexussoftware.bibliotech.servei;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
/**
* Historial acotat d'operacions del cataleg.
*
* LinkedList encaixa aqui: totes les operacions habituals son pels extrems.
*/
public class HistorialOperacions {
private final LinkedList<String> operacions = new LinkedList<>();
private final int maxim;
public HistorialOperacions(int maxim) {
this.maxim = Math.max(maxim, 1);
}
public void registrar(String operacio) {
if (operacio == null || operacio.isBlank()) { return; }
operacions.addLast(operacio); // O(1) REAL: toca el node 'last'
if (operacions.size() > maxim) {
operacions.removeFirst(); // O(1) REAL: toca el node 'first'
}
// En un ArrayList, aquest removeFirst seria remove(0): O(n) pel desplacament.
// Aquest es exactament l'escenari on la llista enllacada te sentit.
}
/** peekLast retorna null si es buida; getLast llancaria NoSuchElementException. */
public String ultima() { return operacions.peekLast(); }
public String mesAntiga() { return operacions.peekFirst(); }
/**
* descendingIterator recorre de 'last' a 'first' aprofitant els enllacos prev.
* Es O(n) en total, amb un sol salt de punter per element.
*/
public List<String> enOrdreInvers() {
List<String> resultat = new ArrayList<>(operacions.size());
Iterator<String> it = operacions.descendingIterator();
while (it.hasNext()) {
resultat.add(it.next());
}
return resultat;
}
/**
* removeIf fa UNA passada O(n) i cada eliminacio individual es O(1),
* perque l'iterador intern ja esta situat al node.
* Un bucle amb remove(Object) seria O(n^2) i a mes fallaria amb
* ConcurrentModificationException si es fes dins d'un for-each.
*/
public int eliminarPerPrefix(String prefix) {
if (prefix == null) { return 0; }
int abans = operacions.size();
operacions.removeIf(op -> op.startsWith(prefix));
return abans - operacions.size();
}
public int mida() { return operacions.size(); }
public boolean buit() { return operacions.isEmpty(); }
}Prova:
HistorialOperacions h = new HistorialOperacions(3);
h.registrar("alta:978-0000000001");
h.registrar("alta:978-0000000002");
h.registrar("baixa:978-0000000001");
h.registrar("alta:978-0000000003"); // supera el maxim: cau la mes antiga
System.out.println("Mes antiga: " + h.mesAntiga()); // alta:978-0000000002
System.out.println("Ultima: " + h.ultima()); // alta:978-0000000003
System.out.println("Invers: " + h.enOrdreInvers());
System.out.println("Baixes eliminades: " + h.eliminarPerPrefix("baixa:"));
System.out.println("Queden: " + h.mida());Aquest és el cas d'ús ideal de LinkedList: una finestra lliscant. Cada registre afegeix per un extrem i treu per l'altre, totes dues O(1) reals. Amb ArrayList, cada remove(0) desplaçaria tota la llista. Dit això, la resposta professional per a una finestra lliscant és ArrayDeque (05-07), que fa el mateix amb un array circular, sense nodes i sense pressió sobre el recol·lector.
Solució 2
package com.nexussoftware.bibliotech.presentacio;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import com.nexussoftware.bibliotech.domini.Material;
public final class SeparadorsCataleg {
private SeparadorsCataleg() { }
/** Material fictici que nomes serveix com a titol de seccio. */
private static Material separador(String tipus) {
return new Llibre("--- " + tipus.toUpperCase() + " ---", "", "SEP-" + tipus, 2000);
}
/**
* Insereix un separador abans del primer material de cada tipus.
* Requereix que el cataleg estigui ORDENAT per tipus.
*/
public static void inserirSeparadors(List<Material> cataleg) {
if (cataleg == null || cataleg.isEmpty()) { return; }
ListIterator<Material> it = cataleg.listIterator();
String tipusAnterior = null;
while (it.hasNext()) {
Material m = it.next(); // el cursor queda DESPRES de m
String tipus = m.getTipus();
if (!tipus.equals(tipusAnterior)) {
// Retrocedim per inserir ABANS de m
it.previous(); // el cursor torna a estar ABANS de m
it.add(separador(tipus)); // insereix i avanca el cursor
it.next(); // tornem a passar sobre m
tipusAnterior = tipus;
}
}
}
/**
* Versio INCORRECTA, per documentar la fallada.
*/
public static void inserirSeparadorsMalament(List<Material> cataleg) {
String tipusAnterior = null;
for (Material m : cataleg) {
if (!m.getTipus().equals(tipusAnterior)) {
cataleg.add(separador(m.getTipus()));
// FALLADA: el for-each usa un Iterator intern que va desar el modCount
// en comencar. Aquest add l'incrementa. A la SEGUENT crida a next()
// l'iterador detecta la discrepancia i llanca
// ConcurrentModificationException.
//
// A mes, encara que no hi hagues error, l'add afegiria SEMPRE AL FINAL,
// no a la posicio correcta: el resultat seria erroni igualment.
tipusAnterior = m.getTipus();
}
}
}
public static void main(String[] args) {
List<Material> cataleg = new LinkedList<>(List.of(
new Dvd("Refactoritzacio en directe", "DVD-0007", 95),
new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018),
new Llibre("Patrons de Disseny", "Erich Gamma", "978-0000000002", 1994),
new Revista("Java Magazine", "REV-2024-03", 42, "Mensual")
));
inserirSeparadors(cataleg);
cataleg.forEach(m -> System.out.println(m.getTitol()));
}
}--- DVD --- Refactoritzacio en directe --- LLIBRE --- Java Eficac Patrons de Disseny --- REVISTA --- Java Magazine
La seqüència previous() → add(...) → next() mereix una explicació acurada. Després d'it.next() el cursor és darrere del material actual, però el separador ha d'anar davant. previous() retrocedeix el cursor a la posició anterior (i retorna el mateix material, que descartem). add insereix aquí i deixa el cursor darrere del que s'ha inserit, és a dir, encara davant del material. next() el torna a saltar per continuar.
Aquest avanç automàtic del cursor després d'add és exactament el que impedeix el bucle infinit: l'element inserit mai no és retornat per next(). Si add no ho fes, el next() següent retornaria el separador, el tipus del qual tampoc no coincidiria amb tipusAnterior, i s'inseriria un altre separador, indefinidament.
I sobre la versió incorrecta, hi ha dues fallades apilades: la ConcurrentModificationException i, més de fons, que cataleg.add(x) insereix al final, no on estàs recorrent. És un bon recordatori que el for-each no coneix la seva pròpia posició.
Solució 3
package com.nexussoftware.bibliotech.presentacio;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Supplier;
/**
* Comparativa illustrativa entre ArrayList i LinkedList.
*
* ADVERTIMENT: aixo NO es un benchmark rigoros. La JVM compila el codi
* sobre la marxa (JIT), el recollidor de brossa pot intervenir enmig
* de la mesura i el compilador pot eliminar bucles el resultat dels quals no
* es faci servir. Per mesurar de debo cal fer servir JMH (Java Microbenchmark
* Harness), l'eina oficial d'OpenJDK. Aquestes xifres serveixen per veure
* ORDRES DE MAGNITUD, res mes.
*/
public class BancDeProves {
private static final int N = 100_000;
private static final int INSERCIONS = 10_000;
private static long escut = 0; // impedeix que el JIT elimini els bucles
public static void main(String[] args) {
System.out.println("Escalfant la JVM (3 passades sense mesurar)...");
for (int i = 0; i < 3; i++) { executar(false); }
System.out.println("\n=== Resultats (N = " + N + ") ===");
System.out.printf("%-28s %12s %12s%n", "Escenari", "ArrayList", "LinkedList");
System.out.println("-".repeat(54));
executar(true);
System.out.println("\n(escut = " + escut + ", ignora'l: nomes evita que el JIT esborri els bucles)");
}
private static void executar(boolean imprimir) {
fila("1. add al final", imprimir,
() -> afegirAlFinal(new ArrayList<>()),
() -> afegirAlFinal(new LinkedList<>()));
List<Integer> a1 = omplir(new ArrayList<>());
List<Integer> l1 = omplir(new LinkedList<>());
fila("2. recorrer for-each", imprimir,
() -> recorrer(a1), () -> recorrer(l1));
fila("3. inserir a l'inici", imprimir,
() -> inserirAlInici(omplir(new ArrayList<>())),
() -> inserirAlInici(omplir(new LinkedList<>())));
fila("4. removeIf parells", imprimir,
() -> purgar(omplir(new ArrayList<>())),
() -> purgar(omplir(new LinkedList<>())));
}
private static void fila(String etiqueta, boolean imprimir,
Supplier<Long> ambArray, Supplier<Long> ambEnllacada) {
long ta = ambArray.get();
long tl = ambEnllacada.get();
if (imprimir) {
System.out.printf("%-28s %9d ms %9d ms%n", etiqueta, ta, tl);
}
}
private static List<Integer> omplir(List<Integer> llista) {
for (int i = 0; i < N; i++) { llista.add(i); }
return llista;
}
private static long afegirAlFinal(List<Integer> llista) {
long t = System.nanoTime();
for (int i = 0; i < N; i++) { llista.add(i); }
return ms(t);
}
private static long recorrer(List<Integer> llista) {
long t = System.nanoTime();
long suma = 0;
for (Integer v : llista) { suma += v; }
escut += suma; // consumim el resultat
return ms(t);
}
private static long inserirAlInici(List<Integer> llista) {
long t = System.nanoTime();
for (int i = 0; i < INSERCIONS; i++) { llista.add(0, i); }
return ms(t);
}
private static long purgar(List<Integer> llista) {
long t = System.nanoTime();
llista.removeIf(v -> v % 2 == 0);
escut += llista.size();
return ms(t);
}
private static long ms(long iniciNano) {
return (System.nanoTime() - iniciNano) / 1_000_000;
}
}Sortida típica (els valors absoluts varien molt segons la màquina; el que importa és la relació):
=== Resultats (N = 100000) === Escenari ArrayList LinkedList ------------------------------------------------------ 1. add al final 3 ms 6 ms 2. recorrer for-each 1 ms 7 ms 3. inserir a l'inici 118 ms 2 ms 4. removeIf parells 2 ms 11 ms
Conclusió raonada. LinkedList guanya en un sol escenari, el 3, i el guanya per molt: inserir pel principi és el seu terreny natural. En els altres tres perd, i en el recorregut —l'operació més freqüent en qualsevol programa real— és unes set vegades més lenta amb la mateixa complexitat O(n): la diferència és purament localitat de memòria cau.
L'escenari 4 és especialment revelador. removeIf és O(n) en totes dues i elimina en O(1) en totes dues (per iterador), però LinkedList continua perdent, perquè recórrer els nodes dispersos domina el temps total.
I l'observació decisiva: si afegeixes al banc de proves un ArrayDeque amb addFirst, l'escenari 3 baixa a aproximadament 1 ms, guanyant a totes dues. És a dir, fins i tot en l'únic cas on LinkedList guanya ArrayList, no és la millor eina disponible. Per això la recomanació pràctica és: ArrayList per a llistes, ArrayDeque per a cues i piles, LinkedList gairebé mai.
Repeteix l'advertiment amb cada número que vegis: per a decisions de rendiment reals, mesura amb JMH sobre la teva càrrega de treball concreta.
Conclusió
Ja saps què és una llista doblement enllaçada: una cadena de nodes amb prev, item i next, sense array, sense capacitat i sense índexs. Entens el que això implica de debò: ~28 bytes per element enfront dels ~4 d'un ArrayList, i nodes dispersos pel heap que destrueixen la localitat de memòria cau i fan que recórrer-la sigui unes quantes vegades més lent encara que la complexitat sigui la mateixa O(n). Has vist al codi del mateix JDK que node(index) recorre la cadena, i amb això has desmuntat el mite: inserir i esborrar és O(1) només si ja tens la posició, cosa que passa únicament als extrems i a través d'un iterador; arribar-hi per índex costa O(n).
Tens la taula comparativa operació per operació i la conclusió honesta que se'n deriva: ArrayList guanya gairebé sempre, perquè System.arraycopy és una instrucció de bloc nativa, perquè la memòria cau domina, perquè els nodes pressionen el recol·lector i perquè les insercions "al mig" gairebé mai no ho són. Saps que el mateix coautor del Framework la considera prescindible avui, i que el seu nínxol real no és ser una List sinó ser un Deque, terreny en què ArrayDeque la supera. També saps llegir l'error de rendiment que més s'hi cola: un for indexat sobre una LinkedList és O(n²) disfressat.
Coneixes la seva doble naturalesa List + Deque i tota la família d'operacions pels extrems, amb la distinció entre la variant que llança excepció (getFirst, removeFirst) i la que retorna null (peekFirst, pollFirst) —que 05-07 desenvoluparà—. I domines el ListIterator: el cursor que viu entre elements, el recorregut bidireccional, add i set durant el recorregut, per què el que s'insereix no es torna a visitar i per què és l'única forma correcta d'inserir mentre recorres.
Saps mesurar amb honestedat: escalfar la JVM, consumir els resultats perquè el JIT no elimini els bucles, desconfiar dels números absoluts i recórrer a JMH quan la decisió importi de debò. I per damunt de tot, has adoptat el criteri professional: mesura abans d'optimitzar, perquè canviar ArrayList per LinkedList "perquè insereix més ràpid" és, gairebé sempre, empitjorar el programa.
BiblioTech ha guanyat la classe Reserva i una CuaReserves que encua pel final, atén pel principi, cancel·la mitjançant iterador descendent en O(1) real i caduca les reserves antigues amb removeIf. És l'únic punt del projecte on una llista enllaçada estava justificada, i tot i així has vist per què a 05-07 la reescriuràs amb ArrayDeque.
I queda una debilitat que ja s'està fent insuportable. Cataleg.eliminar(referencia) recorre la llista sencera. GestorPrestecs.retornar(referencia, dia) recorre la llista sencera. CuaReserves.posicioDe(empleat) recorre la llista sencera. Cada vegada que BiblioTech ha de trobar alguna cosa pel seu identificador, mira un per un. Amb cinc materials és gratis; amb cinquanta mil, cada cerca són cinquanta mil comparacions, i agrupar préstecs per empleat amb bucles imbricats és O(n²).
A la lliçó següent, HashMap, això s'acaba. Veuràs l'estructura que troba un element per la seva clau en temps constant sense importar quants n'hi hagi: com funciona la funció hash, què són els cubells, què passa quan dues claus col·lideixen, per què el factor de càrrega és 0,75 i què passa en un rehash. I allà es complirà per fi la promesa que es va fer al mòdul 3: veuràs amb una demostració pràctica per què equals i hashCode havien d'anar sempre junts, i què li passa exactament a un objecte que entra en un mapa amb un dels dos mal implementat.
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
