Hasta ahora las colecciones de este módulo respondían a dos preguntas: "¿está?" (Set, Map) y "¿dónde está?" (List). Las colas responden a una tercera, que es la que gobierna cualquier sistema que procese trabajo pendiente: "¿a quién le toca ahora?".
Esa pregunta aparece en todas partes. La cola de reservas de un material en BiblioTech. Los trabajos pendientes de un servidor de impresión. Los paquetes que llegan a una tarjeta de red. Las tareas de un planificador. Los nodos por visitar de un recorrido en anchura. Los avisos de vencimiento ordenados por urgencia. En todos esos casos hay elementos que entran, esperan y salen siguiendo una política concreta, y la estructura de datos que modela esa política es una cola.
En esta lección verás la interfaz Queue con su semántica FIFO, su curiosa duplicidad de métodos —dos formas de hacer lo mismo con comportamientos distintos ante el error—, la interfaz Deque que abre los dos extremos, ArrayDeque con su buffer circular y su condición de opción por defecto actual, y la PriorityQueue, que sirve al elemento más urgente en lugar del más antiguo y esconde una sorpresa que atrapa a mucha gente. Al final, BiblioTech tendrá su cola de reservas reescrita como es debido y una cola de prioridad para los avisos de retraso.
Contenido
- FIFO: la semántica de una cola
- La interfaz
Queuey sus dos familias de métodos ArrayDeque: el buffer circularDeque: la cola de doble extremoArrayDequefrente aLinkedListPriorityQueue: atender al más urgente- La sorpresa del iterador de
PriorityQueue BlockingQueuey el patrón productor-consumidor- Casos de uso reales: buffers, planificación y BFS
- Aplicación a BiblioTech
- Errores Comunes y Consejos
- Ejercicios
- FIFO: la semántica de una cola
Una cola (queue) es una colección con una política de salida definida. La política clásica es FIFO: First In, First Out, "el primero que entra es el primero que sale". Exactamente como la cola del supermercado.
flowchart LR
E["entrada<br/>(offer / addLast)"] --> C4["Nuria"]
C4 --> C3["Diego"]
C3 --> C2["Marta"]
C2 --> S["salida<br/>(poll / removeFirst)"]
Los elementos entran por la cola (el final) y salen por la cabeza (el principio). El primero en llegar es el primero en ser atendido: es una política justa, que garantiza que nadie espere indefinidamente.
Compárala con las otras dos políticas que verás en este módulo:
| Política | Quién sale primero | Estructura | Lección |
|---|---|---|---|
| FIFO | El que lleva más tiempo esperando | Queue / Deque |
Esta |
| LIFO | El último que entró | Pila (Deque) |
05-08 |
| Por prioridad | El más urgente, sin importar cuándo llegó | PriorityQueue |
Esta, apartado 6 |
La diferencia esencial con una List es que una cola no ofrece acceso arbitrario. No hay get(i). Solo puedes mirar el primero y sacar el primero. Esa restricción no es una carencia: es la garantía de que el orden de proceso se respeta. Si el código pudiera colarse en medio de la cola, la política dejaría de estar garantizada por la estructura.
import java.util.ArrayDeque;
import java.util.Queue;
Queue<String> pendientes = new ArrayDeque<>();
pendientes.offer("Marta Ruiz"); // entra
pendientes.offer("Diego Alonso");
pendientes.offer("Nuria Vidal");
System.out.println(pendientes.peek()); // Marta Ruiz (mira sin sacar)
System.out.println(pendientes.poll()); // Marta Ruiz (saca)
System.out.println(pendientes.poll()); // Diego Alonso
System.out.println(pendientes.size()); // 1
- La interfaz
Queue y sus dos familias de métodos
Queue y sus dos familias de métodosQueue extiende Collection y declara seis métodos, que en realidad son tres operaciones por duplicado:
| Operación | Lanza excepción si falla | Devuelve valor especial |
|---|---|---|
| Insertar | add(e) → IllegalStateException |
offer(e) → false |
| Extraer | remove() → NoSuchElementException |
poll() → null |
| Consultar | element() → NoSuchElementException |
peek() → null |
¿Por qué existen dos versiones de cada una?
Porque hay dos situaciones distintas y merecen tratamientos distintos.
La familia que lanza excepción (add, remove, element) considera que el fallo es una anomalía. Si esperas que la cola tenga elementos y no los tiene, algo va mal en tu lógica y quieres enterarte de inmediato.
La familia que devuelve un valor especial (offer, poll, peek) considera que el fallo es una situación normal. Una cola vacía no es un error: es que no hay trabajo pendiente.
Queue<Reserva> cola = new ArrayDeque<>();
// Bucle de proceso: que la cola se vacie es NORMAL, es la condicion de parada
Reserva r;
while ((r = cola.poll()) != null) { // poll devuelve null: idiomatico y limpio
atender(r);
}
// Consulta puntual donde vacio SERIA un error de logica
Reserva siguiente = cola.remove(); // NoSuchElementException si esta vaciaAdemás, offer tiene sentido en colas acotadas (con capacidad máxima), donde insertar puede fallar legítimamente porque no cabe. ArrayDeque y LinkedList no tienen límite, así que su offer siempre devuelve true; pero las BlockingQueue del módulo 8 sí lo tienen, y ahí la diferencia importa mucho.
La regla práctica
| Situación | Usa |
|---|---|
| Bucle que consume hasta vaciar la cola | poll() |
| Comprobar si hay trabajo pendiente | peek() |
| Añadir a una cola sin límite | offer() o add(), indistinto |
| Añadir a una cola acotada | offer(), y mira el resultado |
| La cola vacía indica un fallo de lógica | remove() / element() |
Recomendación general: usa offer, poll y peek. Evitan excepciones para situaciones normales y hacen el código más robusto. Las variantes con excepción son útiles cuando quieres que un estado inesperado se manifieste en lugar de propagar un null silencioso.
Un aviso sobre peek y poll: si la cola admitiera elementos null, no podrías distinguir "está vacía" de "el primer elemento es null". Esa es exactamente la razón por la que ArrayDeque y PriorityQueue prohíben los null, como verás enseguida.
ArrayDeque: el buffer circular
ArrayDeque: el buffer circularArrayDeque es la implementación por defecto de colas y pilas en Java moderno. Por dentro es un array circular, y entender esa idea explica por qué es tan rápido.
El problema que resuelve es este: en un ArrayList, sacar el primer elemento obliga a desplazar todos los demás, O(n). ¿Cómo evitarlo sin usar nodos enlazados? La respuesta: no mover los elementos; mover los índices.
ArrayDeque mantiene un array y dos índices, head y tail. Extraer por la cabeza es incrementar head. Insertar por el final es escribir en tail e incrementarlo. Nadie se mueve.
flowchart TB
subgraph estado1["Estado inicial: head=0, tail=3"]
direction LR
A0["[0] Marta"]
A1["[1] Diego"]
A2["[2] Nuria"]
A3["[3] -"]
A4["[4] -"]
A5["[5] -"]
end
subgraph estado2["Tras 2 poll: head=2, tail=3"]
direction LR
B0["[0] -"]
B1["[1] -"]
B2["[2] Nuria"]
B3["[3] -"]
B4["[4] -"]
B5["[5] -"]
end
subgraph estado3["Tras 4 offer: tail DA LA VUELTA a 1"]
direction LR
C0["[0] Ana"]
C1["[1] -"]
C2["[2] Nuria"]
C3["[3] Luis"]
C4["[4] Eva"]
C5["[5] Pau"]
end
estado1 --> estado2 --> estado3
Cuando tail llega al final del array, da la vuelta al principio, siempre que ahí haya sitio libre. De ahí el nombre "circular": el array se comporta como un anillo. El cálculo es tan simple como en HashMap:
Y por el mismo motivo que en HashMap, la capacidad es siempre una potencia de dos: permite sustituir el módulo por un AND de bits.
Cuando el array se llena, se duplica y se copian los elementos, con el mismo razonamiento de coste amortizado que en ArrayList (05-03).
El resultado son estas complejidades:
| Operación | ArrayDeque |
|---|---|
offerFirst / offerLast |
O(1) amortizado |
pollFirst / pollLast |
O(1) |
peekFirst / peekLast |
O(1) |
contains(o) |
O(n) |
remove(Object) |
O(n) |
size() |
O(1) |
Y sus dos restricciones, ambas deliberadas:
No admite null. Porque usa null internamente como marca de celda vacía, y porque poll() y peek() devuelven null para indicar "cola vacía". Permitir elementos null haría esa señal ambigua. Intentar insertar uno lanza NullPointerException.
No es sincronizada. Para uso concurrente están las BlockingQueue y ConcurrentLinkedDeque del módulo 8.
Deque: la cola de doble extremo
Deque: la cola de doble extremoDeque (se pronuncia "deck", de Double Ended QUEue) extiende Queue y permite insertar, extraer y consultar por los dos extremos. Es la interfaz más versátil del Framework: sirve como cola FIFO y como 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 tabla completa de sus doce métodos principales:
| Operación | Extremo | Lanza excepción | Valor especial |
|---|---|---|---|
| Insertar | Cabeza | addFirst(e) |
offerFirst(e) |
| Insertar | Cola | addLast(e) |
offerLast(e) |
| Extraer | Cabeza | removeFirst() |
pollFirst() |
| Extraer | Cola | removeLast() |
pollLast() |
| Consultar | Cabeza | getFirst() |
peekFirst() |
| Consultar | Cola | getLast() |
peekLast() |
Y además hereda los métodos de Queue y añade los de pila, que son alias de los anteriores:
| Método heredado o alias | Equivale a | Semántica |
|---|---|---|
add(e) / offer(e) |
addLast(e) / offerLast(e) |
Cola |
remove() / poll() |
removeFirst() / pollFirst() |
Cola |
element() / peek() |
getFirst() / peekFirst() |
Cola |
push(e) |
addFirst(e) |
Pila |
pop() |
removeFirst() |
Pila |
Los tres modos de uso, sobre la misma clase:
// Como COLA (FIFO): entra por el final, sale por el principio
Deque<String> cola = new ArrayDeque<>();
cola.offerLast("Marta Ruiz");
cola.offerLast("Diego Alonso");
System.out.println(cola.pollFirst()); // Marta Ruiz
// Como PILA (LIFO): entra y sale por el mismo extremo
Deque<String> pila = new ArrayDeque<>();
pila.push("alta");
pila.push("baja");
System.out.println(pila.pop()); // baja (la ultima que entro)
// Como DEQUE de verdad: los dos extremos
Deque<String> doble = new ArrayDeque<>();
doble.offerFirst("urgente"); // se cuela al principio
doble.offerLast("normal"); // espera su turno al final
System.out.println(doble.pollFirst()); // urgenteEse tercer modo es el que da su nombre a la estructura y resuelve casos reales: una cola de trabajo donde las tareas urgentes se insertan por delante, un historial que crece por un lado y se recorta por el otro, un algoritmo que necesita mirar y consumir por ambos extremos.
Deque también ofrece descendingIterator(), que recorre de la cola a la cabeza, y removeFirstOccurrence/removeLastOccurrence para eliminar apariciones concretas.
Y una advertencia importante: Deque no es una List. No tiene get(i), set(i, e) ni indexOf. Si necesitas acceso por índice, no querías un Deque.
ArrayDeque frente a LinkedList
ArrayDeque frente a LinkedListLas dos implementan Deque. La comparación cierra la discusión abierta en 05-04:
| Aspecto | ArrayDeque |
LinkedList |
|---|---|---|
| Estructura interna | Array circular | Nodos doblemente enlazados |
| Memoria por elemento | ~4-8 bytes (una referencia) | ~28 bytes (objeto Node) |
| Localidad de caché | Excelente (contigua) | Mala (dispersa) |
offerFirst / offerLast |
O(1) amortizado | O(1) |
pollFirst / pollLast |
O(1) | O(1) |
| Recorrido | Rápido | 2-10× más lento |
| Presión sobre el recolector | Un solo array | Un objeto por elemento |
Admite null |
No | Sí |
Implementa List |
No | Sí |
| Recomendación oficial | Sí, como cola y pila | Solo si necesitas List + Deque |
La documentación del propio JDK es explícita: "esta clase [ArrayDeque] es probablemente más rápida que Stack cuando se usa como pila, y más rápida que LinkedList cuando se usa como cola".
Elige ArrayDeque salvo que necesites null o el interfaz List en la misma variable. No hay más casos.
Y la conclusión práctica que arrastramos desde 05-03:
| Necesito... | Implementación |
|---|---|
| Una lista | ArrayList |
| Un conjunto | HashSet |
| Un mapa | HashMap |
| Una cola o una pila | ArrayDeque |
| Una cola por prioridad | PriorityQueue |
PriorityQueue: atender al más urgente
PriorityQueue: atender al más urgenteUna PriorityQueue rompe el FIFO: no atiende al que lleva más tiempo esperando, sino al más prioritario. Es la estructura de un servicio de urgencias, de un planificador de tareas o —en BiblioTech— de una lista de avisos donde primero se llama a quien acumula más días de retraso.
import java.util.PriorityQueue;
import java.util.Queue;
// Por orden natural: el MENOR sale primero
Queue<Integer> cola = new PriorityQueue<>();
cola.offer(30);
cola.offer(10);
cola.offer(20);
System.out.println(cola.poll()); // 10
System.out.println(cola.poll()); // 20
System.out.println(cola.poll()); // 30Por defecto usa el orden natural (Comparable) y sirve primero el menor. Con un Comparator puedes definir cualquier criterio, retomando todo 04-06:
// Los prestamos con MAS dias de retraso primero
Queue<Prestamo> avisos = new PriorityQueue<>(
Comparator.comparingInt((Prestamo p) -> p.diasRetraso(diaActual)).reversed());
// Las reservas mas urgentes primero y, a igualdad, las mas antiguas
Queue<Reserva> reservas = new PriorityQueue<>(
Comparator.comparingInt(Reserva::getPrioridad)
.thenComparingInt(Reserva::getDiaSolicitud));Cómo funciona: el montículo binario
PriorityQueue no mantiene todos los elementos ordenados —eso costaría demasiado—. Usa un montículo binario (binary heap), un árbol binario casi completo con una única regla, llamada propiedad de montículo:
Todo nodo es menor o igual que sus hijos.
De ahí se deduce que la raíz es siempre el mínimo, que es lo único que necesitamos para saber a quién le toca.
flowchart TB
R["10<br/>(raiz = minimo)"]
A["20"]
B["15"]
C["40"]
D["25"]
E["30"]
R --> A
R --> B
A --> C
A --> D
B --> E
Fíjate en que el orden no es total: el 15 está a la derecha del 20, aunque sea menor. La única garantía es la relación padre-hijo, y con eso basta.
El montículo se guarda en un array, sin nodos ni referencias: el hijo izquierdo del índice i está en 2i+1 y el derecho en 2i+2. Eso le da una excelente localidad de caché.
Las operaciones funcionan así:
offer(e): se coloca el elemento al final del array y se hace "flotar hacia arriba" intercambiándolo con su padre mientras sea menor. Como el árbol tiene altura log n, el coste es O(log n).poll(): se toma la raíz (el mínimo), se pone el último elemento en su lugar y se hace "hundir hacia abajo" intercambiándolo con el menor de sus hijos. También O(log n).peek(): esarray[0]. O(1).
| Operación | PriorityQueue |
|---|---|
offer(e) |
O(log n) |
poll() |
O(log n) |
peek() |
O(1) |
contains(o) |
O(n) |
remove(Object) |
O(n) |
| Recorrer en orden de prioridad | O(n log n) vaciándola con poll |
Y sus restricciones:
- No admite
null: no se puede comparar connull. - Exige
ComparableoComparator. Sin ninguno de los dos, la primera inserción lanzaClassCastException. - No es estable: dos elementos con la misma prioridad salen en orden arbitrario. Si te importa, añade un criterio de desempate al comparador (por ejemplo, el orden de llegada).
- La sorpresa del iterador de
PriorityQueue
PriorityQueueEste es el detalle que atrapa a casi todo el mundo la primera vez:
Queue<Integer> cola = new PriorityQueue<>();
cola.offer(30);
cola.offer(10);
cola.offer(20);
cola.offer(5);
System.out.println(cola); // [5, 10, 20, 30] o [5, 10, 20, 30]... depende
for (int n : cola) {
System.out.print(n + " "); // 5 10 20 30 ... o NO
}El iterador de una PriorityQueue no recorre en orden de prioridad. Recorre el array interno tal cual está, y ese array solo cumple la propiedad de montículo, no un orden total. Lo mismo vale para toString(), para forEach y para toArray().
Con estos datos:
Queue<Integer> cola = new PriorityQueue<>();
for (int n : new int[]{ 50, 40, 30, 20, 10 }) { cola.offer(n); }
System.out.println("toString: " + cola); // [10, 20, 40, 50, 30] <- NO ordenado
System.out.print("iterador: ");
cola.forEach(n -> System.out.print(n + " ")); // 10 20 40 50 30
System.out.print("\npoll: ");
while (!cola.isEmpty()) { System.out.print(cola.poll() + " "); } // 10 20 30 40 50La única garantía es que peek() y poll() devuelven el elemento de mayor prioridad. Todo lo demás es el estado interno del montículo.
Cómo recorrerla correctamente
Consumiéndola con poll:
while (!cola.isEmpty()) {
Prestamo p = cola.poll(); // en orden de prioridad garantizado
procesar(p);
}
// ...pero la cola queda vaciaSobre una copia, si necesitas conservarla:
Queue<Prestamo> copia = new PriorityQueue<>(cola); // copia el monticulo
while (!copia.isEmpty()) {
procesar(copia.poll());
}
// la cola original sigue intactaVolcando a una lista y ordenándola, si vas a recorrer varias veces:
List<Prestamo> ordenados = new ArrayList<>(cola);
ordenados.sort(cola.comparator()); // el mismo criterio de la colaY una alternativa a considerar: si lo que necesitas es una colección siempre ordenada y recorrible en orden, TreeSet (05-06) es mejor opción que PriorityQueue. La PriorityQueue está optimizada para "dame el siguiente", no para "recórreme entera".
| Necesito | Estructura |
|---|---|
| Extraer siempre el más prioritario | PriorityQueue |
| Recorrer siempre en orden, y consultar rangos | TreeSet |
| Ordenar una vez, al final | List + sort (05-09) |
BlockingQueue y el patrón productor-consumidor
BlockingQueue y el patrón productor-consumidorHay una familia de colas pensada para que varios hilos se comuniquen: las BlockingQueue (ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue). Su rasgo distintivo son dos operaciones que esperan:
take(): si la cola está vacía, el hilo se bloquea hasta que alguien inserte algo.put(e): si la cola está llena, el hilo se bloquea hasta que alguien saque algo.
Sobre ellas se construye el patrón de concurrencia más clásico que existe, el productor-consumidor:
flowchart LR
P1["Productor 1"] --> Q["BlockingQueue<br/>(cola compartida)"]
P2["Productor 2"] --> Q
Q --> C1["Consumidor 1"]
Q --> C2["Consumidor 2"]
Uno o varios hilos producen trabajo y lo depositan en la cola; uno o varios lo consumen. La cola actúa de amortiguador: absorbe los picos de producción y desacopla los ritmos de ambos lados, de modo que ninguno tiene que conocer al otro ni esperar activamente.
Sus ventajas son claras: los productores no se preocupan de si hay consumidores libres, los consumidores no consultan continuamente si hay trabajo (la cola los duerme y los despierta), y la capacidad máxima de la cola ejerce de control de flujo natural: si los consumidores no dan abasto, la cola se llena y los productores frenan solos.
En BiblioTech, un caso natural sería un proceso nocturno que calcula multas: un hilo recorre los préstamos y los deposita en la cola, y varios hilos calculan e imprimen los avisos.
La implementación real de todo esto es el módulo 8, que cubre hilos, sincronización, ExecutorService y colecciones concurrentes. Aquí solo necesitas saber que existe, que se apoya en la misma interfaz Queue que acabas de aprender, y que nunca debes compartir un ArrayDeque o una PriorityQueue entre hilos sin sincronización: no son seguras para concurrencia.
- Casos de uso reales: buffers, planificación y BFS
Buffers
Una cola entre dos procesos de velocidad distinta absorbe las diferencias de ritmo. Un lector rápido deposita líneas en la cola y un procesador lento las consume, sin que ninguno espere al otro más de lo necesario. Es la base de los flujos con BufferedReader (módulo 7) y de las colas de mensajería.
Una variante útil es el buffer circular acotado, que descarta lo más antiguo al llenarse:
Deque<String> ultimasOperaciones = new ArrayDeque<>();
void registrar(String operacion) {
ultimasOperaciones.offerLast(operacion);
if (ultimasOperaciones.size() > 100) {
ultimasOperaciones.pollFirst(); // O(1): descarta la mas antigua
}
}Con un ArrayList esto sería O(n) por cada registro; con un Deque es O(1).
Planificación
Una cola de tareas pendientes:
Queue<Runnable> tareas = new ArrayDeque<>();
tareas.offer(() -> System.out.println("Calcular multas"));
tareas.offer(() -> System.out.println("Enviar avisos"));
tareas.offer(() -> System.out.println("Generar informe"));
Runnable tarea;
while ((tarea = tareas.poll()) != null) {
tarea.run(); // se ejecutan en orden de llegada
}Si además hay urgencias, PriorityQueue con un comparador por prioridad.
Recorrido en anchura (BFS)
Es el uso algorítmico más importante de las colas. El recorrido en anchura (Breadth-First Search) explora una estructura por niveles: primero los vecinos directos, luego los vecinos de los vecinos, y así sucesivamente. Garantiza encontrar el camino más corto en número de pasos.
El esquema es siempre el mismo: una cola de nodos por visitar y un conjunto de ya visitados (Set, de 05-06, para no repetir ni entrar en ciclos).
Un ejemplo pequeño en BiblioTech: materiales relacionados ("quien leyó esto también leyó aquello"), y queremos las recomendaciones ordenadas por cercanía.
package com.nexussoftware.bibliotech.servicio;
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 Recomendador {
/** Grafo: referencia -> referencias relacionadas. */
private final Map<String, List<String>> relacionados;
public Recomendador(Map<String, List<String>> relacionados) {
this.relacionados = relacionados;
}
/**
* Recorrido en anchura: devuelve las referencias alcanzables desde 'origen'
* hasta 'profundidadMaxima' saltos, en orden de CERCANIA.
*/
public List<String> recomendar(String origen, int profundidadMaxima) {
List<String> resultado = new ArrayList<>();
Set<String> visitados = new HashSet<>();
Deque<String> porVisitar = new ArrayDeque<>();
Deque<Integer> profundidades = new ArrayDeque<>();
porVisitar.offerLast(origen);
profundidades.offerLast(0);
visitados.add(origen);
while (!porVisitar.isEmpty()) {
String actual = porVisitar.pollFirst(); // FIFO: por niveles
int profundidad = profundidades.pollFirst();
if (profundidad > 0) { resultado.add(actual); } // el origen no se recomienda
if (profundidad >= profundidadMaxima) { continue; }
for (String vecino : relacionados.getOrDefault(actual, List.of())) {
if (visitados.add(vecino)) { // add devuelve false si ya estaba (05-06)
porVisitar.offerLast(vecino);
profundidades.offerLast(profundidad + 1);
}
}
}
return resultado;
}
}Uso:
Map<String, List<String>> grafo = Map.of(
"978-0000000001", List.of("978-0000000002", "978-0000000003"), // Java Efectivo
"978-0000000002", List.of("978-0000000003", "DVD-0007"), // Patrones
"978-0000000003", List.of("DVD-0007"), // Refactorizacion
"DVD-0007", List.of("REV-2024-03")
);
Recomendador r = new Recomendador(grafo);
System.out.println(r.recomendar("978-0000000001", 1)); // vecinos directos
System.out.println(r.recomendar("978-0000000001", 3)); // hasta 3 saltos, por cercaniaQue la cola sea FIFO es lo que garantiza el orden por niveles: todos los nodos a distancia 1 se procesan antes que cualquiera a distancia 2. Si cambiaras la cola por una pila (push/pop), el mismo código haría un recorrido en profundidad, con resultados completamente distintos. Esa comparación la verás en 05-08.
- Aplicación a BiblioTech
La cola de reservas, ahora con Deque
En 05-04 escribiste ColaReservas sobre LinkedList y ya se anunció que ArrayDeque sería mejor. Aquí está la versión definitiva:
package com.nexussoftware.bibliotech.servicio;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import com.nexussoftware.bibliotech.dominio.Empleado;
import com.nexussoftware.bibliotech.dominio.Material;
import com.nexussoftware.bibliotech.dominio.Reserva;
/**
* Cola FIFO de reservas de UN material, sobre ArrayDeque.
*
* Frente a la version con LinkedList de 05-04: mismo O(1) en los extremos,
* pero con un array circular en lugar de nodos -> menos memoria, mejor
* localidad de cache y ninguna presion sobre el recolector.
*/
public class ColaReservas {
private final Material material;
private final Deque<Reserva> pendientes = new ArrayDeque<>();
public ColaReservas(Material material) {
this.material = material;
}
/** Encolar por el final: O(1) amortizado. */
public void reservar(Empleado empleado, int dia) {
pendientes.offerLast(new Reserva(empleado, material, dia));
}
/** Reserva urgente: se cuela por delante. Solo un Deque permite esto. */
public void reservarUrgente(Empleado empleado, int dia) {
pendientes.offerFirst(new Reserva(empleado, material, dia, 1));
}
/** peekFirst: null si no hay nadie. Nunca lanza excepcion. */
public Reserva siguiente() {
return pendientes.peekFirst();
}
/** pollFirst: atiende y saca. O(1). */
public Reserva atenderSiguiente(int dia) {
Reserva r = pendientes.pollFirst();
if (r != null) {
r.marcarAtendida();
material.prestar();
}
return r;
}
/** Cancela la reserva mas reciente de un empleado, recorriendo desde el final. */
public boolean cancelarUltimaDe(Empleado empleado) {
Iterator<Reserva> it = pendientes.descendingIterator();
while (it.hasNext()) {
if (it.next().getEmpleado().equals(empleado)) {
it.remove();
return true;
}
}
return false;
}
/** Caduca las reservas que llevan demasiado esperando. */
public int caducar(int diaActual, int diasMaximos) {
int antes = pendientes.size();
pendientes.removeIf(r -> r.diasEnEspera(diaActual) > diasMaximos);
return antes - pendientes.size();
}
/** Posicion en la cola (1 = el siguiente). 0 si no tiene reserva. */
public int posicionDe(Empleado empleado) {
int posicion = 1;
for (Reserva r : pendientes) { // un Deque NO tiene get(i): siempre for-each
if (r.getEmpleado().equals(empleado)) { return posicion; }
posicion++;
}
return 0;
}
public List<Reserva> listar() { return new ArrayList<>(pendientes); }
public int enEspera() { return pendientes.size(); }
public boolean hayPendientes() { return !pendientes.isEmpty(); }
}La cola de avisos por prioridad
Y ahora una PriorityQueue que atiende siempre al préstamo con más días de retraso:
package com.nexussoftware.bibliotech.servicio;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;
import com.nexussoftware.bibliotech.dominio.Gravedad;
import com.nexussoftware.bibliotech.dominio.Prestamo;
/** Avisos de vencimiento atendidos por urgencia, no por orden de llegada. */
public class ColaAvisos {
private final Queue<Prestamo> avisos;
private final int diaActual;
public ColaAvisos(int diaActual) {
this.diaActual = diaActual;
// Mas dias de retraso primero; a igualdad, el prestamo mas antiguo.
// El desempate hace el orden DETERMINISTA: sin el, dos prestamos con
// el mismo retraso saldrian en orden arbitrario.
this.avisos = new PriorityQueue<>(
Comparator.comparingInt((Prestamo p) -> diasRetraso(p)).reversed()
.thenComparingInt(Prestamo::getDiaPrestamo));
}
private int diasRetraso(Prestamo p) {
return p.getMaterial().calcularDiasRetraso(diaActual - p.getDiaPrestamo());
}
/** Encola solo lo que esta realmente vencido. O(log n). */
public void encolar(Prestamo p) {
if (p != null && !p.estaDevuelto() && p.estaVencido(diaActual)) {
avisos.offer(p);
}
}
public void encolarTodos(List<Prestamo> prestamos) {
for (Prestamo p : prestamos) { encolar(p); }
}
/** El mas urgente, sin sacarlo. O(1). */
public Prestamo masUrgente() {
return avisos.peek();
}
/**
* Procesa TODOS los avisos en orden de urgencia.
*
* IMPORTANTE: hay que vaciar la cola con poll(). Recorrerla con for-each
* o imprimir su toString() daria el orden del monticulo interno, que NO
* es el orden de prioridad.
*/
public int procesarTodos() {
int procesados = 0;
Prestamo p;
while ((p = avisos.poll()) != null) { // idioma del apartado 2
int retraso = diasRetraso(p);
Gravedad g = p.getMaterial().clasificarGravedad(diaActual - p.getDiaPrestamo());
System.out.printf("[%-10s] %-16s %-24s %3d dias %6.2f EUR%n",
g, p.getEmpleado().getNombre(), p.getMaterial().getTitulo(),
retraso, p.calcularMulta(diaActual));
procesados++;
}
return procesados;
}
/** Los N mas urgentes, SIN vaciar la cola: se trabaja sobre una copia. */
public List<Prestamo> topUrgentes(int cuantos) {
Queue<Prestamo> copia = new PriorityQueue<>(avisos); // copia el monticulo
List<Prestamo> resultado = new ArrayList<>();
for (int i = 0; i < cuantos && !copia.isEmpty(); i++) {
resultado.add(copia.poll());
}
return resultado;
}
public int pendientes() { return avisos.size(); }
}Uso conjunto:
Empleado marta = new Empleado("Marta Ruiz", "EMP-001");
Empleado diego = new Empleado("Diego Alonso", "EMP-002");
Empleado nuria = new Empleado("Nuria Vidal", "EMP-003");
Material javaEfectivo = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);
// --- Cola de reservas (FIFO) ---
javaEfectivo.prestar();
ColaReservas reservas = new ColaReservas(javaEfectivo);
reservas.reservar(marta, 100);
reservas.reservar(diego, 101);
reservas.reservarUrgente(nuria, 102); // se cuela por delante
System.out.println("Siguiente: " + reservas.siguiente().getEmpleado().getNombre());
System.out.println("Posicion de Marta: " + reservas.posicionDe(marta));
// --- Cola de avisos (por prioridad) ---
List<Prestamo> prestamos = List.of(
new Prestamo(new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018), marta, 100),
new Prestamo(new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"), diego, 120),
new Prestamo(new Dvd("Refactorizacion en vivo", "DVD-0007", 95), nuria, 125),
new Prestamo(new Libro("Patrones de Diseno", "Erich Gamma", "978-0000000002", 1994), diego, 110)
);
ColaAvisos avisos = new ColaAvisos(140);
avisos.encolarTodos(prestamos);
System.out.println("\nAvisos pendientes: " + avisos.pendientes());
System.out.println("Mas urgente: " + avisos.masUrgente().getMaterial().getTitulo());
System.out.println("\n--- Procesando por urgencia ---");
avisos.procesarTodos();Siguiente: Nuria Vidal Posicion de Marta: 2 Avisos pendientes: 4 Mas urgente: Java Magazine --- Procesando por urgencia --- [GRAVE ] Diego Alonso Java Magazine 13 dias 1.30 EUR [GRAVE ] Nuria Vidal Refactorizacion en vivo 12 dias 6.00 EUR [GRAVE ] Marta Ruiz Java Efectivo 25 dias 6.25 EUR [GRAVE ] Diego Alonso Patrones de Diseno 15 dias 3.75 EUR
Observa que el orden no es el de inserción ni el de días de préstamo: es el de días de retraso descendente, calculado según el plazo propio de cada tipo de material. Una revista con 13 días de retraso es más urgente que un libro con 25, porque el plazo de una revista es de 7 días y el de un libro de 15. La PriorityQueue aplica esa lógica sin que el código de proceso tenga que saber nada.
Errores Comunes y Consejos
Recorrer una PriorityQueue con for-each esperando orden de prioridad. No lo da: recorre el montículo interno. Lo mismo con toString(), forEach y toArray(). La única forma es vaciarla con poll(), o volcar a una lista y ordenarla.
Insertar null en un ArrayDeque o en una PriorityQueue. NullPointerException. Es deliberado: poll() y peek() usan null para señalar "vacío". Si necesitas guardar ausencias, replantea el modelo o usa Optional (10-04).
Confundir remove() con poll(). Sobre una cola vacía, remove() lanza NoSuchElementException y poll() devuelve null. Elige según si la cola vacía es normal o es un error.
Usar LinkedList como cola. Funciona, pero ArrayDeque es más rápido y consume mucha menos memoria. La recomendación oficial es ArrayDeque.
Buscar get(i) en un Deque. No existe: un Deque no es una List. Si necesitas acceso por índice, elegiste la estructura equivocada.
Usar contains o remove(Object) en una cola dentro de un bucle. Ambos son O(n) en ArrayDeque y PriorityQueue. Si necesitas buscar a menudo, mantén además un Set o un Map de apoyo.
Comparator sin desempate en una PriorityQueue. No perderás elementos —eso solo pasa en TreeSet (05-06)—, pero el orden entre empatados será arbitrario y no reproducible entre ejecuciones. Añade un criterio de desempate si el orden debe ser determinista.
Modificar un elemento ya encolado en una PriorityQueue. Si cambias el campo por el que se ordena, el montículo no se reorganiza: el elemento queda en una posición incorrecta y el orden de salida deja de ser fiable. Sácalo, modifícalo y vuelve a encolarlo.
Compartir un ArrayDeque o PriorityQueue entre hilos. No son seguras para concurrencia. Para eso están las BlockingQueue y ConcurrentLinkedQueue del módulo 8.
Consejo: while ((x = cola.poll()) != null) es el idioma estándar para vaciar una cola. Es más compacto y más seguro que combinar isEmpty() con remove().
Consejo: declara por la interfaz que refleje el uso. Queue<X> si solo consumes FIFO, Deque<X> si usas los dos extremos o es una pila. Así el tipo documenta la intención.
Ejercicios
Ejercicio 1: sala de espera de préstamos
Escribe SalaEspera que gestione la atención de empleados en el mostrador de BiblioTech, usando un Deque<Empleado>:
void llegar(Empleado e): se coloca al final.void llegarPrioritario(Empleado e): se coloca al principio (personal de dirección).Empleado atender(): atiende al primero,nullsi no hay nadie.Empleado siguiente(): consulta sin atender.boolean irse(Empleado e): el empleado abandona la cola desde donde esté.int posicionDe(Empleado e).List<Empleado> ordenInverso(): del último al primero, condescendingIterator.void cerrarMostrador(): atiende a todos en orden, imprimiendo cada atención.
Documenta en comentarios qué operaciones son O(1) y cuáles O(n).
Ejercicio 2: planificador de tareas con prioridad
Crea un record TareaMantenimiento(String descripcion, int prioridad, int diaCreacion) y escribe PlanificadorTareas con una PriorityQueue<TareaMantenimiento> donde salga primero la de menor número de prioridad (1 = máxima) y, a igualdad, la más antigua:
void programar(TareaMantenimiento t).TareaMantenimiento siguiente()sin extraer.TareaMantenimiento ejecutar()extrayendo.List<TareaMantenimiento> proximas(int cuantas): las N siguientes sin vaciar la cola.int ejecutarTodas(): las ejecuta todas en orden, imprimiendo.void demostrarIteradorNoOrdenado(): imprime la cola contoString, confor-eachy luego vaciándola conpoll, mostrando que solo la última va en orden.
Ejercicio 3: recorrido en anchura frente a profundidad
Amplía el Recomendador del apartado 9 con un método recomendarEnProfundidad(String origen, int profundidadMaxima) que use una pila (push/pop sobre un ArrayDeque) en lugar de una cola, dejando el resto del algoritmo idéntico.
Escribe un main que ejecute ambos sobre el mismo grafo y explique en comentarios por qué los resultados difieren y en qué situaciones interesa cada uno.
Soluciones
Solución 1
package com.nexussoftware.bibliotech.servicio;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import com.nexussoftware.bibliotech.dominio.Empleado;
/** Sala de espera del mostrador de prestamos. */
public class SalaEspera {
private final Deque<Empleado> cola = new ArrayDeque<>();
/** O(1) amortizado: escribir en 'tail' e incrementarlo. */
public void llegar(Empleado e) {
if (e != null) { cola.offerLast(e); }
}
/**
* O(1): escribir en 'head-1' y decrementarlo (el array es CIRCULAR,
* asi que 'head' puede dar la vuelta al final del array).
* Con un ArrayList esto seria add(0, e): O(n).
*/
public void llegarPrioritario(Empleado e) {
if (e != null) { cola.offerFirst(e); }
}
/** O(1). pollFirst devuelve null si esta vacia; removeFirst lanzaria excepcion. */
public Empleado atender() {
return cola.pollFirst();
}
/** O(1). */
public Empleado siguiente() {
return cola.peekFirst();
}
/**
* O(n): hay que recorrer la cola buscando al empleado. Es la unica
* operacion cara de esta clase, y es aceptable porque irse a mitad
* de cola es excepcional.
*/
public boolean irse(Empleado e) {
return cola.removeFirstOccurrence(e); // usa equals (03-09)
}
/** O(n). Un Deque no tiene get(i): la posicion solo se sabe recorriendo. */
public int posicionDe(Empleado e) {
int posicion = 1;
for (Empleado actual : cola) {
if (actual.equals(e)) { return posicion; }
posicion++;
}
return 0;
}
/** O(n). descendingIterator recorre de 'tail' a 'head'. */
public List<Empleado> ordenInverso() {
List<Empleado> resultado = new ArrayList<>(cola.size());
Iterator<Empleado> it = cola.descendingIterator();
while (it.hasNext()) { resultado.add(it.next()); }
return resultado;
}
/** Vacia la cola atendiendo en orden. Idioma estandar con poll. */
public void cerrarMostrador() {
System.out.println("--- Cerrando mostrador: " + cola.size() + " en espera ---");
Empleado e;
int turno = 1;
while ((e = cola.pollFirst()) != null) {
System.out.printf(" Turno %d: %s (%s)%n",
turno++, e.getNombre(), e.getIdentificador());
}
System.out.println("--- Mostrador cerrado ---");
}
public int enEspera() { return cola.size(); }
public boolean vacia() { return cola.isEmpty(); }
}Prueba:
Empleado marta = new Empleado("Marta Ruiz", "EMP-001");
Empleado diego = new Empleado("Diego Alonso", "EMP-002");
Empleado nuria = new Empleado("Nuria Vidal", "EMP-003");
SalaEspera sala = new SalaEspera();
sala.llegar(marta);
sala.llegar(diego);
sala.llegarPrioritario(nuria); // se cuela por delante
System.out.println("Siguiente: " + sala.siguiente().getNombre()); // Nuria Vidal
System.out.println("Posicion de Marta: " + sala.posicionDe(marta)); // 2
System.out.println("Diego se va: " + sala.irse(diego)); // true
sala.cerrarMostrador();Siguiente: Nuria Vidal Posicion de Marta: 2 Diego se va: true --- Cerrando mostrador: 2 en espera --- Turno 1: Nuria Vidal (EMP-003) Turno 2: Marta Ruiz (EMP-001) --- Mostrador cerrado ---
El ejercicio ilustra bien el reparto de costes de un Deque: todo lo que ocurre en los extremos es O(1), y todo lo que exige mirar dentro es O(n). Eso encaja perfectamente con el dominio: llegar, atender y consultar el siguiente son constantes; irse a mitad de cola o preguntar la posición son excepcionales.
Fíjate también en llegarPrioritario: es O(1) gracias al array circular. Un ArrayList tendría que desplazar todos los elementos.
Solución 2
package com.nexussoftware.bibliotech.servicio;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;
/** Tarea de mantenimiento del catalogo. Prioridad 1 = maxima urgencia. */
record TareaMantenimiento(String descripcion, int prioridad, int diaCreacion) {
TareaMantenimiento {
if (descripcion == null || descripcion.isBlank()) { descripcion = "(sin descripcion)"; }
if (prioridad < 1 || prioridad > 10) { prioridad = 5; }
if (diaCreacion < 0) { diaCreacion = 0; }
}
}
public class PlanificadorTareas {
private final Queue<TareaMantenimiento> cola;
public PlanificadorTareas() {
// Menor numero de prioridad primero. A igualdad, la mas antigua.
// El desempate hace el orden DETERMINISTA y ademas justo: entre dos
// urgencias iguales, gana la que lleva mas tiempo esperando.
this.cola = new PriorityQueue<>(
Comparator.comparingInt(TareaMantenimiento::prioridad)
.thenComparingInt(TareaMantenimiento::diaCreacion));
}
/** O(log n): el elemento flota hacia arriba en el monticulo. */
public void programar(TareaMantenimiento t) {
if (t != null) { cola.offer(t); }
}
/** O(1): la raiz del monticulo. */
public TareaMantenimiento siguiente() {
return cola.peek();
}
/** O(log n): saca la raiz y reorganiza. */
public TareaMantenimiento ejecutar() {
return cola.poll();
}
/**
* Las N siguientes SIN vaciar la cola original.
* El constructor de copia de PriorityQueue duplica el monticulo.
*/
public List<TareaMantenimiento> proximas(int cuantas) {
Queue<TareaMantenimiento> copia = new PriorityQueue<>(cola);
List<TareaMantenimiento> resultado = new ArrayList<>();
for (int i = 0; i < cuantas && !copia.isEmpty(); i++) {
resultado.add(copia.poll());
}
return resultado;
}
public int ejecutarTodas() {
int n = 0;
TareaMantenimiento t;
while ((t = cola.poll()) != null) {
System.out.printf(" [P%d] dia %3d %s%n", t.prioridad(), t.diaCreacion(),
t.descripcion());
n++;
}
return n;
}
/** Demuestra que solo poll() respeta el orden de prioridad. */
public void demostrarIteradorNoOrdenado() {
System.out.println("=== El iterador NO recorre en orden de prioridad ===");
System.out.println("toString():");
System.out.println(" " + cola);
System.out.print("for-each: ");
for (TareaMantenimiento t : cola) { System.out.print("P" + t.prioridad() + " "); }
System.out.println();
System.out.print("forEach: ");
cola.forEach(t -> System.out.print("P" + t.prioridad() + " "));
System.out.println();
System.out.print("poll: ");
Queue<TareaMantenimiento> copia = new PriorityQueue<>(cola);
TareaMantenimiento t;
while ((t = copia.poll()) != null) { System.out.print("P" + t.prioridad() + " "); }
System.out.println(" <- el UNICO orden garantizado");
System.out.println("Causa: el array interno solo cumple la propiedad de monticulo");
System.out.println("(cada nodo <= sus hijos), no un orden total.");
}
public int pendientes() { return cola.size(); }
}Prueba:
PlanificadorTareas p = new PlanificadorTareas();
p.programar(new TareaMantenimiento("Revisar ejemplares deteriorados", 5, 100));
p.programar(new TareaMantenimiento("Reponer DVD danado", 1, 130));
p.programar(new TareaMantenimiento("Inventario anual", 8, 90));
p.programar(new TareaMantenimiento("Actualizar ISBN erroneos", 1, 110));
p.programar(new TareaMantenimiento("Limpiar estanterias", 5, 95));
System.out.println("Siguiente: " + p.siguiente().descripcion());
System.out.println("Proximas 2: ");
p.proximas(2).forEach(t -> System.out.println(" " + t.descripcion()));
System.out.println("Pendientes tras consultar: " + p.pendientes());
p.demostrarIteradorNoOrdenado();
System.out.println("\n--- Ejecutando todas ---");
p.ejecutarTodas();Siguiente: Actualizar ISBN erroneos Proximas 2: Actualizar ISBN erroneos Reponer DVD danado Pendientes tras consultar: 5 === El iterador NO recorre en orden de prioridad === toString(): [TareaMantenimiento[...prioridad=1, diaCreacion=110], ...] for-each: P1 P1 P8 P5 P5 forEach: P1 P1 P8 P5 P5 poll: P1 P1 P5 P5 P8 <- el UNICO orden garantizado ... --- Ejecutando todas --- [P1] dia 110 Actualizar ISBN erroneos [P1] dia 130 Reponer DVD danado [P5] dia 95 Limpiar estanterias [P5] dia 100 Revisar ejemplares deteriorados [P8] dia 90 Inventario anual
Tres puntos que resumen la lección. Primero, proximas(2) no vacía la cola porque trabaja sobre una copia del montículo; olvidarlo es el error más frecuente al escribir un "top N". Segundo, el iterador produce P1 P1 P8 P5 P5 —desordenado— mientras que poll produce P1 P1 P5 P5 P8: la demostración visual de que el montículo no es un orden total. Y tercero, el desempate por diaCreacion hace que entre las dos tareas P1 salga primero la del día 110, la más antigua: sin él, el orden sería arbitrario.
Solución 3
package com.nexussoftware.bibliotech.servicio;
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 RecomendadorComparado {
private final Map<String, List<String>> relacionados;
public RecomendadorComparado(Map<String, List<String>> relacionados) {
this.relacionados = relacionados;
}
/**
* ANCHURA (BFS): cola FIFO. Explora por NIVELES: primero todos los
* vecinos directos, luego los vecinos de esos, etc.
*/
public List<String> enAnchura(String origen, int profundidadMaxima) {
return recorrer(origen, profundidadMaxima, true);
}
/**
* PROFUNDIDAD (DFS): pila LIFO. Baja todo lo que puede por una rama
* antes de volver atras. MISMO codigo, solo cambia por que extremo se saca.
*/
public List<String> enProfundidad(String origen, int profundidadMaxima) {
return recorrer(origen, profundidadMaxima, false);
}
private List<String> recorrer(String origen, int profundidadMaxima, boolean anchura) {
List<String> resultado = new ArrayList<>();
Set<String> visitados = new HashSet<>();
Deque<String> porVisitar = new ArrayDeque<>();
Deque<Integer> profundidades = new ArrayDeque<>();
porVisitar.offerLast(origen);
profundidades.offerLast(0);
visitados.add(origen);
while (!porVisitar.isEmpty()) {
// LA UNICA DIFERENCIA entre BFS y DFS esta en esta linea:
// pollFirst -> FIFO -> anchura
// pollLast -> LIFO -> profundidad
String actual = anchura ? porVisitar.pollFirst() : porVisitar.pollLast();
int profundidad = anchura ? profundidades.pollFirst() : profundidades.pollLast();
if (profundidad > 0) { resultado.add(actual); }
if (profundidad >= profundidadMaxima) { continue; }
for (String vecino : relacionados.getOrDefault(actual, List.of())) {
if (visitados.add(vecino)) { // add devuelve false si ya estaba (05-06)
porVisitar.offerLast(vecino);
profundidades.offerLast(profundidad + 1);
}
}
}
return resultado;
}
public static void main(String[] args) {
Map<String, List<String>> grafo = 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")
);
RecomendadorComparado r = new RecomendadorComparado(grafo);
System.out.println("ANCHURA (BFS): " + r.enAnchura("978-0000000001", 3));
System.out.println("PROFUNDIDAD (DFS): " + r.enProfundidad("978-0000000001", 3));
}
}ANCHURA (BFS): [978-0000000002, DVD-0007, 978-0000000003, REV-2024-03] PROFUNDIDAD (DFS): [DVD-0007, REV-2024-03, 978-0000000002, 978-0000000003]
Por qué difieren. La única línea distinta es de dónde se saca el siguiente nodo. Con pollFirst (FIFO), el nodo que lleva más tiempo esperando sale primero, así que se agotan todos los de distancia 1 antes de tocar los de distancia 2: el resultado sale ordenado por cercanía. Con pollLast (LIFO), sale el último añadido, que siempre es el más profundo, así que el algoritmo baja hasta el fondo de una rama antes de volver.
Cuándo interesa cada uno:
| Necesito | Recorrido |
|---|---|
| El camino más corto en número de saltos | Anchura |
| Recomendaciones ordenadas por cercanía | Anchura |
| Explorar todo un subárbol antes de pasar al siguiente | Profundidad |
| Detectar ciclos, ordenar dependencias, resolver laberintos | Profundidad |
| El grafo es muy ancho (pocos niveles, muchos vecinos) | Profundidad (usa menos memoria) |
| El grafo es muy profundo (muchos niveles) | Anchura (evita pilas enormes) |
Que la misma estructura de datos, un Deque, produzca dos algoritmos fundamentalmente distintos según por qué extremo se consuma es una de las ideas más elegantes de la programación, y explica por qué Deque es la interfaz más versátil del Framework de Colecciones.
Conclusión
Has añadido a tu repertorio la familia de colecciones que responde a "¿a quién le toca?". Sabes que una cola FIFO entrega primero al que lleva más esperando y que su restricción —no hay acceso arbitrario— es precisamente lo que garantiza que la política se respete.
Dominas la interfaz Queue con sus dos familias de métodos y, sobre todo, el criterio para elegir entre ellas: add/remove/element lanzan excepción porque tratan el fallo como una anomalía; offer/poll/peek devuelven un valor especial porque tratan la cola vacía como una situación normal. Y tienes el idioma estándar para consumir una cola: while ((x = cola.poll()) != null).
Entiendes ArrayDeque por dentro: un array circular con dos índices que se mueven en lugar de mover los elementos, con capacidad potencia de dos, crecimiento por duplicación y coste amortizado O(1) en ambos extremos. Sabes por qué rechaza los null —porque poll y peek los usan como señal de "vacío"— y por qué es la opción por defecto para colas y pilas: menos memoria, mejor localidad de caché y ninguna presión sobre el recolector frente a LinkedList.
Conoces Deque como la interfaz más versátil del Framework: doce métodos por los dos extremos, más los alias de cola (offer/poll/peek) y de pila (push/pop), lo que le permite ser cola FIFO, pila LIFO o cola de doble extremo con la misma clase. Y sabes que no es una List: no hay get(i).
Manejas PriorityQueue y su montículo binario: la raíz es siempre el mínimo, offer y poll cuestan O(log n) porque el elemento flota o se hunde por un árbol de altura logarítmica, y peek es O(1). Sabes que necesita Comparable o Comparator, que no admite null, que no es estable, y —el detalle que atrapa a todo el mundo— que su iterador, su toString y su forEach no recorren en orden de prioridad, porque el array interno solo cumple la propiedad de montículo. La única forma correcta es vaciarla con poll, o hacerlo sobre una copia si necesitas conservarla.
Sabes que existen las BlockingQueue con sus operaciones take y put que esperan, y que sobre ellas se construye el patrón productor-consumidor, cuya implementación real llega en el módulo 8; y que las colas normales nunca deben compartirse entre hilos sin sincronización. Y has visto sus tres usos canónicos: buffers que absorben diferencias de ritmo, planificación de tareas y recorrido en anchura, donde has descubierto algo notable: cambiar pollFirst por pollLast en una sola línea convierte un BFS en un DFS.
BiblioTech tiene ahora su ColaReservas reescrita sobre ArrayDeque —con reservas urgentes que se cuelan por delante en O(1) gracias al array circular— y una ColaAvisos con PriorityQueue que atiende primero al préstamo con más días de retraso, aplicando el plazo propio de cada tipo de material sin que el código de proceso sepa nada de esa lógica.
En la lección siguiente, Pila, exploras la otra política de acceso: LIFO, el último en entrar es el primero en salir. Verás para qué sirve realmente —deshacer, evaluar expresiones, recorrido en profundidad y la propia pila de llamadas de la JVM, con su conexión con el StackOverflowError y la recursión de 03-03—, por qué la clase Stack heredada está desaconsejada (extiende Vector, está sincronizada y su iterador recorre al revés de lo que esperarías, con demostración incluida), cómo usar Deque como pila y por qué es la recomendación oficial, cómo implementar una pila propia con un array para entender la estructura desde dentro, y dos casos prácticos completos: paréntesis equilibrados e historial de navegación con deshacer y rehacer mediante dos pilas. En BiblioTech aparecerá la pila de operaciones que permite anular la última alta o baja del catálogo.
Curso de Programación en Java
Módulo 1: Introducción a Java
- Introducción a Java
- Configuración del Entorno de Desarrollo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Operadores
- Entrada y Salida por Consola
- Tu Primer Programa Completo: BiblioTech
Módulo 2: Flujo de Control
- Sentencias Condicionales
- Bucles
- Sentencias Switch
- Break y Continue
- Depuración y Trazas de Ejecución
- Proyecto: Menú Interactivo de BiblioTech
Módulo 3: Programación Orientada a Objetos
- Introducción a la POO
- Clases y Objetos
- Métodos
- Constructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- La Clase Object: equals, hashCode y toString
Módulo 4: Programación Orientada a Objetos Avanzada
- Interfaces
- Clases Abstractas
- Clases Internas
- Clases Anónimas
- Expresiones Lambda
- Interfaces Funcionales y Referencias a Métodos
- Enumeraciones y Registros
Módulo 5: Estructuras de Datos y Colecciones
- Arreglos
- El Framework de Colecciones
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cola y Deque
- Pila
- Ordenación y Búsqueda en Colecciones
Módulo 6: Manejo de Excepciones
- Introducción a las Excepciones
- Bloque Try-Catch
- Throw y Throws
- Excepciones Personalizadas
- Bloque Finally
- Try-with-resources y AutoCloseable
- Estrategias de Manejo de Errores y Logging
Módulo 7: Entrada/Salida de Archivos
- Lectura de Archivos
- Escritura de Archivos
- Flujos de Archivos
- BufferedReader y BufferedWriter
- Serialización
- La API NIO.2: Path y Files
- Formatos de Intercambio: CSV y Properties
Módulo 8: Multihilo y Concurrencia
- Introducción al Multihilo
- Creación de Hilos
- Ciclo de Vida de un Hilo
- Sincronización
- Utilidades de Concurrencia
- Colecciones Concurrentes y Variables Atómicas
- Tareas Asíncronas con CompletableFuture
Módulo 9: Redes
- Introducción a las Redes
- Sockets
- ServerSocket
- DatagramSocket y DatagramPacket
- URL y HttpURLConnection
- El Cliente HTTP Moderno
Módulo 10: Temas Avanzados
- Genéricos
- Anotaciones
- Reflexión
- Características de Java 8: Streams y Optional
- Fechas y Horas con java.time
- Java 9 y Más Allá
- Memoria, Recolección de Basura y Rendimiento
Módulo 11: Frameworks y Librerías de Java
- Introducción a los Frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Pruebas Avanzadas con Mockito
- Librerías Esenciales del Ecosistema
