Fins ara les col·leccions d'aquest mòdul responien a dues preguntes: "hi és?" (Set, Map) i "on és?" (List). Les cues responen a una tercera, que és la que governa qualsevol sistema que processi feina pendent: "a qui li toca ara?".
Aquesta pregunta apareix arreu. La cua de reserves d'un material a BiblioTech. Els treballs pendents d'un servidor d'impressió. Els paquets que arriben a una targeta de xarxa. Les tasques d'un planificador. Els nodes per visitar d'un recorregut en amplada. Els avisos de venciment ordenats per urgència. En tots aquests casos hi ha elements que entren, esperen i surten seguint una política concreta, i l'estructura de dades que modela aquesta política és una cua.
En aquesta lliçó veuràs la interfície Queue amb la seva semàntica FIFO, la seva curiosa duplicitat de mètodes —dues formes de fer el mateix amb comportaments diferents davant l'error—, la interfície Deque que obre els dos extrems, ArrayDeque amb la seva memòria intermèdia circular i la seva condició d'opció per defecte actual, i la PriorityQueue, que serveix l'element més urgent en lloc del més antic i amaga una sorpresa que atrapa molta gent. Al final, BiblioTech tindrà la seva cua de reserves reescrita com cal i una cua de prioritat per als avisos de retard.
Contingut
- FIFO: la semàntica d'una cua
- La interfície
Queuei les seves dues famílies de mètodes ArrayDeque: la memòria intermèdia circularDeque: la cua de doble extremArrayDequeenfront deLinkedListPriorityQueue: atendre el més urgent- La sorpresa de l'iterador de
PriorityQueue BlockingQueuei el patró productor-consumidor- Casos d'ús reals: memòries intermèdies, planificació i BFS
- Aplicació a BiblioTech
- Errors Habituals i Consells
- Exercicis
- FIFO: la semàntica d'una cua
Una cua (queue) és una col·lecció amb una política de sortida definida. La política clàssica és FIFO: First In, First Out, "el primer que entra és el primer que surt". Exactament com la cua del supermercat.
flowchart LR
E["entrada<br/>(offer / addLast)"] --> C4["Nuria"]
C4 --> C3["Diego"]
C3 --> C2["Marta"]
C2 --> S["sortida<br/>(poll / removeFirst)"]
Els elements entren per la cua (el final) i surten pel cap (el principi). El primer a arribar és el primer a ser atès: és una política justa, que garanteix que ningú no esperi indefinidament.
Compara-la amb les altres dues polítiques que veuràs en aquest mòdul:
| Política | Qui surt primer | Estructura | Lliçó |
|---|---|---|---|
| FIFO | El que fa més temps que espera | Queue / Deque |
Aquesta |
| LIFO | L'últim que va entrar | Pila (Deque) |
05-08 |
| Per prioritat | El més urgent, sense importar quan va arribar | PriorityQueue |
Aquesta, apartat 6 |
La diferència essencial amb una List és que una cua no ofereix accés arbitrari. No hi ha get(i). Només pots mirar el primer i treure el primer. Aquesta restricció no és una mancança: és la garantia que l'ordre de procés es respecta. Si el codi pogués colar-se al mig de la cua, la política deixaria d'estar garantida per l'estructura.
import java.util.ArrayDeque;
import java.util.Queue;
Queue<String> pendents = new ArrayDeque<>();
pendents.offer("Marta Ruiz"); // entra
pendents.offer("Diego Alonso");
pendents.offer("Nuria Vidal");
System.out.println(pendents.peek()); // Marta Ruiz (mira sense treure)
System.out.println(pendents.poll()); // Marta Ruiz (treu)
System.out.println(pendents.poll()); // Diego Alonso
System.out.println(pendents.size()); // 1
- La interfície
Queue i les seves dues famílies de mètodes
Queue i les seves dues famílies de mètodesQueue estén Collection i declara sis mètodes, que en realitat són tres operacions per duplicat:
| Operació | Llança excepció si falla | Retorna valor especial |
|---|---|---|
| Inserir | add(e) → IllegalStateException |
offer(e) → false |
| Extreure | remove() → NoSuchElementException |
poll() → null |
| Consultar | element() → NoSuchElementException |
peek() → null |
Per què existeixen dues versions de cadascuna?
Perquè hi ha dues situacions diferents i mereixen tractaments diferents.
La família que llança excepció (add, remove, element) considera que la fallada és una anomalia. Si esperes que la cua tingui elements i no en té, alguna cosa va malament a la teva lògica i vols assabentar-te'n immediatament.
La família que retorna un valor especial (offer, poll, peek) considera que la fallada és una situació normal. Una cua buida no és cap error: és que no hi ha feina pendent.
Queue<Reserva> cua = new ArrayDeque<>();
// Bucle de proces: que la cua es buidi es NORMAL, es la condicio de parada
Reserva r;
while ((r = cua.poll()) != null) { // poll retorna null: idiomatic i net
atendre(r);
}
// Consulta puntual on buida SERIA un error de logica
Reserva seguent = cua.remove(); // NoSuchElementException si es buidaA més, offer té sentit en cues acotades (amb capacitat màxima), on inserir pot fallar legítimament perquè no hi cap. ArrayDeque i LinkedList no tenen límit, així que el seu offer sempre retorna true; però les BlockingQueue del mòdul 8 sí que en tenen, i allà la diferència importa molt.
La regla pràctica
| Situació | Fes servir |
|---|---|
| Bucle que consumeix fins a buidar la cua | poll() |
| Comprovar si hi ha feina pendent | peek() |
| Afegir a una cua sense límit | offer() o add(), indistint |
| Afegir a una cua acotada | offer(), i mira el resultat |
| La cua buida indica una fallada de lògica | remove() / element() |
Recomanació general: fes servir offer, poll i peek. Eviten excepcions per a situacions normals i fan el codi més robust. Les variants amb excepció són útils quan vols que un estat inesperat es manifesti en lloc de propagar un null silenciós.
Un avís sobre peek i poll: si la cua admetés elements null, no podries distingir "és buida" de "el primer element és null". Aquesta és exactament la raó per la qual ArrayDeque i PriorityQueue prohibeixen els null, com veuràs de seguida.
ArrayDeque: la memòria intermèdia circular
ArrayDeque: la memòria intermèdia circularArrayDeque és la implementació per defecte de cues i piles en Java modern. Per dins és un array circular, i entendre aquesta idea explica per què és tan ràpid.
El problema que resol és aquest: en un ArrayList, treure el primer element obliga a desplaçar tots els altres, O(n). Com evitar-ho sense fer servir nodes enllaçats? La resposta: no moure els elements; moure els índexs.
ArrayDeque manté un array i dos índexs, head i tail. Extreure pel cap és incrementar head. Inserir pel final és escriure a tail i incrementar-lo. Ningú no es mou.
flowchart TB
subgraph estat1["Estat inicial: head=0, tail=3"]
direction LR
A0["[0] Marta"]
A1["[1] Diego"]
A2["[2] Nuria"]
A3["[3] -"]
A4["[4] -"]
A5["[5] -"]
end
subgraph estat2["Despres de 2 poll: head=2, tail=3"]
direction LR
B0["[0] -"]
B1["[1] -"]
B2["[2] Nuria"]
B3["[3] -"]
B4["[4] -"]
B5["[5] -"]
end
subgraph estat3["Despres de 4 offer: tail FA LA VOLTA a 1"]
direction LR
C0["[0] Ana"]
C1["[1] -"]
C2["[2] Nuria"]
C3["[3] Luis"]
C4["[4] Eva"]
C5["[5] Pau"]
end
estat1 --> estat2 --> estat3
Quan tail arriba al final de l'array, fa la volta al principi, sempre que allà hi hagi lloc lliure. D'aquí el nom "circular": l'array es comporta com un anell. El càlcul és tan simple com a HashMap:
I pel mateix motiu que a HashMap, la capacitat és sempre una potència de dos: permet substituir el mòdul per un AND de bits.
Quan l'array s'omple, es duplica i es copien els elements, amb el mateix raonament de cost amortitzat que a ArrayList (05-03).
El resultat són aquestes complexitats:
| Operació | ArrayDeque |
|---|---|
offerFirst / offerLast |
O(1) amortitzat |
pollFirst / pollLast |
O(1) |
peekFirst / peekLast |
O(1) |
contains(o) |
O(n) |
remove(Object) |
O(n) |
size() |
O(1) |
I les seves dues restriccions, totes dues deliberades:
No admet null. Perquè fa servir null internament com a marca de cel·la buida, i perquè poll() i peek() retornen null per indicar "cua buida". Permetre elements null faria ambigu aquest senyal. Intentar-ne inserir un llança NullPointerException.
No és sincronitzada. Per a ús concurrent hi ha les BlockingQueue i ConcurrentLinkedDeque del mòdul 8.
Deque: la cua de doble extrem
Deque: la cua de doble extremDeque (es pronuncia "dec", de Double Ended QUEue) estén Queue i permet inserir, extreure i consultar pels dos extrems. És la interfície més versàtil del Framework: serveix com a cua FIFO i com a pila LIFO.
flowchart LR
IF["addFirst<br/>offerFirst"] --> D["DEQUE"]
D --> RF["removeFirst / pollFirst<br/>getFirst / peekFirst"]
IL["addLast<br/>offerLast"] --> D
D --> RL["removeLast / pollLast<br/>getLast / peekLast"]
La taula completa dels seus dotze mètodes principals:
| Operació | Extrem | Llança excepció | Valor especial |
|---|---|---|---|
| Inserir | Cap | addFirst(e) |
offerFirst(e) |
| Inserir | Cua | addLast(e) |
offerLast(e) |
| Extreure | Cap | removeFirst() |
pollFirst() |
| Extreure | Cua | removeLast() |
pollLast() |
| Consultar | Cap | getFirst() |
peekFirst() |
| Consultar | Cua | getLast() |
peekLast() |
I a més hereta els mètodes de Queue i afegeix els de pila, que són àlies dels anteriors:
| Mètode heretat o àlies | Equival a | Semàntica |
|---|---|---|
add(e) / offer(e) |
addLast(e) / offerLast(e) |
Cua |
remove() / poll() |
removeFirst() / pollFirst() |
Cua |
element() / peek() |
getFirst() / peekFirst() |
Cua |
push(e) |
addFirst(e) |
Pila |
pop() |
removeFirst() |
Pila |
Els tres modes d'ús, sobre la mateixa classe:
// Com a CUA (FIFO): entra pel final, surt pel principi
Deque<String> cua = new ArrayDeque<>();
cua.offerLast("Marta Ruiz");
cua.offerLast("Diego Alonso");
System.out.println(cua.pollFirst()); // Marta Ruiz
// Com a PILA (LIFO): entra i surt pel mateix extrem
Deque<String> pila = new ArrayDeque<>();
pila.push("alta");
pila.push("baixa");
System.out.println(pila.pop()); // baixa (l'ultima que va entrar)
// Com a DEQUE de debo: els dos extrems
Deque<String> doble = new ArrayDeque<>();
doble.offerFirst("urgent"); // es cola al principi
doble.offerLast("normal"); // espera el seu torn al final
System.out.println(doble.pollFirst()); // urgentAquest tercer mode és el que dona nom a l'estructura i resol casos reals: una cua de treball on les tasques urgents s'insereixen per davant, un historial que creix per un costat i es retalla per l'altre, un algorisme que necessita mirar i consumir per tots dos extrems.
Deque també ofereix descendingIterator(), que recorre de la cua al cap, i removeFirstOccurrence/removeLastOccurrence per eliminar aparicions concretes.
I un advertiment important: Deque no és una List. No té get(i), set(i, e) ni indexOf. Si necessites accés per índex, no volies un Deque.
ArrayDeque enfront de LinkedList
ArrayDeque enfront de LinkedListTotes dues implementen Deque. La comparació tanca la discussió oberta a 05-04:
| Aspecte | ArrayDeque |
LinkedList |
|---|---|---|
| Estructura interna | Array circular | Nodes doblement enllaçats |
| Memòria per element | ~4-8 bytes (una referència) | ~28 bytes (objecte Node) |
| Localitat de memòria cau | Excel·lent (contigua) | Dolenta (dispersa) |
offerFirst / offerLast |
O(1) amortitzat | O(1) |
pollFirst / pollLast |
O(1) | O(1) |
| Recorregut | Ràpid | 2-10× més lent |
| Pressió sobre el recol·lector | Un sol array | Un objecte per element |
Admet null |
No | Sí |
Implementa List |
No | Sí |
| Recomanació oficial | Sí, com a cua i pila | Només si necessites List + Deque |
La documentació del mateix JDK és explícita: "aquesta classe [ArrayDeque] és probablement més ràpida que Stack quan es fa servir com a pila, i més ràpida que LinkedList quan es fa servir com a cua".
Tria ArrayDeque llevat que necessitis null o la interfície List a la mateixa variable. No hi ha més casos.
I la conclusió pràctica que arrosseguem des de 05-03:
| Necessito... | Implementació |
|---|---|
| Una llista | ArrayList |
| Un conjunt | HashSet |
| Un mapa | HashMap |
| Una cua o una pila | ArrayDeque |
| Una cua per prioritat | PriorityQueue |
PriorityQueue: atendre el més urgent
PriorityQueue: atendre el més urgentUna PriorityQueue trenca el FIFO: no atén qui fa més temps que espera, sinó el més prioritari. És l'estructura d'un servei d'urgències, d'un planificador de tasques o —a BiblioTech— d'una llista d'avisos on primer es crida qui acumula més dies de retard.
import java.util.PriorityQueue;
import java.util.Queue;
// Per ordre natural: el MENOR surt primer
Queue<Integer> cua = new PriorityQueue<>();
cua.offer(30);
cua.offer(10);
cua.offer(20);
System.out.println(cua.poll()); // 10
System.out.println(cua.poll()); // 20
System.out.println(cua.poll()); // 30Per defecte fa servir l'ordre natural (Comparable) i serveix primer el menor. Amb un Comparator pots definir qualsevol criteri, reprenent tot 04-06:
// Els prestecs amb MES dies de retard primer
Queue<Prestec> avisos = new PriorityQueue<>(
Comparator.comparingInt((Prestec p) -> p.diesRetard(diaActual)).reversed());
// Les reserves mes urgents primer i, a igualtat, les mes antigues
Queue<Reserva> reserves = new PriorityQueue<>(
Comparator.comparingInt(Reserva::getPrioritat)
.thenComparingInt(Reserva::getDiaSollicitud));Com funciona: el monticle binari
PriorityQueue no manté tots els elements ordenats —això costaria massa—. Fa servir un monticle binari (binary heap), un arbre binari gairebé complet amb una única regla, anomenada propietat de monticle:
Tot node és menor o igual que els seus fills.
D'aquí es dedueix que l'arrel és sempre el mínim, que és l'única cosa que necessitem per saber a qui li toca.
flowchart TB
R["10<br/>(arrel = minim)"]
A["20"]
B["15"]
C["40"]
D["25"]
E["30"]
R --> A
R --> B
A --> C
A --> D
B --> E
Fixa't que l'ordre no és total: el 15 és a la dreta del 20, encara que sigui menor. L'única garantia és la relació pare-fill, i amb això n'hi ha prou.
El monticle es guarda en un array, sense nodes ni referències: el fill esquerre de l'índex i és a 2i+1 i el dret a 2i+2. Això li dona una excel·lent localitat de memòria cau.
Les operacions funcionen així:
offer(e): es col·loca l'element al final de l'array i es fa "surar cap amunt" intercanviant-lo amb el seu pare mentre sigui menor. Com que l'arbre té alçada log n, el cost és O(log n).poll(): es pren l'arrel (el mínim), es posa l'últim element al seu lloc i es fa "enfonsar cap avall" intercanviant-lo amb el menor dels seus fills. També O(log n).peek(): ésarray[0]. O(1).
| Operació | PriorityQueue |
|---|---|
offer(e) |
O(log n) |
poll() |
O(log n) |
peek() |
O(1) |
contains(o) |
O(n) |
remove(Object) |
O(n) |
| Recórrer en ordre de prioritat | O(n log n) buidant-la amb poll |
I les seves restriccions:
- No admet
null: no es pot comparar ambnull. - Exigeix
ComparableoComparator. Sense cap dels dos, la primera inserció llançaClassCastException. - No és estable: dos elements amb la mateixa prioritat surten en ordre arbitrari. Si t'importa, afegeix un criteri de desempat al comparador (per exemple, l'ordre d'arribada).
- La sorpresa de l'iterador de
PriorityQueue
PriorityQueueAquest és el detall que atrapa gairebé tothom la primera vegada:
Queue<Integer> cua = new PriorityQueue<>();
cua.offer(30);
cua.offer(10);
cua.offer(20);
cua.offer(5);
System.out.println(cua); // [5, 10, 20, 30] o [5, 10, 20, 30]... depen
for (int n : cua) {
System.out.print(n + " "); // 5 10 20 30 ... o NO
}L'iterador d'una PriorityQueue no recorre en ordre de prioritat. Recorre l'array intern tal com és, i aquest array només compleix la propietat de monticle, no un ordre total. El mateix val per a toString(), per a forEach i per a toArray().
Amb aquestes dades:
Queue<Integer> cua = new PriorityQueue<>();
for (int n : new int[]{ 50, 40, 30, 20, 10 }) { cua.offer(n); }
System.out.println("toString: " + cua); // [10, 20, 40, 50, 30] <- NO ordenat
System.out.print("iterador: ");
cua.forEach(n -> System.out.print(n + " ")); // 10 20 40 50 30
System.out.print("\npoll: ");
while (!cua.isEmpty()) { System.out.print(cua.poll() + " "); } // 10 20 30 40 50L'única garantia és que peek() i poll() retornen l'element de més prioritat. Tota la resta és l'estat intern del monticle.
Com recórrer-la correctament
Consumint-la amb poll:
while (!cua.isEmpty()) {
Prestec p = cua.poll(); // en ordre de prioritat garantit
processar(p);
}
// ...pero la cua queda buidaSobre una còpia, si necessites conservar-la:
Queue<Prestec> copia = new PriorityQueue<>(cua); // copia el monticle
while (!copia.isEmpty()) {
processar(copia.poll());
}
// la cua original continua intactaAbocant a una llista i ordenant-la, si la recorreràs diverses vegades:
List<Prestec> ordenats = new ArrayList<>(cua);
ordenats.sort(cua.comparator()); // el mateix criteri de la cuaI una alternativa a considerar: si el que necessites és una col·lecció sempre ordenada i recorrible en ordre, TreeSet (05-06) és millor opció que PriorityQueue. La PriorityQueue està optimitzada per a "dona'm el següent", no per a "recorre'm sencera".
| Necessito | Estructura |
|---|---|
| Extreure sempre el més prioritari | PriorityQueue |
| Recórrer sempre en ordre, i consultar rangs | TreeSet |
| Ordenar una vegada, al final | List + sort (05-09) |
BlockingQueue i el patró productor-consumidor
BlockingQueue i el patró productor-consumidorHi ha una família de cues pensada perquè diversos fils es comuniquin: les BlockingQueue (ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue). El seu tret distintiu són dues operacions que esperen:
take(): si la cua és buida, el fil es bloqueja fins que algú hi insereixi alguna cosa.put(e): si la cua és plena, el fil es bloqueja fins que algú en tregui alguna cosa.
Sobre elles es construeix el patró de concurrència més clàssic que existeix, el productor-consumidor:
flowchart LR
P1["Productor 1"] --> Q["BlockingQueue<br/>(cua compartida)"]
P2["Productor 2"] --> Q
Q --> C1["Consumidor 1"]
Q --> C2["Consumidor 2"]
Un o diversos fils produeixen feina i la dipositen a la cua; un o diversos la consumeixen. La cua fa d'amortidor: absorbeix els pics de producció i desacobla els ritmes de tots dos costats, de manera que cap no ha de conèixer l'altre ni esperar activament.
Els seus avantatges són clars: els productors no es preocupen de si hi ha consumidors lliures, els consumidors no consulten contínuament si hi ha feina (la cua els adorm i els desperta), i la capacitat màxima de la cua fa de control de flux natural: si els consumidors no donen l'abast, la cua s'omple i els productors frenen sols.
A BiblioTech, un cas natural seria un procés nocturn que calcula multes: un fil recorre els préstecs i els diposita a la cua, i diversos fils calculen i imprimeixen els avisos.
La implementació real de tot això és el mòdul 8, que cobreix fils, sincronització, ExecutorService i col·leccions concurrents. Aquí només necessites saber que existeix, que es recolza en la mateixa interfície Queue que acabes d'aprendre, i que mai no has de compartir un ArrayDeque o una PriorityQueue entre fils sense sincronització: no són segures per a concurrència.
- Casos d'ús reals: memòries intermèdies, planificació i BFS
Memòries intermèdies
Una cua entre dos processos de velocitat diferent absorbeix les diferències de ritme. Un lector ràpid diposita línies a la cua i un processador lent les consumeix, sense que cap esperi l'altre més del necessari. És la base dels fluxos amb BufferedReader (mòdul 7) i de les cues de missatgeria.
Una variant útil és la memòria intermèdia circular acotada, que descarta el més antic en omplir-se:
Deque<String> ultimesOperacions = new ArrayDeque<>();
void registrar(String operacio) {
ultimesOperacions.offerLast(operacio);
if (ultimesOperacions.size() > 100) {
ultimesOperacions.pollFirst(); // O(1): descarta la mes antiga
}
}Amb un ArrayList això seria O(n) per cada registre; amb un Deque és O(1).
Planificació
Una cua de tasques pendents:
Queue<Runnable> tasques = new ArrayDeque<>();
tasques.offer(() -> System.out.println("Calcular multes"));
tasques.offer(() -> System.out.println("Enviar avisos"));
tasques.offer(() -> System.out.println("Generar informe"));
Runnable tasca;
while ((tasca = tasques.poll()) != null) {
tasca.run(); // s'executen en ordre d'arribada
}Si a més hi ha urgències, PriorityQueue amb un comparador per prioritat.
Recorregut en amplada (BFS)
És l'ús algorísmic més important de les cues. El recorregut en amplada (Breadth-First Search) explora una estructura per nivells: primer els veïns directes, després els veïns dels veïns, i així successivament. Garanteix trobar el camí més curt en nombre de passos.
L'esquema és sempre el mateix: una cua de nodes per visitar i un conjunt de ja visitats (Set, de 05-06, per no repetir ni entrar en cicles).
Un exemple petit a BiblioTech: materials relacionats ("qui va llegir això també va llegir allò"), i volem les recomanacions ordenades per proximitat.
package com.nexussoftware.bibliotech.servei;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Recomanador {
/** Graf: referencia -> referencies relacionades. */
private final Map<String, List<String>> relacionats;
public Recomanador(Map<String, List<String>> relacionats) {
this.relacionats = relacionats;
}
/**
* Recorregut en amplada: retorna les referencies assolibles des d'"origen"
* fins a "profunditatMaxima" salts, en ordre de PROXIMITAT.
*/
public List<String> recomanar(String origen, int profunditatMaxima) {
List<String> resultat = new ArrayList<>();
Set<String> visitats = new HashSet<>();
Deque<String> perVisitar = new ArrayDeque<>();
Deque<Integer> profunditats = new ArrayDeque<>();
perVisitar.offerLast(origen);
profunditats.offerLast(0);
visitats.add(origen);
while (!perVisitar.isEmpty()) {
String actual = perVisitar.pollFirst(); // FIFO: per nivells
int profunditat = profunditats.pollFirst();
if (profunditat > 0) { resultat.add(actual); } // l'origen no es recomana
if (profunditat >= profunditatMaxima) { continue; }
for (String vei : relacionats.getOrDefault(actual, List.of())) {
if (visitats.add(vei)) { // add retorna false si ja hi era (05-06)
perVisitar.offerLast(vei);
profunditats.offerLast(profunditat + 1);
}
}
}
return resultat;
}
}Ús:
Map<String, List<String>> graf = Map.of(
"978-0000000001", List.of("978-0000000002", "978-0000000003"), // Java Eficac
"978-0000000002", List.of("978-0000000003", "DVD-0007"), // Patrons
"978-0000000003", List.of("DVD-0007"), // Refactoritzacio
"DVD-0007", List.of("REV-2024-03")
);
Recomanador r = new Recomanador(graf);
System.out.println(r.recomanar("978-0000000001", 1)); // veins directes
System.out.println(r.recomanar("978-0000000001", 3)); // fins a 3 salts, per proximitatQue la cua sigui FIFO és el que garanteix l'ordre per nivells: tots els nodes a distància 1 es processen abans que qualsevol a distància 2. Si canviessis la cua per una pila (push/pop), el mateix codi faria un recorregut en profunditat, amb resultats completament diferents. Aquesta comparació la veuràs a 05-08.
- Aplicació a BiblioTech
La cua de reserves, ara amb Deque
A 05-04 vas escriure CuaReserves sobre LinkedList i ja es va anunciar que ArrayDeque seria millor. Aquí tens la versió definitiva:
package com.nexussoftware.bibliotech.servei;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
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, sobre ArrayDeque.
*
* Enfront de la versio amb LinkedList de 05-04: mateix O(1) als extrems,
* pero amb un array circular en lloc de nodes -> menys memoria, millor
* localitat de memoria cau i cap pressio sobre el recollidor.
*/
public class CuaReserves {
private final Material material;
private final Deque<Reserva> pendents = new ArrayDeque<>();
public CuaReserves(Material material) {
this.material = material;
}
/** Encuar pel final: O(1) amortitzat. */
public void reservar(Empleat empleat, int dia) {
pendents.offerLast(new Reserva(empleat, material, dia));
}
/** Reserva urgent: es cola per davant. Nomes un Deque permet aixo. */
public void reservarUrgent(Empleat empleat, int dia) {
pendents.offerFirst(new Reserva(empleat, material, dia, 1));
}
/** peekFirst: null si no hi ha ningu. No llanca mai excepcio. */
public Reserva seguent() {
return pendents.peekFirst();
}
/** pollFirst: aten i treu. O(1). */
public Reserva atendreSeguent(int dia) {
Reserva r = pendents.pollFirst();
if (r != null) {
r.marcarAtesa();
material.prestar();
}
return r;
}
/** Cancella la reserva mes recent d'un empleat, recorrent des del final. */
public boolean cancellarUltimaDe(Empleat empleat) {
Iterator<Reserva> it = pendents.descendingIterator();
while (it.hasNext()) {
if (it.next().getEmpleat().equals(empleat)) {
it.remove();
return true;
}
}
return false;
}
/** Caduca les reserves que fa massa temps que esperen. */
public int caducar(int diaActual, int diesMaxims) {
int abans = pendents.size();
pendents.removeIf(r -> r.diesEnEspera(diaActual) > diesMaxims);
return abans - pendents.size();
}
/** Posicio a la cua (1 = el seguent). 0 si no te reserva. */
public int posicioDe(Empleat empleat) {
int posicio = 1;
for (Reserva r : pendents) { // un Deque NO te get(i): sempre for-each
if (r.getEmpleat().equals(empleat)) { return posicio; }
posicio++;
}
return 0;
}
public List<Reserva> llistar() { return new ArrayList<>(pendents); }
public int enEspera() { return pendents.size(); }
public boolean hiHaPendents() { return !pendents.isEmpty(); }
}La cua d'avisos per prioritat
I ara una PriorityQueue que atén sempre el préstec amb més dies de retard:
package com.nexussoftware.bibliotech.servei;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;
import com.nexussoftware.bibliotech.domini.Gravetat;
import com.nexussoftware.bibliotech.domini.Prestec;
/** Avisos de venciment atesos per urgencia, no per ordre d'arribada. */
public class CuaAvisos {
private final Queue<Prestec> avisos;
private final int diaActual;
public CuaAvisos(int diaActual) {
this.diaActual = diaActual;
// Mes dies de retard primer; a igualtat, el prestec mes antic.
// El desempat fa l'ordre DETERMINISTA: sense ell, dos prestecs amb
// el mateix retard sortirien en ordre arbitrari.
this.avisos = new PriorityQueue<>(
Comparator.comparingInt((Prestec p) -> diesRetard(p)).reversed()
.thenComparingInt(Prestec::getDiaPrestec));
}
private int diesRetard(Prestec p) {
return p.getMaterial().calcularDiesRetard(diaActual - p.getDiaPrestec());
}
/** Encua nomes el que esta realment vencut. O(log n). */
public void encuar(Prestec p) {
if (p != null && !p.estaRetornat() && p.estaVencut(diaActual)) {
avisos.offer(p);
}
}
public void encuarTots(List<Prestec> prestecs) {
for (Prestec p : prestecs) { encuar(p); }
}
/** El mes urgent, sense treure'l. O(1). */
public Prestec mesUrgent() {
return avisos.peek();
}
/**
* Processa TOTS els avisos en ordre d'urgencia.
*
* IMPORTANT: cal buidar la cua amb poll(). Recorrer-la amb for-each
* o imprimir el seu toString() donaria l'ordre del monticle intern, que NO
* es l'ordre de prioritat.
*/
public int processarTots() {
int processats = 0;
Prestec p;
while ((p = avisos.poll()) != null) { // idioma de l'apartat 2
int retard = diesRetard(p);
Gravetat g = p.getMaterial().classificarGravetat(diaActual - p.getDiaPrestec());
System.out.printf("[%-10s] %-16s %-24s %3d dies %6.2f EUR%n",
g, p.getEmpleat().getNom(), p.getMaterial().getTitol(),
retard, p.calcularMulta(diaActual));
processats++;
}
return processats;
}
/** Els N mes urgents, SENSE buidar la cua: es treballa sobre una copia. */
public List<Prestec> topUrgents(int quants) {
Queue<Prestec> copia = new PriorityQueue<>(avisos); // copia el monticle
List<Prestec> resultat = new ArrayList<>();
for (int i = 0; i < quants && !copia.isEmpty(); i++) {
resultat.add(copia.poll());
}
return resultat;
}
public int pendents() { return avisos.size(); }
}Ús conjunt:
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);
// --- Cua de reserves (FIFO) ---
javaEficac.prestar();
CuaReserves reserves = new CuaReserves(javaEficac);
reserves.reservar(marta, 100);
reserves.reservar(diego, 101);
reserves.reservarUrgent(nuria, 102); // es cola per davant
System.out.println("Seguent: " + reserves.seguent().getEmpleat().getNom());
System.out.println("Posicio de Marta: " + reserves.posicioDe(marta));
// --- Cua d'avisos (per prioritat) ---
List<Prestec> prestecs = List.of(
new Prestec(new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018), marta, 100),
new Prestec(new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"), diego, 120),
new Prestec(new Dvd("Refactoritzacio en directe", "DVD-0007", 95), nuria, 125),
new Prestec(new Llibre("Patrons de Disseny", "Erich Gamma", "978-0000000002", 1994), diego, 110)
);
CuaAvisos avisos = new CuaAvisos(140);
avisos.encuarTots(prestecs);
System.out.println("\nAvisos pendents: " + avisos.pendents());
System.out.println("Mes urgent: " + avisos.mesUrgent().getMaterial().getTitol());
System.out.println("\n--- Processant per urgencia ---");
avisos.processarTots();Seguent: Nuria Vidal Posicio de Marta: 2 Avisos pendents: 4 Mes urgent: Java Magazine --- Processant per urgencia --- [GREU ] Diego Alonso Java Magazine 13 dies 1.30 EUR [GREU ] Nuria Vidal Refactoritzacio en directe 12 dies 6.00 EUR [GREU ] Marta Ruiz Java Eficac 25 dies 6.25 EUR [GREU ] Diego Alonso Patrons de Disseny 15 dies 3.75 EUR
Observa que l'ordre no és el d'inserció ni el de dies de préstec: és el de dies de retard descendent, calculat segons el termini propi de cada tipus de material. Una revista amb 13 dies de retard és més urgent que un llibre amb 25, perquè el termini d'una revista és de 7 dies i el d'un llibre de 15. La PriorityQueue aplica aquesta lògica sense que el codi de procés hagi de saber res.
Errors Habituals i Consells
Recórrer una PriorityQueue amb for-each esperant ordre de prioritat. No el dona: recorre el monticle intern. El mateix amb toString(), forEach i toArray(). L'única forma és buidar-la amb poll(), o abocar a una llista i ordenar-la.
Inserir null en un ArrayDeque o en una PriorityQueue. NullPointerException. És deliberat: poll() i peek() fan servir null per senyalar "buida". Si necessites guardar absències, replanteja el model o fes servir Optional (10-04).
Confondre remove() amb poll(). Sobre una cua buida, remove() llança NoSuchElementException i poll() retorna null. Tria segons si la cua buida és normal o és un error.
Fer servir LinkedList com a cua. Funciona, però ArrayDeque és més ràpid i consumeix molta menys memòria. La recomanació oficial és ArrayDeque.
Buscar get(i) en un Deque. No existeix: un Deque no és una List. Si necessites accés per índex, has triat l'estructura equivocada.
Fer servir contains o remove(Object) en una cua dins d'un bucle. Tots dos són O(n) a ArrayDeque i PriorityQueue. Si necessites buscar sovint, mantén a més un Set o un Map de suport.
Comparator sense desempat en una PriorityQueue. No perdràs elements —això només passa a TreeSet (05-06)—, però l'ordre entre empatats serà arbitrari i no reproduïble entre execucions. Afegeix un criteri de desempat si l'ordre ha de ser determinista.
Modificar un element ja encuat en una PriorityQueue. Si canvies el camp pel qual s'ordena, el monticle no es reorganitza: l'element queda en una posició incorrecta i l'ordre de sortida deixa de ser fiable. Treu-lo, modifica'l i torna a encuar-lo.
Compartir un ArrayDeque o PriorityQueue entre fils. No són segures per a concurrència. Per a això hi ha les BlockingQueue i ConcurrentLinkedQueue del mòdul 8.
Consell: while ((x = cua.poll()) != null) és l'idioma estàndard per buidar una cua. És més compacte i més segur que combinar isEmpty() amb remove().
Consell: declara per la interfície que reflecteixi l'ús. Queue<X> si només consumeixes FIFO, Deque<X> si fas servir els dos extrems o és una pila. Així el tipus documenta la intenció.
Exercicis
Exercici 1: sala d'espera de préstecs
Escriu SalaEspera que gestioni l'atenció d'empleats al taulell de BiblioTech, fent servir un Deque<Empleat>:
void arribar(Empleat e): es col·loca al final.void arribarPrioritari(Empleat e): es col·loca al principi (personal de direcció).Empleat atendre(): atén el primer,nullsi no hi ha ningú.Empleat seguent(): consulta sense atendre.boolean marxar(Empleat e): l'empleat abandona la cua des d'on sigui.int posicioDe(Empleat e).List<Empleat> ordreInvers(): de l'últim al primer, ambdescendingIterator.void tancarMostrador(): atén tothom en ordre, imprimint cada atenció.
Documenta en comentaris quines operacions són O(1) i quines O(n).
Exercici 2: planificador de tasques amb prioritat
Crea un record TascaManteniment(String descripcio, int prioritat, int diaCreacio) i escriu PlanificadorTasques amb una PriorityQueue<TascaManteniment> on surti primer la de menor número de prioritat (1 = màxima) i, a igualtat, la més antiga:
void programar(TascaManteniment t).TascaManteniment seguent()sense extreure.TascaManteniment executar()extraient.List<TascaManteniment> properes(int quantes): les N següents sense buidar la cua.int executarTotes(): les executa totes en ordre, imprimint.void demostrarIteradorNoOrdenat(): imprimeix la cua ambtoString, ambfor-eachi després buidant-la ambpoll, mostrant que només l'última va en ordre.
Exercici 3: recorregut en amplada enfront de profunditat
Amplia el Recomanador de l'apartat 9 amb un mètode recomanarEnProfunditat(String origen, int profunditatMaxima) que faci servir una pila (push/pop sobre un ArrayDeque) en lloc d'una cua, deixant la resta de l'algorisme idèntica.
Escriu un main que executi tots dos sobre el mateix graf i expliqui en comentaris per què els resultats difereixen i en quines situacions interessa cadascun.
Solucions
Solució 1
package com.nexussoftware.bibliotech.servei;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import com.nexussoftware.bibliotech.domini.Empleat;
/** Sala d'espera del taulell de prestecs. */
public class SalaEspera {
private final Deque<Empleat> cua = new ArrayDeque<>();
/** O(1) amortitzat: escriure a 'tail' i incrementar-lo. */
public void arribar(Empleat e) {
if (e != null) { cua.offerLast(e); }
}
/**
* O(1): escriure a 'head-1' i decrementar-lo (l'array es CIRCULAR,
* aixi que 'head' pot fer la volta al final de l'array).
* Amb un ArrayList aixo seria add(0, e): O(n).
*/
public void arribarPrioritari(Empleat e) {
if (e != null) { cua.offerFirst(e); }
}
/** O(1). pollFirst retorna null si es buida; removeFirst llancaria excepcio. */
public Empleat atendre() {
return cua.pollFirst();
}
/** O(1). */
public Empleat seguent() {
return cua.peekFirst();
}
/**
* O(n): cal recorrer la cua buscant l'empleat. Es l'unica
* operacio cara d'aquesta classe, i es acceptable perque marxar a mitja
* cua es excepcional.
*/
public boolean marxar(Empleat e) {
return cua.removeFirstOccurrence(e); // usa equals (03-09)
}
/** O(n). Un Deque no te get(i): la posicio nomes se sap recorrent. */
public int posicioDe(Empleat e) {
int posicio = 1;
for (Empleat actual : cua) {
if (actual.equals(e)) { return posicio; }
posicio++;
}
return 0;
}
/** O(n). descendingIterator recorre de 'tail' a 'head'. */
public List<Empleat> ordreInvers() {
List<Empleat> resultat = new ArrayList<>(cua.size());
Iterator<Empleat> it = cua.descendingIterator();
while (it.hasNext()) { resultat.add(it.next()); }
return resultat;
}
/** Buida la cua atenent en ordre. Idioma estandard amb poll. */
public void tancarMostrador() {
System.out.println("--- Tancant mostrador: " + cua.size() + " en espera ---");
Empleat e;
int torn = 1;
while ((e = cua.pollFirst()) != null) {
System.out.printf(" Torn %d: %s (%s)%n",
torn++, e.getNom(), e.getIdentificador());
}
System.out.println("--- Mostrador tancat ---");
}
public int enEspera() { return cua.size(); }
public boolean buida() { return cua.isEmpty(); }
}Prova:
Empleat marta = new Empleat("Marta Ruiz", "EMP-001");
Empleat diego = new Empleat("Diego Alonso", "EMP-002");
Empleat nuria = new Empleat("Nuria Vidal", "EMP-003");
SalaEspera sala = new SalaEspera();
sala.arribar(marta);
sala.arribar(diego);
sala.arribarPrioritari(nuria); // es cola per davant
System.out.println("Seguent: " + sala.seguent().getNom()); // Nuria Vidal
System.out.println("Posicio de Marta: " + sala.posicioDe(marta)); // 2
System.out.println("Diego marxa: " + sala.marxar(diego)); // true
sala.tancarMostrador();Seguent: Nuria Vidal Posicio de Marta: 2 Diego marxa: true --- Tancant mostrador: 2 en espera --- Torn 1: Nuria Vidal (EMP-003) Torn 2: Marta Ruiz (EMP-001) --- Mostrador tancat ---
L'exercici il·lustra bé el repartiment de costos d'un Deque: tot el que passa als extrems és O(1), i tot el que exigeix mirar-hi dins és O(n). Això encaixa perfectament amb el domini: arribar, atendre i consultar el següent són constants; marxar a mitja cua o preguntar la posició són excepcionals.
Fixa't també en arribarPrioritari: és O(1) gràcies a l'array circular. Un ArrayList hauria de desplaçar tots els elements.
Solució 2
package com.nexussoftware.bibliotech.servei;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;
/** Tasca de manteniment del cataleg. Prioritat 1 = maxima urgencia. */
record TascaManteniment(String descripcio, int prioritat, int diaCreacio) {
TascaManteniment {
if (descripcio == null || descripcio.isBlank()) { descripcio = "(sense descripcio)"; }
if (prioritat < 1 || prioritat > 10) { prioritat = 5; }
if (diaCreacio < 0) { diaCreacio = 0; }
}
}
public class PlanificadorTasques {
private final Queue<TascaManteniment> cua;
public PlanificadorTasques() {
// Menor numero de prioritat primer. A igualtat, la mes antiga.
// El desempat fa l'ordre DETERMINISTA i a mes just: entre dues
// urgencies iguals, guanya la que fa mes temps que espera.
this.cua = new PriorityQueue<>(
Comparator.comparingInt(TascaManteniment::prioritat)
.thenComparingInt(TascaManteniment::diaCreacio));
}
/** O(log n): l'element sura cap amunt al monticle. */
public void programar(TascaManteniment t) {
if (t != null) { cua.offer(t); }
}
/** O(1): l'arrel del monticle. */
public TascaManteniment seguent() {
return cua.peek();
}
/** O(log n): treu l'arrel i reorganitza. */
public TascaManteniment executar() {
return cua.poll();
}
/**
* Les N seguents SENSE buidar la cua original.
* El constructor de copia de PriorityQueue duplica el monticle.
*/
public List<TascaManteniment> properes(int quantes) {
Queue<TascaManteniment> copia = new PriorityQueue<>(cua);
List<TascaManteniment> resultat = new ArrayList<>();
for (int i = 0; i < quantes && !copia.isEmpty(); i++) {
resultat.add(copia.poll());
}
return resultat;
}
public int executarTotes() {
int n = 0;
TascaManteniment t;
while ((t = cua.poll()) != null) {
System.out.printf(" [P%d] dia %3d %s%n", t.prioritat(), t.diaCreacio(),
t.descripcio());
n++;
}
return n;
}
/** Demostra que nomes poll() respecta l'ordre de prioritat. */
public void demostrarIteradorNoOrdenat() {
System.out.println("=== L'iterador NO recorre en ordre de prioritat ===");
System.out.println("toString():");
System.out.println(" " + cua);
System.out.print("for-each: ");
for (TascaManteniment t : cua) { System.out.print("P" + t.prioritat() + " "); }
System.out.println();
System.out.print("forEach: ");
cua.forEach(t -> System.out.print("P" + t.prioritat() + " "));
System.out.println();
System.out.print("poll: ");
Queue<TascaManteniment> copia = new PriorityQueue<>(cua);
TascaManteniment t;
while ((t = copia.poll()) != null) { System.out.print("P" + t.prioritat() + " "); }
System.out.println(" <- l'UNIC ordre garantit");
System.out.println("Causa: l'array intern nomes compleix la propietat de monticle");
System.out.println("(cada node <= els seus fills), no un ordre total.");
}
public int pendents() { return cua.size(); }
}Prova:
PlanificadorTasques p = new PlanificadorTasques();
p.programar(new TascaManteniment("Revisar exemplars deteriorats", 5, 100));
p.programar(new TascaManteniment("Reposar DVD malmes", 1, 130));
p.programar(new TascaManteniment("Inventari anual", 8, 90));
p.programar(new TascaManteniment("Actualitzar ISBN erronis", 1, 110));
p.programar(new TascaManteniment("Netejar prestatgeries", 5, 95));
System.out.println("Seguent: " + p.seguent().descripcio());
System.out.println("Properes 2: ");
p.properes(2).forEach(t -> System.out.println(" " + t.descripcio()));
System.out.println("Pendents despres de consultar: " + p.pendents());
p.demostrarIteradorNoOrdenat();
System.out.println("\n--- Executant totes ---");
p.executarTotes();Seguent: Actualitzar ISBN erronis Properes 2: Actualitzar ISBN erronis Reposar DVD malmes Pendents despres de consultar: 5 === L'iterador NO recorre en ordre de prioritat === toString(): [TascaManteniment[...prioritat=1, diaCreacio=110], ...] for-each: P1 P1 P8 P5 P5 forEach: P1 P1 P8 P5 P5 poll: P1 P1 P5 P5 P8 <- l'UNIC ordre garantit ... --- Executant totes --- [P1] dia 110 Actualitzar ISBN erronis [P1] dia 130 Reposar DVD malmes [P5] dia 95 Netejar prestatgeries [P5] dia 100 Revisar exemplars deteriorats [P8] dia 90 Inventari anual
Tres punts que resumeixen la lliçó. Primer, properes(2) no buida la cua perquè treballa sobre una còpia del monticle; oblidar-ho és l'error més freqüent en escriure un "top N". Segon, l'iterador produeix P1 P1 P8 P5 P5 —desordenat— mentre que poll produeix P1 P1 P5 P5 P8: la demostració visual que el monticle no és un ordre total. I tercer, el desempat per diaCreacio fa que entre les dues tasques P1 surti primer la del dia 110, la més antiga: sense ell, l'ordre seria arbitrari.
Solució 3
package com.nexussoftware.bibliotech.servei;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class RecomanadorComparat {
private final Map<String, List<String>> relacionats;
public RecomanadorComparat(Map<String, List<String>> relacionats) {
this.relacionats = relacionats;
}
/**
* AMPLADA (BFS): cua FIFO. Explora per NIVELLS: primer tots els
* veins directes, despres els veins d'aquests, etc.
*/
public List<String> enAmplada(String origen, int profunditatMaxima) {
return recorrer(origen, profunditatMaxima, true);
}
/**
* PROFUNDITAT (DFS): pila LIFO. Baixa tot el que pot per una branca
* abans de tornar enrere. MATEIX codi, nomes canvia per quin extrem es treu.
*/
public List<String> enProfunditat(String origen, int profunditatMaxima) {
return recorrer(origen, profunditatMaxima, false);
}
private List<String> recorrer(String origen, int profunditatMaxima, boolean amplada) {
List<String> resultat = new ArrayList<>();
Set<String> visitats = new HashSet<>();
Deque<String> perVisitar = new ArrayDeque<>();
Deque<Integer> profunditats = new ArrayDeque<>();
perVisitar.offerLast(origen);
profunditats.offerLast(0);
visitats.add(origen);
while (!perVisitar.isEmpty()) {
// L'UNICA DIFERENCIA entre BFS i DFS es en aquesta linia:
// pollFirst -> FIFO -> amplada
// pollLast -> LIFO -> profunditat
String actual = amplada ? perVisitar.pollFirst() : perVisitar.pollLast();
int profunditat = amplada ? profunditats.pollFirst() : profunditats.pollLast();
if (profunditat > 0) { resultat.add(actual); }
if (profunditat >= profunditatMaxima) { continue; }
for (String vei : relacionats.getOrDefault(actual, List.of())) {
if (visitats.add(vei)) { // add retorna false si ja hi era (05-06)
perVisitar.offerLast(vei);
profunditats.offerLast(profunditat + 1);
}
}
}
return resultat;
}
public static void main(String[] args) {
Map<String, List<String>> graf = Map.of(
"978-0000000001", List.of("978-0000000002", "DVD-0007"),
"978-0000000002", List.of("978-0000000003"),
"978-0000000003", List.of("REV-2024-03"),
"DVD-0007", List.of("REV-2024-03")
);
RecomanadorComparat r = new RecomanadorComparat(graf);
System.out.println("AMPLADA (BFS): " + r.enAmplada("978-0000000001", 3));
System.out.println("PROFUNDITAT (DFS): " + r.enProfunditat("978-0000000001", 3));
}
}AMPLADA (BFS): [978-0000000002, DVD-0007, 978-0000000003, REV-2024-03] PROFUNDITAT (DFS): [DVD-0007, REV-2024-03, 978-0000000002, 978-0000000003]
Per què difereixen. L'única línia diferent és d'on es treu el node següent. Amb pollFirst (FIFO), el node que fa més temps que espera surt primer, així que s'esgoten tots els de distància 1 abans de tocar els de distància 2: el resultat surt ordenat per proximitat. Amb pollLast (LIFO), surt l'últim afegit, que sempre és el més profund, així que l'algorisme baixa fins al fons d'una branca abans de tornar.
Quan interessa cadascun:
| Necessito | Recorregut |
|---|---|
| El camí més curt en nombre de salts | Amplada |
| Recomanacions ordenades per proximitat | Amplada |
| Explorar tot un subarbre abans de passar al següent | Profunditat |
| Detectar cicles, ordenar dependències, resoldre laberints | Profunditat |
| El graf és molt ample (pocs nivells, molts veïns) | Profunditat (fa servir menys memòria) |
| El graf és molt profund (molts nivells) | Amplada (evita piles enormes) |
Que la mateixa estructura de dades, un Deque, produeixi dos algorismes fonamentalment diferents segons per quin extrem es consumeixi és una de les idees més elegants de la programació, i explica per què Deque és la interfície més versàtil del Framework de Col·leccions.
Conclusió
Has afegit al teu repertori la família de col·leccions que respon a "a qui li toca?". Saps que una cua FIFO lliura primer qui fa més temps que espera i que la seva restricció —no hi ha accés arbitrari— és precisament el que garanteix que la política es respecti.
Domines la interfície Queue amb les seves dues famílies de mètodes i, sobretot, el criteri per triar entre elles: add/remove/element llancen excepció perquè tracten la fallada com una anomalia; offer/poll/peek retornen un valor especial perquè tracten la cua buida com una situació normal. I tens l'idioma estàndard per consumir una cua: while ((x = cua.poll()) != null).
Entens ArrayDeque per dins: un array circular amb dos índexs que es mouen en lloc de moure els elements, amb capacitat potència de dos, creixement per duplicació i cost amortitzat O(1) a tots dos extrems. Saps per què rebutja els null —perquè poll i peek els fan servir com a senyal de "buida"— i per què és l'opció per defecte per a cues i piles: menys memòria, millor localitat de memòria cau i cap pressió sobre el recol·lector enfront de LinkedList.
Coneixes Deque com la interfície més versàtil del Framework: dotze mètodes pels dos extrems, més els àlies de cua (offer/poll/peek) i de pila (push/pop), cosa que li permet ser cua FIFO, pila LIFO o cua de doble extrem amb la mateixa classe. I saps que no és una List: no hi ha get(i).
Manegues PriorityQueue i el seu monticle binari: l'arrel és sempre el mínim, offer i poll costen O(log n) perquè l'element sura o s'enfonsa per un arbre d'alçada logarítmica, i peek és O(1). Saps que necessita Comparable o Comparator, que no admet null, que no és estable, i —el detall que atrapa tothom— que el seu iterador, el seu toString i el seu forEach no recorren en ordre de prioritat, perquè l'array intern només compleix la propietat de monticle. L'única forma correcta és buidar-la amb poll, o fer-ho sobre una còpia si necessites conservar-la.
Saps que existeixen les BlockingQueue amb les seves operacions take i put que esperen, i que sobre elles es construeix el patró productor-consumidor, la implementació real del qual arriba al mòdul 8; i que les cues normals mai no s'han de compartir entre fils sense sincronització. I n'has vist els tres usos canònics: memòries intermèdies que absorbeixen diferències de ritme, planificació de tasques i recorregut en amplada, on has descobert una cosa notable: canviar pollFirst per pollLast en una sola línia converteix un BFS en un DFS.
BiblioTech té ara la seva CuaReserves reescrita sobre ArrayDeque —amb reserves urgents que es colen per davant en O(1) gràcies a l'array circular— i una CuaAvisos amb PriorityQueue que atén primer el préstec amb més dies de retard, aplicant el termini propi de cada tipus de material sense que el codi de procés sàpiga res d'aquesta lògica.
A la lliçó següent, Pila, explores l'altra política d'accés: LIFO, l'últim a entrar és el primer a sortir. Veuràs per a què serveix realment —desfer, avaluar expressions, recorregut en profunditat i la mateixa pila de crides de la JVM, amb la seva connexió amb el StackOverflowError i la recursivitat de 03-03—, per què la classe Stack heretada està desaconsellada (estén Vector, està sincronitzada i el seu iterador recorre a l'inrevés del que esperaries, amb demostració inclosa), com fer servir Deque com a pila i per què és la recomanació oficial, com implementar una pila pròpia amb un array per entendre l'estructura des de dins, i dos casos pràctics complets: parèntesis equilibrats i historial de navegació amb desfer i refer mitjançant dues piles. A BiblioTech apareixerà la pila d'operacions que permet anul·lar l'última alta o baixa del catàleg.
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
