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

  1. FIFO: la semàntica d'una cua
  2. La interfície Queue i les seves dues famílies de mètodes
  3. ArrayDeque: la memòria intermèdia circular
  4. Deque: la cua de doble extrem
  5. ArrayDeque enfront de LinkedList
  6. PriorityQueue: atendre el més urgent
  7. La sorpresa de l'iterador de PriorityQueue
  8. BlockingQueue i el patró productor-consumidor
  9. Casos d'ús reals: memòries intermèdies, planificació i BFS
  10. Aplicació a BiblioTech
  11. Errors Habituals i Consells
  12. Exercicis

  1. 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

  1. La interfície Queue i les seves dues famílies de mètodes

Queue 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 buida

A 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.

  1. ArrayDeque: la memòria intermèdia circular

ArrayDeque é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:

tail = (tail + 1) & (elements.length - 1);   // torna a 0 en passar del final

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.

  1. Deque: la cua de doble extrem

Deque (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());    // urgent

Aquest 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.

  1. ArrayDeque enfront de LinkedList

Totes 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
Implementa List No
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

  1. PriorityQueue: atendre el més urgent

Una 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());     // 30

Per 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(): és array[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 amb null.
  • Exigeix Comparable o Comparator. Sense cap dels dos, la primera inserció llança ClassCastException.
  • 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).

  1. La sorpresa de l'iterador de PriorityQueue

Aquest é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 50

L'ú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 buida

Sobre 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 intacta

Abocant 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 cua

I 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)

  1. BlockingQueue i el patró productor-consumidor

Hi 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.

  1. 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 proximitat
[978-0000000002, 978-0000000003]
[978-0000000002, 978-0000000003, DVD-0007, REV-2024-03]

Que 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.

  1. 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, null si 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, amb descendingIterator.
  • 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 amb toString, amb for-each i després buidant-la amb poll, 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

Mòdul 2: Flux de control

Mòdul 3: Programació orientada a objectes

Mòdul 4: Programació orientada a objectes avançada

Mòdul 5: Estructures de dades i col·leccions

Mòdul 6: Gestió d'excepcions

Mòdul 7: Entrada/sortida de fitxers

Mòdul 8: Multifil i concurrència

Mòdul 9: Xarxes

Mòdul 10: Temes avançats

Mòdul 11: Frameworks i llibreries de Java

Mòdul 12: Construcció d'aplicacions del món real

© Copyright 2026. Tots els drets reservats