Pilas y colas en Java
En esta lección
Hay problemas en los que no te importa buscar un elemento concreto, sino en qué orden salen. El botón “Deshacer” de un editor recupera siempre lo último que hiciste; la cola de la carnicería atiende siempre a quien llegó primero. Para esos casos Java tiene dos estructuras clásicas: las pilas y las colas. En esta lección aprenderás qué son, cómo usarlas con ArrayDeque, por qué no conviene usar la vieja clase Stack y cuándo necesitas una cola con prioridad (PriorityQueue).
LIFO y FIFO: dos formas de hacer fila
Piensa en dos situaciones del día a día:
- Una pila de platos en la cocina. Pones cada plato limpio encima del anterior y, cuando necesitas uno, coges el de arriba. El último que pusiste es el primero que sale. Esto se llama LIFO (Last In, First Out: el último en entrar es el primero en salir).
- La cola del supermercado. El primero que llega es el primero al que atienden, y los nuevos se ponen al final. Esto se llama FIFO (First In, First Out: el primero en entrar es el primero en salir).
| Estructura | Orden | Se añade por… | Se saca por… | Ejemplos reales |
|---|---|---|---|---|
| Pila (stack) | LIFO | Arriba | Arriba | Deshacer, botón “Atrás” del navegador, platos |
| Cola (queue) | FIFO | El final | El principio | Turnos de una tienda, cola de impresión, mensajes pendientes |
En las dos estructuras no accedes por posición: solo puedes mirar o sacar el elemento de un extremo. Esa limitación es justo lo que las hace útiles, porque el propio código deja claro en qué orden se procesan las cosas.
Las interfaces Queue y Deque
En el framework de colecciones las pilas y las colas se representan con dos interfaces del paquete java.util:
Queue<E>: una cola. Se añade por un extremo y se saca por el otro.Deque<E>(se pronuncia “dek”, de double-ended queue): una cola de doble extremo, en la que puedes añadir y sacar por los dos lados. Por eso sirve tanto de cola como de pila.
La implementación recomendada para ambas es ArrayDeque: es rápida, gasta poca memoria y no tiene límite de tamaño. Como siempre, declara la variable con la interfaz:
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Queue;
Deque<String> pila = new ArrayDeque<>(); // la usarás como pila
Queue<String> cola = new ArrayDeque<>(); // la usarás como cola
Pilas con ArrayDeque
Una pila tiene tres operaciones básicas:
| Método | Qué hace | Si la pila está vacía |
|---|---|---|
push(x) | Pone x arriba | — |
pop() | Quita y devuelve el de arriba | Lanza NoSuchElementException |
peek() | Devuelve el de arriba sin quitarlo | Devuelve null |
Vamos con la pila de platos:
Deque<String> platos = new ArrayDeque<>();
platos.push("plato azul");
platos.push("plato rojo");
platos.push("plato verde");
System.out.println(platos); // [plato verde, plato rojo, plato azul]
System.out.println(platos.peek()); // plato verde
System.out.println(platos.pop()); // plato verde
System.out.println(platos.pop()); // plato rojo
System.out.println(platos); // [plato azul]
System.out.println(platos.size()); // 1
Fíjate en que println muestra la pila empezando por arriba: el primer elemento que ves es el que saldrá con el próximo pop.
Lo habitual es vaciar una pila con un while que comprueba isEmpty(). Así funciona, simplificado, el botón “Atrás” de un navegador:
Deque<String> historial = new ArrayDeque<>();
historial.push("inicio");
historial.push("productos");
historial.push("carrito");
while (!historial.isEmpty()) {
System.out.println("Volver desde: " + historial.pop());
}
// Volver desde: carrito
// Volver desde: productos
// Volver desde: inicio
System.out.println(historial.peek()); // null
Ejemplo: deshacer en un editor de texto
Cada vez que el usuario escribe algo, guardamos ese trozo en una pila. Al pulsar “Deshacer”, sacamos el último trozo y lo borramos del texto:
StringBuilder texto = new StringBuilder();
Deque<String> deshacer = new ArrayDeque<>();
String[] escrituras = {"Hola", ", ", "mundo", "!!!"};
for (String trozo : escrituras) {
texto.append(trozo);
deshacer.push(trozo);
}
System.out.println(texto); // Hola, mundo!!!
for (int i = 0; i < 2; i++) { // el usuario pulsa Deshacer dos veces
String ultimo = deshacer.pop();
texto.setLength(texto.length() - ultimo.length());
System.out.println("Deshecho \"" + ultimo + "\" -> " + texto);
}
// Deshecho "!!!" -> Hola, mundo
// Deshecho "mundo" -> Hola,
setLength recorta el StringBuilder (lo viste en cadenas). Un editor real guardaría acciones más complejas (borrar, pegar, cambiar formato), pero la idea es la misma: lo último que hiciste es lo primero que se deshace, y eso es una pila.
Ejemplo: paréntesis equilibrados
Un problema clásico de examen y de entrevista: comprobar si en un texto los paréntesis, corchetes y llaves están bien abiertos y cerrados. El compilador de Java hace algo parecido con tu código.
La idea: recorremos el texto carácter a carácter. Cada vez que algo se abre, lo apilamos. Cada vez que algo se cierra, tiene que coincidir con lo último que se abrió, que está arriba de la pila.
static boolean equilibrado(String texto) {
Deque<Character> pila = new ArrayDeque<>();
for (char c : texto.toCharArray()) {
switch (c) {
case '(', '[', '{' -> pila.push(c);
case ')', ']', '}' -> {
if (pila.isEmpty()) {
return false; // cierra algo que nunca se abrió
}
char abierto = pila.pop();
if ((c == ')' && abierto != '(')
|| (c == ']' && abierto != '[')
|| (c == '}' && abierto != '{')) {
return false; // no casan: ( con ] por ejemplo
}
}
default -> { } // el resto de caracteres no importa
}
}
return pila.isEmpty(); // si queda algo, faltan cierres
}
System.out.println(equilibrado("(a + b) * [c - d]")); // true
System.out.println(equilibrado("{ lista[(i + 1)] }")); // true
System.out.println(equilibrado("(a + b]")); // false
System.out.println(equilibrado("((x)")); // false
System.out.println(equilibrado("x)(")); // false
Hay tres formas de fallar y el método comprueba las tres: un cierre sin apertura (pila vacía), un cierre que no casa con su apertura, y aperturas que se quedan sin cerrar (la pila no termina vacía). La pila es de Character y no de char porque las colecciones solo guardan objetos; Java convierte entre ambos automáticamente.
Colas con ArrayDeque
Para una cola FIFO se usan estos métodos de Queue:
| Método | Qué hace | Si la cola está vacía |
|---|---|---|
offer(x) | Añade x al final | — |
poll() | Quita y devuelve el primero | Devuelve null |
peek() | Devuelve el primero sin quitarlo | Devuelve null |
Simulemos los turnos del mostrador de una tienda:
Queue<String> turnos = new ArrayDeque<>();
turnos.offer("Carmen");
turnos.offer("Iván");
turnos.offer("Rosa");
System.out.println(turnos); // [Carmen, Iván, Rosa]
System.out.println("Siguiente: " + turnos.peek()); // Siguiente: Carmen
System.out.println("Atendiendo a " + turnos.poll()); // Atendiendo a Carmen
turnos.offer("Óscar"); // llega alguien nuevo: al final
System.out.println(turnos); // [Iván, Rosa, Óscar]
while (!turnos.isEmpty()) {
System.out.println("Atendiendo a " + turnos.poll());
}
// Atendiendo a Iván
// Atendiendo a Rosa
// Atendiendo a Óscar
System.out.println(turnos.poll()); // null
Óscar llegó después que Iván y Rosa, así que se le atiende después aunque se añadiera a mitad del proceso. Eso es lo que garantiza una cola.
Dos familias de métodos: excepción o valor especial
Queue tiene cada operación repetida en dos versiones. Se diferencian en qué hacen cuando no pueden cumplir su trabajo:
| Operación | Lanza excepción | Devuelve valor especial |
|---|---|---|
| Añadir | add(x) | offer(x) → false |
| Sacar | remove() | poll() → null |
| Consultar | element() | peek() → null |
Queue<String> vacia = new ArrayDeque<>();
System.out.println(vacia.poll()); // null
System.out.println(vacia.peek()); // null
vacia.remove(); // NoSuchElementException
Con ArrayDeque, add y offer siempre funcionan porque no tiene límite. La diferencia importa en colas con capacidad máxima, como las que se usan para repartir trabajo entre hilos.
Consejo: usa
offer,pollypeeky comprueba elnulloisEmpty(). Es más claro que capturar excepciones. Para la pila,pop()lanza excepción si está vacía, así que compruebaisEmpty()antes.
Deque: añadir y sacar por los dos extremos
Como ArrayDeque es de doble extremo, tiene versiones “First” y “Last” de cada método:
| Al principio | Al final |
|---|---|
offerFirst(x) | offerLast(x) |
pollFirst() | pollLast() |
peekFirst() | peekLast() |
De hecho, push, pop y peek de la pila son atajos de offerFirst, pollFirst y peekFirst; y offer/poll de la cola son offerLast/pollFirst. Un ejemplo en el que hace falta usar los dos extremos: en la cola de una consulta, quien tiene cita previa pasa delante, y quien se cansa de esperar se va por el final.
Deque<String> fila = new ArrayDeque<>();
fila.offerLast("Ana");
fila.offerLast("Bruno");
fila.offerFirst("Clara (cita previa)");
System.out.println(fila); // [Clara (cita previa), Ana, Bruno]
System.out.println(fila.pollLast()); // Bruno (se cansa y se va)
System.out.println(fila.peekFirst() + " / " + fila.peekLast()); // Clara (cita previa) / Ana
Cuidado: no mezcles los dos estilos con la misma variable. Si una
Dequees tu pila, usa solopush/pop/peek; si es tu cola, solooffer/poll/peek. Mezclarlos produce órdenes muy difíciles de seguir.
Por qué no usar la clase Stack
En apuntes y código antiguo verás la clase Stack. Funciona, pero hoy no se recomienda, y la propia documentación de Java aconseja ArrayDeque en su lugar. Los motivos:
- Hereda de
Vector, una lista antigua. Eso significa que tiene métodos comoget(i)oadd(posicion, x)que permiten saltarse el orden de la pila, justo lo que una pila debería impedir. - Es más lenta, porque todos sus métodos están sincronizados (preparados para varios hilos a la vez) aunque no lo necesites.
- Su orden al recorrerla o imprimirla es el contrario al de
ArrayDeque: empieza por abajo.
Stack<String> vieja = new Stack<>();
vieja.push("a"); vieja.push("b"); vieja.push("c");
Deque<String> nueva = new ArrayDeque<>();
nueva.push("a"); nueva.push("b"); nueva.push("c");
System.out.println(vieja); // [a, b, c] (de abajo arriba)
System.out.println(nueva); // [c, b, a] (de arriba abajo)
System.out.println(vieja.get(0)); // a (acceso por posición: rompe la idea de pila)
vieja.add(0, "intruso");
System.out.println(vieja); // [intruso, a, b, c]
Las operaciones push, pop y peek se llaman igual en las dos, así que cambiar código antiguo a ArrayDeque suele ser tan sencillo como cambiar la declaración.
PriorityQueue: colas con prioridad
En urgencias de un hospital no se atiende por orden de llegada, sino por gravedad. Para eso existe PriorityQueue, una cola en la que poll() saca siempre el elemento más prioritario, en lugar del más antiguo.
Por defecto, el más prioritario es el menor según el orden natural:
PriorityQueue<Integer> numeros = new PriorityQueue<>(List.of(42, 7, 19, 3, 25));
while (!numeros.isEmpty()) {
System.out.print(numeros.poll() + " ");
}
// 3 7 19 25 42
Con objetos propios hay que decirle cómo compararlos pasándole un Comparator al crearla. Aquí queremos que salga primero el de mayor gravedad:
record Paciente(String nombre, int gravedad) {}
PriorityQueue<Paciente> urgencias = new PriorityQueue<>(
Comparator.comparingInt(Paciente::gravedad).reversed());
urgencias.offer(new Paciente("Lola", 2));
urgencias.offer(new Paciente("Tomás", 5));
urgencias.offer(new Paciente("Nerea", 1));
urgencias.offer(new Paciente("Javi", 4));
System.out.println(urgencias.peek().nombre()); // Tomás
while (!urgencias.isEmpty()) {
Paciente p = urgencias.poll();
System.out.println(p.nombre() + " (gravedad " + p.gravedad() + ")");
}
// Tomás (gravedad 5)
// Javi (gravedad 4)
// Lola (gravedad 2)
// Nerea (gravedad 1)
Los comparadores (comparingInt, reversed, thenComparing…) se explican a fondo en Comparable y Comparator.
Cuidado: una
PriorityQueueno está ordenada por dentro. Solo garantiza quepeek()ypoll()dan el más prioritario. Si la imprimes o la recorres confor-each, el orden parece aleatorio:System.out.println(new PriorityQueue<>(List.of(42, 7, 19, 3, 25)))muestra[3, 7, 19, 42, 25]. Para procesarla en orden, sácalos conpoll().
¿Y LinkedList?
LinkedList implementa a la vez List y Deque, así que puede hacer de lista, de pila y de cola. La verás a menudo en ejemplos como Queue<String> cola = new LinkedList<>();. Diferencias con ArrayDeque:
ArrayDeque | LinkedList | |
|---|---|---|
| Velocidad como pila/cola | Más rápida | Más lenta |
| Memoria | Menos | Más (cada elemento guarda enlaces al anterior y al siguiente) |
Acceso por posición get(i) | No | Sí (pero lento) |
Admite null | No (NullPointerException) | Sí |
Elige ArrayDeque para pilas y colas. LinkedList solo tiene sentido si además necesitas que sea una List o guardar null, cosa que conviene evitar porque poll() y peek() usan null para decir “vacía”.
Cuál elegir
| Necesitas… | Usa |
|---|---|
| Una pila (LIFO) | Deque<T> p = new ArrayDeque<>(); con push, pop, peek |
| Una cola (FIFO) | Queue<T> c = new ArrayDeque<>(); con offer, poll, peek |
| Añadir y sacar por los dos lados | Deque<T> con offerFirst, pollLast… |
| Atender por prioridad | PriorityQueue<T> con un Comparator |
| Buscar por posición | Una lista, no una pila ni una cola |
Errores frecuentes
- Usar
Stacken código nuevo. Funciona, pero es antigua, lenta y permite saltarse el orden. UsaArrayDeque. - Llamar a
pop()sobre una pila vacía. LanzaNoSuchElementException. Comprueba antesisEmpty(). - Confundir
popypeek.peeksolo mira; si lo usas en unwhile (!pila.isEmpty())en lugar depop, el bucle no termina nunca. - Mezclar métodos de pila y de cola en la misma
Deque. Los elementos acaban saliendo en un orden que nadie espera. - Esperar que una
PriorityQueueimpresa salga ordenada. Solopoll()respeta la prioridad. - Guardar
nullen unArrayDeque. LanzaNullPointerException. - Usar
Deque<char>oQueue<int>. No compila: sonDeque<Character>yQueue<Integer>.
Resumen
| Concepto | Código |
|---|---|
| Crear pila | Deque<String> p = new ArrayDeque<>(); |
| Apilar / desapilar / mirar | p.push(x), p.pop(), p.peek() |
| Crear cola | Queue<String> c = new ArrayDeque<>(); |
| Encolar / sacar / mirar | c.offer(x), c.poll(), c.peek() |
| ¿Está vacía? | isEmpty(), size() |
| Doble extremo | offerFirst, offerLast, pollFirst, pollLast |
| Cola con prioridad | new PriorityQueue<>(comparador) |
| LIFO | Pila: el último en entrar sale primero |
| FIFO | Cola: el primero en entrar sale primero |
En la siguiente lección aprenderás a escribir tus propias clases y métodos que funcionan con cualquier tipo, como hacen ArrayDeque<E> o List<E>: genéricos.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.