LinkedList es la segunda implementación de List del JDK y probablemente la más malinterpretada de todo el Framework. La frase que se repite en tutoriales, en entrevistas de trabajo y en comentarios de código es siempre la misma: "usa ArrayList para acceder por índice y LinkedList para insertar y borrar, que es O(1)". Esa frase es, tal como suena, falsa, y creerla lleva a escribir código considerablemente más lento pensando que se está optimizando.
En esta lección vas a entender exactamente qué es una lista doblemente enlazada, qué operaciones son realmente O(1) y bajo qué condición precisa, y por qué en la práctica —incluso en los casos que parecen su terreno— ArrayList suele ganar. También verás dónde LinkedList sí tiene sentido: no tanto como List, sino como Deque, es decir, como cola de doble extremo. Y terminarás con el único caso de BiblioTech donde su semántica encaja de verdad: la cola de reservas pendientes, que se consume por un extremo y crece por el otro.
Esta lección no va solo de una clase. Va de aprender a razonar honestamente sobre estructuras de datos, distinguiendo lo que dice la teoría de lo que hace el procesador.
Contenido
- La estructura: nodos doblemente enlazados
- Qué implica para la memoria y para la caché
- El matiz que casi todo el mundo explica mal
ArrayListfrente aLinkedList, operación por operación- La conclusión honesta y actual
LinkedListcomoDequeListIterator: recorrido bidireccional e inserción- Medir el rendimiento de forma honesta
- Cuándo
LinkedListsí es la elección correcta: la cola de reservas - Errores Comunes y Consejos
- Ejercicios
- La estructura: nodos doblemente enlazados
Un ArrayList guarda los elementos en un bloque contiguo de memoria. Una LinkedList no guarda nada contiguo: guarda una cadena de nodos, cada uno en su propio rincón del heap, unidos por referencias.
Este es el nodo real del JDK, y cabe en cinco líneas:
private static class Node<E> {
E item; // el elemento que transporta
Node<E> next; // referencia al nodo SIGUIENTE (null si es el ultimo)
Node<E> prev; // referencia al nodo ANTERIOR (null si es el primero)
}Y la lista en sí guarda solo tres campos:
transient int size = 0;
transient Node<E> first; // primer nodo
transient Node<E> last; // ultimo nodoflowchart LR
LL["LinkedList<br/>size = 4<br/>first, last"]
N1["null ← prev<br/><b>Java Efectivo</b><br/>next →"]
N2["← prev<br/><b>Patrones de Diseno</b><br/>next →"]
N3["← prev<br/><b>Refactorizacion</b><br/>next →"]
N4["← prev<br/><b>Java Magazine</b><br/>next → null"]
LL -.->|first| N1
LL -.->|last| N4
N1 --> N2
N2 --> N3
N3 --> N4
N4 -.-> N3
N3 -.-> N2
N2 -.-> N1
Las flechas continuas son los next; las punteadas, los prev. Que haya enlaces en ambos sentidos es lo que la hace doblemente enlazada, y tiene dos consecuencias importantes: se puede recorrer hacia atrás, y desde un nodo se puede eliminar ese nodo sin conocer el anterior.
De esta estructura se deducen inmediatamente sus dos características fundamentales:
No hay índices. Los nodos no están numerados ni colocados en posiciones calculables. Para llegar al elemento 500 hay que empezar en first y saltar 500 veces. No existe forma de hacerlo más rápido, y ahí está la debilidad estructural de LinkedList.
No hay capacidad. Cada nodo se crea cuando hace falta y se descarta cuando se elimina. No hay redimensionados, no hay copias, no hay array interno que llenar. Añadir el elemento un millón cuesta exactamente lo mismo que añadir el segundo.
Ver cómo se inserta un nodo en medio explica el resto:
flowchart LR
subgraph antes["ANTES"]
direction LR
A1["Nodo A"] --> A2["Nodo B"]
end
subgraph despues["DESPUES de insertar X entre A y B"]
direction LR
B1["Nodo A"] --> BX["Nodo X"]
BX --> B2["Nodo B"]
end
La operación consiste en crear el nodo X y cambiar cuatro referencias: A.next = X, X.prev = A, X.next = B, B.prev = X. Cuatro asignaciones. Ningún elemento se mueve, ni el primero ni el millonésimo. Eso es lo que quiere decir "inserción O(1)". Y ahora viene el matiz.
- Qué implica para la memoria y para la caché
Antes del matiz, dos costes que las tablas de complejidad no muestran y que en la práctica pesan más que la propia O().
Coste de memoria
En un ArrayList, cada elemento ocupa una referencia en el array interno: 4 bytes con compresión de punteros, 8 sin ella.
En una LinkedList, cada elemento necesita un objeto Node completo:
| Componente del nodo | Bytes aproximados (JVM de 64 bits con punteros comprimidos) |
|---|---|
| Cabecera del objeto | 12 |
item (referencia al elemento) |
4 |
next (referencia) |
4 |
prev (referencia) |
4 |
| Relleno de alineación | 4 |
| Total por elemento | ~28 bytes |
Frente a los ~4 bytes por elemento de un ArrayList bien dimensionado, eso es unas siete veces más memoria solo en infraestructura, sin contar los objetos apuntados. Para un millón de elementos: unos 4 MB frente a unos 28 MB. Y cada nodo es un objeto que el recolector de basura tiene que rastrear (módulo 10-07).
Coste de localidad de caché
Este es el factor decisivo y el que más gente ignora. Como explicaba 05-01, el procesador no lee memoria byte a byte: trae bloques de 64 bytes a su caché. En un ArrayList, leer el elemento 0 trae también los 15 siguientes gratis; recorrer la lista es prácticamente lectura secuencial, el patrón que más le gusta al hardware.
En una LinkedList, cada nodo puede estar en cualquier parte del heap. Si los creaste en orden puede que estén cerca, pero después de un tiempo de altas y bajas, y tras varias pasadas del recolector, quedan dispersos. Recorrer significa entonces un salto impredecible de memoria por elemento, y cada salto que falla en caché cuesta del orden de cien veces más que un acceso en caché.
flowchart TB
subgraph AL["ArrayList: recorrido secuencial"]
direction LR
X0["e0"] --- X1["e1"] --- X2["e2"] --- X3["e3"] --- X4["e4"]
end
subgraph LL["LinkedList: saltos por todo el heap"]
direction LR
Y0["nodo0"] -.-> Y3["nodo1"]
Y3 -.-> Y1["nodo2"]
Y1 -.-> Y4["nodo3"]
Y4 -.-> Y2["nodo4"]
end
El resultado medido una y otra vez: recorrer una LinkedList suele ser entre 2 y 10 veces más lento que recorrer un ArrayList del mismo tamaño, aunque ambas operaciones sean O(n). La notación O() ignora las constantes, y aquí la constante importa muchísimo.
- El matiz que casi todo el mundo explica mal
Repitamos la afirmación popular: "insertar y borrar en LinkedList es O(1)".
Lo correcto es: insertar y borrar es O(1) SI YA TIENES LA POSICIÓN, es decir, si ya tienes en la mano una referencia al nodo (o estás en un extremo). Llegar a esa posición cuesta O(n).
Míralo en el código real del JDK:
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size) { linkLast(element); }
else { linkBefore(element, node(index)); } // node(index) es el problema
}
Node<E> node(int index) {
if (index < (size >> 1)) { // si esta en la primera mitad
Node<E> x = first;
for (int i = 0; i < index; i++) { x = x.next; } // O(n) saltos
return x;
} else { // si esta en la segunda mitad
Node<E> x = last;
for (int i = size - 1; i > index; i--) { x = x.prev; }
return x;
}
}linkBefore es O(1): cuatro asignaciones. Pero node(index) recorre hasta la mitad de la lista. El método completo es O(n). La optimización de empezar por el extremo más cercano solo divide entre dos: sigue siendo O(n).
Consecuencia directa: este bucle, que parece razonable, es un desastre.
List<Integer> lista = new LinkedList<>();
for (int i = 0; i < 100_000; i++) { lista.add(i); }
// "Inserto en el medio, que en LinkedList es O(1)"... NO
for (int i = 0; i < 1000; i++) {
lista.add(lista.size() / 2, i); // cada llamada recorre 50.000 nodos: O(n)
}Mil llamadas × 50 000 saltos = 50 millones de saltos de puntero. El mismo bucle con ArrayList haría mil System.arraycopy de 50 000 referencias, que es la misma O() pero ejecutada por una instrucción de copia de bloque optimizada por el hardware: en la práctica, varias veces más rápido.
Cuándo sí es O(1) de verdad
Solo en tres situaciones:
1. En los extremos. addFirst, addLast, removeFirst, removeLast acceden directamente a first y last. Genuinamente O(1), sin recorrido.
LinkedList<String> cola = new LinkedList<>();
cola.addLast("Marta Ruiz"); // O(1) real
cola.removeFirst(); // O(1) real2. Con un iterador que ya está en la posición.
ListIterator<Material> it = lista.listIterator();
while (it.hasNext()) {
Material m = it.next();
if (m.getTitulo().startsWith("Java")) {
it.add(new Nota("Revisar")); // O(1) REAL: el iterador ya esta ahi
}
}El recorrido completo es O(n), pero cada inserción individual es O(1), y no hay desplazamiento de elementos. Aquí LinkedList sí es teóricamente superior a ArrayList, donde cada add(i, e) desplazaría el resto.
3. Iterator.remove() durante un recorrido. Mismo razonamiento: el iterador ya tiene el nodo.
La regla resumida, que conviene memorizar:
En
LinkedListla operación es barata; llegar al sitio es caro. EnArrayListllegar al sitio es gratis; la operación es cara.
Y como "llegar al sitio" es lo que hace cualquier método indexado, el resultado neto casi siempre favorece a ArrayList.
ArrayList frente a LinkedList, operación por operación
ArrayList frente a LinkedList, operación por operación| Operación | ArrayList |
LinkedList |
Quién gana en la práctica |
|---|---|---|---|
get(i) / set(i, e) |
O(1) | O(n) | ArrayList, sin discusión |
add(e) al final |
O(1) amortizado | O(1) | Empate; ArrayList suele ser más rápido por caché |
add(0, e) al principio |
O(n) | O(1) | LinkedList en teoría; ArrayDeque mejor que ambos |
add(i, e) en medio |
O(n) desplazamiento | O(n) recorrido + O(1) | ArrayList: el arraycopy es más rápido que saltar nodos |
remove(size()-1) |
O(1) | O(1) | Empate |
remove(0) |
O(n) | O(1) | LinkedList; ArrayDeque mejor que ambos |
remove(i) |
O(n) | O(n) | ArrayList |
remove(Object) |
O(n) | O(n) | ArrayList |
contains / indexOf |
O(n) | O(n) | ArrayList (mucho mejor caché) |
Recorrer con for-each |
O(n) rápido | O(n) lento | ArrayList, 2-10× |
Recorrer con for indexado |
O(n) | O(n²) | ArrayList; en LinkedList es un error grave |
Insertar con ListIterator |
O(n) por inserción | O(1) por inserción | LinkedList |
addFirst / pollFirst |
No existen en List |
O(1) | LinkedList (o ArrayDeque) |
| Memoria por elemento | ~4-8 bytes | ~28 bytes | ArrayList |
sort |
O(n log n) directo | O(n log n) + volcar a array y reconstruir | ArrayList |
Presta especial atención a la fila del for indexado, porque es un error de rendimiento que se cuela con facilidad:
// Sobre una LinkedList de 100.000 elementos: cada get(i) recorre hasta i nodos.
// Total: 1 + 2 + 3 + ... + 100.000 = unos 5.000 millones de saltos.
for (int i = 0; i < lista.size(); i++) {
procesar(lista.get(i)); // O(n^2) DISFRAZADO
}
// Correcto en ambas implementaciones:
for (Material m : lista) { // O(n): el iterador avanza de nodo en nodo
procesar(m);
}Si el parámetro de tu método es List y no sabes qué implementación te llegará, recorre siempre con for-each. Esa es una de las razones por las que existe la interfaz marcadora RandomAccess (05-03): permite a un algoritmo genérico preguntar si el acceso indexado es barato.
if (lista instanceof RandomAccess) {
for (int i = 0; i < lista.size(); i++) { procesar(lista.get(i)); }
} else {
for (Material m : lista) { procesar(m); }
}En tu código no escribirás esto casi nunca —usa for-each y ya está—, pero es útil saber por qué existe.
- La conclusión honesta y actual
Es la parte de la lección que más se aparta del discurso habitual, así que va con nombres y argumentos.
En la práctica, ArrayList gana casi siempre. Incluso en escenarios que parecen favorecer a LinkedList, las mediciones repetidas en JVM modernas dan la victoria al array, porque:
- La localidad de caché domina. La diferencia entre un acceso en caché L1 y un fallo que va a memoria principal es de dos órdenes de magnitud. Ninguna ventaja algorítmica sobrevive a eso cuando la O() es la misma.
System.arraycopyes una instrucción nativa que copia bloques de memoria a velocidad de hardware. Desplazar 10 000 referencias contiguas es sorprendentemente barato; seguir 10 000 punteros dispersos, no.- Los nodos presionan al recolector. Un millón de elementos son un millón de objetos
Nodeque rastrear, frente a un solo array. - Las inserciones "en medio" casi nunca son en medio de verdad. En el código real se inserta al final, o se elimina por criterio con
removeIf, o se ordena. Todas esas operaciones favorecen alArrayList.
Esta es también la opinión pública de Joshua Bloch, autor de Java Efectivo y coautor del propio Framework de Colecciones, que ha llegado a decir que a día de hoy LinkedList no aporta valor suficiente para justificar su uso general.
¿Entonces LinkedList no sirve para nada? Sirve, pero no como List: como Deque. Cuando lo que quieres es una cola o una pila —añadir por un extremo, sacar por el otro— LinkedList cumple con O(1) real y sin capacidad que gestionar.
Y aun así, en ese terreno tiene un competidor mejor: ArrayDeque, que verás en 05-07, y que es más rápido y consume menos memoria porque usa un array circular. La recomendación oficial del JDK para colas y pilas es ArrayDeque, no LinkedList.
El resumen práctico, sin rodeos:
| Necesito... | Usa |
|---|---|
| Una lista | ArrayList |
| Una cola o una pila | ArrayDeque (05-07, 05-08) |
Insertar mucho en medio recorriendo con ListIterator |
LinkedList (caso raro pero legítimo) |
Una List que además sea Deque en la misma variable |
LinkedList |
Una cola que admita null |
LinkedList (ArrayDeque los rechaza) |
Que sigas aprendiendo LinkedList tiene tres motivos sólidos: la vas a encontrar en código existente, es el ejemplo canónico de lista enlazada —una estructura que aparece en mil sitios— y entender por qué no es la respuesta te enseña a razonar sobre rendimiento mejor que cualquier regla memorizada.
LinkedList como Deque
LinkedList como DequeLinkedList implementa dos interfaces a la vez, y ahí está su rasgo distintivo:
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.SerializableEs la única clase del JDK que es List y Deque simultáneamente. Eso le da acceso a toda la familia de operaciones por los extremos, todas genuinamente O(1):
| Método | Qué hace | Si está vacía |
|---|---|---|
addFirst(e) / offerFirst(e) |
Inserta al principio | — |
addLast(e) / offerLast(e) |
Inserta al final | — |
getFirst() / getLast() |
Consulta sin quitar | NoSuchElementException |
peekFirst() / peekLast() |
Consulta sin quitar | Devuelve null |
removeFirst() / removeLast() |
Quita y devuelve | NoSuchElementException |
pollFirst() / pollLast() |
Quita y devuelve | Devuelve null |
peek() / poll() |
Alias de peekFirst/pollFirst (semántica de cola) |
null |
push(e) / pop() |
Alias de addFirst/removeFirst (semántica de pila) |
pop: NoSuchElementException |
La distinción entre las dos familias —lanzar excepción o devolver null— es una decisión de diseño importante de Queue y Deque, y se explica a fondo en 05-07. Por ahora quédate con la regla: usa peek/poll/offer cuando la colección vacía sea una situación normal; usa getFirst/removeFirst/addFirst cuando esté vacía signifique que algo va mal.
Ejemplo con las dos semánticas sobre la misma clase:
LinkedList<String> cola = new LinkedList<>();
// Como COLA (FIFO): entra por el final, sale por el principio
cola.addLast("Marta Ruiz");
cola.addLast("Diego Alonso");
cola.addLast("Nuria Vidal");
System.out.println(cola.pollFirst()); // Marta Ruiz (la primera que llego)
System.out.println(cola); // [Diego Alonso, Nuria Vidal]
// Como PILA (LIFO): entra y sale por el mismo extremo
LinkedList<String> pila = new LinkedList<>();
pila.push("alta");
pila.push("baja");
pila.push("modificacion");
System.out.println(pila.pop()); // modificacion (la ultima que entro)
System.out.println(pila); // [baja, alta]Fíjate en el tipo de la variable: aquí está declarada como LinkedList porque necesitamos métodos que List no tiene. Si solo vas a usarla como cola, lo correcto según la regla de oro de 05-02 es declararla por la interfaz que refleje su uso:
El uso de cola y pila en detalle son las lecciones 05-07 y 05-08.
ListIterator: recorrido bidireccional e inserción
ListIterator: recorrido bidireccional e inserciónListIterator es una extensión de Iterator exclusiva de las listas, y es la forma correcta de insertar o sustituir elementos mientras recorres. Su API:
| Método | Qué hace |
|---|---|
hasNext() / next() |
Avanzar, como en Iterator |
hasPrevious() / previous() |
Retroceder |
nextIndex() / previousIndex() |
Posición del siguiente / anterior |
add(E e) |
Inserta en la posición actual, antes del que devolvería next() |
set(E e) |
Sustituye el último devuelto por next() o previous() |
remove() |
Elimina el último devuelto |
La clave conceptual: un ListIterator no señala un elemento, señala un hueco entre elementos (un cursor). next() salta el elemento a su derecha y devuelve su valor; previous() salta el de su izquierda.
flowchart LR
P0["^0"] --- A["A"] --- P1["^1"] --- B["B"] --- P2["^2"] --- C["C"] --- P3["^3"]
Los ^ son las posiciones posibles del cursor. Con el cursor en ^1, next() devuelve B y deja el cursor en ^2; previous() devolvería A y lo dejaría en ^0.
Insertar mientras recorres
Este es el uso que justifica su existencia:
List<String> operaciones = new LinkedList<>(
List.of("alta:978-0000000001", "baja:978-0000000002", "alta:978-0000000003"));
ListIterator<String> it = operaciones.listIterator();
while (it.hasNext()) {
String op = it.next();
if (op.startsWith("baja:")) {
it.add("aviso:revisar-" + op.substring(5)); // se inserta DESPUES del actual
}
}
System.out.println(operaciones);Tres detalles que hay que entender:
it.add(x)inserta en la posición del cursor, que trasnext()está justo después del elemento devuelto. Por eso el aviso aparece detrás de la baja.- El elemento insertado no se vuelve a visitar.
addavanza el cursor por encima de lo insertado, así que no hay bucle infinito. Compruébalo: siaddno lo hiciera, el nuevo elemento se examinaría y podría generar otro, indefinidamente. - No hay
ConcurrentModificationException. El iterador es quien modifica, así que actualiza suexpectedModCount(05-02).
Intentar lo mismo con un for-each y lista.add(...) daría la excepción de inmediato. Y hacerlo con índices sobre un ArrayList obligaría a recalcular la posición tras cada inserción, un clásico generador de fallos de límite.
Sustituir mientras recorres
ListIterator<String> it = nombres.listIterator();
while (it.hasNext()) {
String n = it.next();
if (n.isBlank()) {
it.set("(sin nombre)"); // sustituye el ultimo devuelto por next()
}
}Equivalente a replaceAll cuando la condición es simple, pero permite lógica arbitraria y decidir elemento a elemento.
Recorrer hacia atrás
// listIterator(size) coloca el cursor al FINAL
ListIterator<Material> it = catalogo.listIterator(catalogo.size());
while (it.hasPrevious()) {
Material m = it.previous();
System.out.println(m.getTitulo());
}Sobre una LinkedList esto es eficiente gracias a los enlaces prev; sobre un ArrayList también, porque el acceso indexado es O(1). En ambos casos es más claro que un for decreciente cuando además necesitas insertar o eliminar.
Aviso importante: mezclar un ListIterator con modificaciones directas de la lista rompe todo. Mientras un iterador esté vivo, todos los cambios deben pasar por él.
- Medir el rendimiento de forma honesta
Vamos a comparar las dos implementaciones con un experimento sencillo. Antes, una advertencia que hay que tomarse en serio.
Los microbenchmarks en Java son traicioneros. La JVM compila el código a medida que se ejecuta (JIT), así que las primeras iteraciones son mucho más lentas que las siguientes; el recolector de basura puede saltar en mitad de la medición; y el compilador puede eliminar por completo un bucle cuyo resultado no se usa, dándote tiempos de cero. La herramienta correcta es JMH (Java Microbenchmark Harness), la librería oficial de OpenJDK, que gestiona el calentamiento, las iteraciones y el consumo de resultados. Lo que sigue es una aproximación ilustrativa, útil para ver órdenes de magnitud, no para publicar cifras.
package com.nexussoftware.bibliotech.presentacion;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class ComparativaListas {
private static final int N = 100_000;
public static void main(String[] args) {
// Calentamiento: dejamos que el JIT compile antes de medir
for (int i = 0; i < 3; i++) { medirTodo(false); }
System.out.println("=== Medicion (N = " + N + ") ===");
medirTodo(true);
}
private static void medirTodo(boolean imprimir) {
List<Integer> array = new ArrayList<>();
List<Integer> enlaza = new LinkedList<>();
long t1 = medir(() -> { for (int i = 0; i < N; i++) { array.add(i); } });
long t2 = medir(() -> { for (int i = 0; i < N; i++) { enlaza.add(i); } });
long t3 = medir(() -> { long s = 0; for (int i = 0; i < N; i++) { s += array.get(i); } consumir(s); });
long t4 = medir(() -> { long s = 0; for (Integer v : array) { s += v; } consumir(s); });
long t5 = medir(() -> { long s = 0; for (Integer v : enlaza) { s += v; } consumir(s); });
List<Integer> a2 = new ArrayList<>(array);
List<Integer> l2 = new LinkedList<>(array);
long t6 = medir(() -> { for (int i = 0; i < 20_000; i++) { a2.add(0, i); } });
long t7 = medir(() -> { for (int i = 0; i < 20_000; i++) { l2.add(0, i); } });
if (imprimir) {
System.out.printf("add al final ArrayList %6d ms | LinkedList %6d ms%n", t1, t2);
System.out.printf("get(i) indexado ArrayList %6d ms | LinkedList (NO se mide: O(n^2))%n", t3);
System.out.printf("recorrer foreach ArrayList %6d ms | LinkedList %6d ms%n", t4, t5);
System.out.printf("add(0, e) x20000 ArrayList %6d ms | LinkedList %6d ms%n", t6, t7);
}
}
private static long medir(Runnable tarea) {
long inicio = System.nanoTime();
tarea.run();
return (System.nanoTime() - inicio) / 1_000_000; // a milisegundos
}
/** Impide que el JIT elimine el bucle por considerar su resultado inutil. */
private static void consumir(long valor) {
if (valor == Long.MIN_VALUE) { System.out.print(""); }
}
}Resultados típicos en una máquina de escritorio, con el orden de magnitud como única información fiable:
| Operación (N = 100 000) | ArrayList |
LinkedList |
Comentario |
|---|---|---|---|
add al final |
~3 ms | ~5 ms | Empate práctico; el array gana por caché |
get(i) en bucle indexado |
~1 ms | minutos | O(n²): ni se mide |
Recorrer con for-each |
~1 ms | ~6 ms | 6× más lento, misma O() |
add(0, e) × 20 000 |
~120 ms | ~2 ms | Aquí sí gana LinkedList... |
addFirst × 20 000 en ArrayDeque |
~1 ms | — | ...pero ArrayDeque gana a las dos |
La última fila resume la lección: el único caso claro a favor de LinkedList es insertar por el principio, y para eso existe una estructura mejor.
Y una conclusión metodológica igual de valiosa: mide antes de optimizar. Si alguien cambia un ArrayList por un LinkedList "porque inserta más rápido" sin haber medido, hay muchas probabilidades de que haya empeorado el programa.
- Cuándo
LinkedList sí es la elección correcta: la cola de reservas
LinkedList sí es la elección correcta: la cola de reservasLlega el caso real de BiblioTech. Cuando un material está prestado, un empleado puede reservarlo. Las reservas forman una cola: se atienden por orden de llegada, se añaden por el final y se consumen por el principio. Es el patrón exacto en que una lista enlazada es adecuada: cero acceso por índice, todo por los extremos.
Primero, la clase Reserva. Como es un dato con identidad propia y estado (atendida), la modelamos como clase, no como record (04-07):
package com.nexussoftware.bibliotech.dominio;
/** Solicitud de reserva de un material ya prestado. */
public class Reserva {
public static final int PRIORIDAD_NORMAL = 5;
private final Empleado empleado;
private final Material material;
private final int diaSolicitud;
private final int prioridad; // 1 = maxima urgencia, 10 = minima
private boolean atendida;
public Reserva(Empleado empleado, Material material, int diaSolicitud, int prioridad) {
this.empleado = empleado;
this.material = material;
this.diaSolicitud = Math.max(diaSolicitud, 0);
this.prioridad = (prioridad < 1 || prioridad > 10) ? PRIORIDAD_NORMAL : prioridad;
this.atendida = false;
}
public Reserva(Empleado empleado, Material material, int diaSolicitud) {
this(empleado, material, diaSolicitud, PRIORIDAD_NORMAL);
}
public Empleado getEmpleado() { return empleado; }
public Material getMaterial() { return material; }
public int getDiaSolicitud() { return diaSolicitud; }
public int getPrioridad() { return prioridad; }
public boolean estaAtendida() { return atendida; }
public void marcarAtendida() { this.atendida = true; }
/** Dias que lleva esperando esta reserva. */
public int diasEnEspera(int diaActual) {
return Math.max(0, diaActual - diaSolicitud);
}
@Override
public String toString() {
return String.format("Reserva[%s -> %s, dia %d, prioridad %d%s]",
empleado.getNombre(), material.getTitulo(), diaSolicitud, prioridad,
atendida ? ", ATENDIDA" : "");
}
}Y la cola de reservas de un material:
package com.nexussoftware.bibliotech.servicio;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
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.
*
* LinkedList es aqui una eleccion justificada: solo se opera en los extremos
* (addLast al reservar, pollFirst al atender) y nunca por indice. Aun asi,
* un ArrayDeque (05-07) seria mas rapido; se usa LinkedList porque necesitamos
* ademas recorrer y eliminar por criterio, y porque ilustra el caso.
*/
public class ColaReservas {
private final Material material;
private final LinkedList<Reserva> pendientes = new LinkedList<>();
public ColaReservas(Material material) {
this.material = material;
}
/** Encolar por el final. O(1) real: se toca el nodo 'last'. */
public void reservar(Empleado empleado, int dia) {
pendientes.addLast(new Reserva(empleado, material, dia));
}
/** Consultar quien es el siguiente SIN sacarlo. Devuelve null si no hay nadie. */
public Reserva siguiente() {
return pendientes.peekFirst();
}
/**
* Atiende la primera reserva de la cola. O(1) real: se toca el nodo 'first'.
* Devuelve null si no habia ninguna pendiente.
*/
public Reserva atenderSiguiente(int dia) {
Reserva r = pendientes.pollFirst(); // poll: null si esta vacia, no excepcion
if (r != null) {
r.marcarAtendida();
material.prestar();
}
return r;
}
/** Cancela la reserva mas reciente de un empleado. Recorre desde el final. */
public boolean cancelarUltimaDe(Empleado empleado) {
Iterator<Reserva> it = pendientes.descendingIterator(); // de last a first
while (it.hasNext()) {
if (it.next().getEmpleado().equals(empleado)) {
it.remove(); // O(1) REAL: el iterador ya tiene el nodo
return true;
}
}
return false;
}
/** Caduca las reservas que llevan demasiados dias esperando. */
public int caducar(int diaActual, int diasMaximos) {
int antes = pendientes.size();
pendientes.removeIf(r -> r.diasEnEspera(diaActual) > diasMaximos);
return antes - pendientes.size();
}
/** Adelanta una reserva urgente al principio de la cola. O(1) real. */
public void adelantar(Reserva urgente) {
pendientes.remove(urgente); // O(n): hay que localizarla
pendientes.addFirst(urgente); // O(1): al principio
}
public List<Reserva> listar() { return new ArrayList<>(pendientes); }
public int enEspera() { return pendientes.size(); }
public boolean hayPendientes() { return !pendientes.isEmpty(); }
/** Posicion en la cola (1 = el siguiente). 0 si el empleado no tiene reserva. */
public int posicionDe(Empleado empleado) {
int posicion = 1;
for (Reserva r : pendientes) { // for-each, NUNCA get(i) en una LinkedList
if (r.getEmpleado().equals(empleado)) { return posicion; }
posicion++;
}
return 0;
}
}Uso completo:
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);
javaEfectivo.prestar(); // ya esta prestado a otra persona
ColaReservas cola = new ColaReservas(javaEfectivo);
cola.reservar(marta, 100);
cola.reservar(diego, 101);
cola.reservar(nuria, 103);
System.out.println("En espera: " + cola.enEspera());
System.out.println("Siguiente: " + cola.siguiente());
System.out.println("Posicion de Nuria: " + cola.posicionDe(nuria));
javaEfectivo.devolver();
Reserva atendida = cola.atenderSiguiente(110);
System.out.println("Atendida: " + atendida);
System.out.println("Ahora es el turno de: " + cola.siguiente().getEmpleado().getNombre());
int caducadas = cola.caducar(130, 20); // mas de 20 dias esperando
System.out.println("Reservas caducadas: " + caducadas);
System.out.println("Quedan en espera: " + cola.enEspera());En espera: 3 Siguiente: Reserva[Marta Ruiz -> Java Efectivo, dia 100, prioridad 5] Posicion de Nuria: 3 Atendida: Reserva[Marta Ruiz -> Java Efectivo, dia 110, prioridad 5, ATENDIDA] Ahora es el turno de: Diego Alonso Reservas caducadas: 1 Quedan en espera: 1
Fíjate en tres decisiones deliberadas de este código:
posicionDerecorre confor-each, nunca conget(i). Sobre unaLinkedList, el bucle indexado sería O(n²).cancelarUltimaDeusadescendingIteratoryit.remove(): el iterador ya tiene el nodo, así que la eliminación es O(1) real. Es el caso legítimo del apartado 3.adelantares honesto sobre su coste: elremove(Object)es O(n) porque hay que localizar la reserva; solo eladdFirstes O(1). Este método sería el primer candidato a revisar si la cola creciera mucho, y unaPriorityQueue(05-07) resolvería mejor el problema de las urgencias.
Y una nota final de diseño: aunque LinkedList funciona bien aquí, la declaración canónica de una cola es Deque<Reserva> pendientes = new ArrayDeque<>(). Se ha usado LinkedList porque además necesitamos removeIf y recorridos, y porque este era su caso natural; en 05-07 verás la versión con ArrayDeque y por qué suele ser mejor.
Errores Comunes y Consejos
Recorrer una LinkedList con for indexado. for (int i = 0; i < lista.size(); i++) lista.get(i) es O(n²): cada get recorre la cadena desde un extremo. Con 100 000 elementos pasa de milisegundos a minutos. Usa siempre for-each o un iterador.
Creer que add(i, e) es O(1) en LinkedList. La inserción sí lo es; llegar al índice i no. El método completo es O(n). Solo los extremos y las inserciones vía iterador son O(1) de verdad.
Elegir LinkedList "porque inserta más rápido" sin medir. En la práctica pierde casi siempre por localidad de caché y por consumo de memoria. Empieza por ArrayList y cambia solo con datos en la mano.
Usar LinkedList como cola cuando existe ArrayDeque. ArrayDeque es más rápido y ocupa menos. La única razón para preferir LinkedList es necesitar List y Deque en la misma variable, o admitir elementos null.
Confundir get/remove con peek/poll. Sobre una lista vacía, getFirst() y removeFirst() lanzan NoSuchElementException; peekFirst() y pollFirst() devuelven null. Elige según si la cola vacía es normal o es un error.
Modificar la lista mientras hay un ListIterator vivo. Cualquier lista.add, lista.remove o lista.clear directo invalida el iterador y provoca ConcurrentModificationException en la siguiente operación. Mientras el iterador exista, todo pasa por él.
Olvidar que it.add(x) no vuelve a visitar lo insertado. Es lo que evita el bucle infinito, y es lo correcto; pero si esperabas que el nuevo elemento se evaluara, no ocurrirá.
Ordenar una LinkedList a menudo. sort vuelca a un array, ordena y reconstruye toda la cadena de nodos. Si vas a ordenar con frecuencia, usa ArrayList; si necesitas orden permanente, TreeSet o PriorityQueue (05-06, 05-07).
Consejo: declara por la interfaz que refleje el uso. Si es una lista, List<X> l = new ArrayList<>(). Si es una cola, Deque<X> d = new ArrayDeque<>(). Declarar LinkedList<X> solo se justifica cuando necesitas las dos caras a la vez.
Consejo: LinkedList es un magnífico ejercicio mental. Implementarla a mano —con nodos, prev y next— enseña más sobre punteros y estructuras de datos que muchos libros. Pero en producción, ArrayList.
Ejercicios
Ejercicio 1: historial de operaciones con tamaño máximo
Escribe HistorialOperaciones que guarde las últimas N operaciones del catálogo (cadenas como "alta:978-0000000001"), usando una LinkedList como estructura interna:
void registrar(String operacion): añade al final y, si se supera el máximo, elimina la más antigua (la primera).String ultima()yString masAntigua(): sin eliminarlas, devolviendonullsi está vacío.List<String> enOrdenInverso(): de la más reciente a la más antigua, condescendingIterator.int eliminarPorPrefijo(String prefijo): elimina todas las que empiecen por ese prefijo y devuelve cuántas quitó.
Justifica en comentarios qué operaciones son O(1) reales y cuáles no.
Ejercicio 2: insertar separadores con ListIterator
Escribe un método estático void insertarSeparadores(List<Material> catalogo) que recorra un catálogo ya ordenado por tipo e inserte, antes del primer material de cada tipo, un Material especial de tipo separador (usa un Libro con título "--- LIBROS ---" o crea una pequeña clase Separador extends Material).
Debe funcionar con ListIterator y sin provocar ConcurrentModificationException ni bucles infinitos. Explica por qué la inserción no se vuelve a visitar.
Añade después una versión insertarSeparadoresMal que intente lo mismo con un for-each y documenta qué falla exactamente.
Ejercicio 3: comparativa honesta
Escribe BancoDePruebas que compare ArrayList y LinkedList en cuatro escenarios, con calentamiento previo y con protección contra la eliminación de código por el JIT:
- Añadir N elementos al final.
- Recorrer con
for-eachsumando. - Insertar 10 000 elementos por el principio.
- Eliminar por criterio con
removeIf.
Imprime una tabla con los tiempos y escribe, en un comentario final, tu conclusión razonada y la advertencia sobre JMH.
Soluciones
Solución 1
package com.nexussoftware.bibliotech.servicio;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
/**
* Historial acotado de operaciones del catalogo.
*
* LinkedList encaja aqui: todas las operaciones habituales son por los extremos.
*/
public class HistorialOperaciones {
private final LinkedList<String> operaciones = new LinkedList<>();
private final int maximo;
public HistorialOperaciones(int maximo) {
this.maximo = Math.max(maximo, 1);
}
public void registrar(String operacion) {
if (operacion == null || operacion.isBlank()) { return; }
operaciones.addLast(operacion); // O(1) REAL: toca el nodo 'last'
if (operaciones.size() > maximo) {
operaciones.removeFirst(); // O(1) REAL: toca el nodo 'first'
}
// En un ArrayList, ese removeFirst seria remove(0): O(n) por el desplazamiento.
// Este es exactamente el escenario donde la lista enlazada tiene sentido.
}
/** peekLast devuelve null si esta vacia; getLast lanzaria NoSuchElementException. */
public String ultima() { return operaciones.peekLast(); }
public String masAntigua() { return operaciones.peekFirst(); }
/**
* descendingIterator recorre de 'last' a 'first' aprovechando los enlaces prev.
* Es O(n) en total, con un solo salto de puntero por elemento.
*/
public List<String> enOrdenInverso() {
List<String> resultado = new ArrayList<>(operaciones.size());
Iterator<String> it = operaciones.descendingIterator();
while (it.hasNext()) {
resultado.add(it.next());
}
return resultado;
}
/**
* removeIf hace UNA pasada O(n) y cada eliminacion individual es O(1),
* porque el iterador interno ya esta situado en el nodo.
* Un bucle con remove(Object) seria O(n^2) y ademas fallaria con
* ConcurrentModificationException si se hiciera dentro de un for-each.
*/
public int eliminarPorPrefijo(String prefijo) {
if (prefijo == null) { return 0; }
int antes = operaciones.size();
operaciones.removeIf(op -> op.startsWith(prefijo));
return antes - operaciones.size();
}
public int tamano() { return operaciones.size(); }
public boolean vacio() { return operaciones.isEmpty(); }
}Prueba:
HistorialOperaciones h = new HistorialOperaciones(3);
h.registrar("alta:978-0000000001");
h.registrar("alta:978-0000000002");
h.registrar("baja:978-0000000001");
h.registrar("alta:978-0000000003"); // supera el maximo: cae la mas antigua
System.out.println("Mas antigua: " + h.masAntigua()); // alta:978-0000000002
System.out.println("Ultima: " + h.ultima()); // alta:978-0000000003
System.out.println("Inverso: " + h.enOrdenInverso());
System.out.println("Bajas eliminadas: " + h.eliminarPorPrefijo("baja:"));
System.out.println("Quedan: " + h.tamano());Este es el caso de uso ideal de LinkedList: una ventana deslizante. Cada registro añade por un extremo y quita por el otro, ambas O(1) reales. Con ArrayList, cada remove(0) desplazaría toda la lista. Dicho eso, la respuesta profesional para una ventana deslizante es ArrayDeque (05-07), que hace lo mismo con un array circular, sin nodos y sin presión sobre el recolector.
Solución 2
package com.nexussoftware.bibliotech.presentacion;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import com.nexussoftware.bibliotech.dominio.Material;
public final class SeparadoresCatalogo {
private SeparadoresCatalogo() { }
/** Material ficticio que solo sirve como titulo de seccion. */
private static Material separador(String tipo) {
return new Libro("--- " + tipo.toUpperCase() + " ---", "", "SEP-" + tipo, 2000);
}
/**
* Inserta un separador antes del primer material de cada tipo.
* Requiere que el catalogo este ORDENADO por tipo.
*/
public static void insertarSeparadores(List<Material> catalogo) {
if (catalogo == null || catalogo.isEmpty()) { return; }
ListIterator<Material> it = catalogo.listIterator();
String tipoAnterior = null;
while (it.hasNext()) {
Material m = it.next(); // el cursor queda DESPUES de m
String tipo = m.getTipo();
if (!tipo.equals(tipoAnterior)) {
// Retrocedemos para insertar ANTES de m
it.previous(); // el cursor vuelve a estar ANTES de m
it.add(separador(tipo)); // inserta y avanza el cursor
it.next(); // volvemos a pasar sobre m
tipoAnterior = tipo;
}
}
}
/**
* Version INCORRECTA, para documentar el fallo.
*/
public static void insertarSeparadoresMal(List<Material> catalogo) {
String tipoAnterior = null;
for (Material m : catalogo) {
if (!m.getTipo().equals(tipoAnterior)) {
catalogo.add(separador(m.getTipo()));
// FALLO: el for-each usa un Iterator interno que guardo el modCount
// al empezar. Este add lo incrementa. En la SIGUIENTE llamada a next()
// el iterador detecta la discrepancia y lanza
// ConcurrentModificationException.
//
// Ademas, aunque no fallara, el add anadiria SIEMPRE AL FINAL,
// no en la posicion correcta: el resultado seria erroneo igualmente.
tipoAnterior = m.getTipo();
}
}
}
public static void main(String[] args) {
List<Material> catalogo = new LinkedList<>(List.of(
new Dvd("Refactorizacion en vivo", "DVD-0007", 95),
new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018),
new Libro("Patrones de Diseno", "Erich Gamma", "978-0000000002", 1994),
new Revista("Java Magazine", "REV-2024-03", 42, "Mensual")
));
insertarSeparadores(catalogo);
catalogo.forEach(m -> System.out.println(m.getTitulo()));
}
}--- DVD --- Refactorizacion en vivo --- LIBRO --- Java Efectivo Patrones de Diseno --- REVISTA --- Java Magazine
La secuencia previous() → add(...) → next() merece una explicación cuidadosa. Tras it.next() el cursor está detrás del material actual, pero el separador debe ir delante. previous() retrocede el cursor a la posición anterior (y devuelve el mismo material, que descartamos). add inserta ahí y deja el cursor detrás de lo insertado, es decir, todavía delante del material. next() vuelve a saltarlo para continuar.
Ese avance automático del cursor tras add es exactamente lo que impide el bucle infinito: el elemento insertado nunca es devuelto por next(). Si add no lo hiciera, el siguiente next() devolvería el separador, cuyo tipo tampoco coincidiría con tipoAnterior, y se insertaría otro separador, indefinidamente.
Y sobre la versión incorrecta, hay dos fallos apilados: la ConcurrentModificationException y, más de fondo, que catalogo.add(x) inserta al final, no donde estás recorriendo. Es un buen recordatorio de que el for-each no conoce su propia posición.
Solución 3
package com.nexussoftware.bibliotech.presentacion;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Supplier;
/**
* Comparativa ilustrativa entre ArrayList y LinkedList.
*
* ADVERTENCIA: esto NO es un benchmark riguroso. La JVM compila el codigo
* sobre la marcha (JIT), el recolector de basura puede intervenir en mitad
* de la medicion y el compilador puede eliminar bucles cuyo resultado no se
* use. Para medir en serio hay que usar JMH (Java Microbenchmark Harness),
* la herramienta oficial de OpenJDK. Estas cifras sirven para ver ORDENES
* DE MAGNITUD, nada mas.
*/
public class BancoDePruebas {
private static final int N = 100_000;
private static final int INSERCIONES = 10_000;
private static long escudo = 0; // impide que el JIT elimine los bucles
public static void main(String[] args) {
System.out.println("Calentando la JVM (3 pasadas sin medir)...");
for (int i = 0; i < 3; i++) { ejecutar(false); }
System.out.println("\n=== Resultados (N = " + N + ") ===");
System.out.printf("%-28s %12s %12s%n", "Escenario", "ArrayList", "LinkedList");
System.out.println("-".repeat(54));
ejecutar(true);
System.out.println("\n(escudo = " + escudo + ", ignoralo: solo evita que el JIT borre los bucles)");
}
private static void ejecutar(boolean imprimir) {
fila("1. add al final", imprimir,
() -> anadirAlFinal(new ArrayList<>()),
() -> anadirAlFinal(new LinkedList<>()));
List<Integer> a1 = llenar(new ArrayList<>());
List<Integer> l1 = llenar(new LinkedList<>());
fila("2. recorrer for-each", imprimir,
() -> recorrer(a1), () -> recorrer(l1));
fila("3. insertar al inicio", imprimir,
() -> insertarAlInicio(llenar(new ArrayList<>())),
() -> insertarAlInicio(llenar(new LinkedList<>())));
fila("4. removeIf pares", imprimir,
() -> purgar(llenar(new ArrayList<>())),
() -> purgar(llenar(new LinkedList<>())));
}
private static void fila(String etiqueta, boolean imprimir,
Supplier<Long> conArray, Supplier<Long> conEnlazada) {
long ta = conArray.get();
long tl = conEnlazada.get();
if (imprimir) {
System.out.printf("%-28s %9d ms %9d ms%n", etiqueta, ta, tl);
}
}
private static List<Integer> llenar(List<Integer> lista) {
for (int i = 0; i < N; i++) { lista.add(i); }
return lista;
}
private static long anadirAlFinal(List<Integer> lista) {
long t = System.nanoTime();
for (int i = 0; i < N; i++) { lista.add(i); }
return ms(t);
}
private static long recorrer(List<Integer> lista) {
long t = System.nanoTime();
long suma = 0;
for (Integer v : lista) { suma += v; }
escudo += suma; // consumimos el resultado
return ms(t);
}
private static long insertarAlInicio(List<Integer> lista) {
long t = System.nanoTime();
for (int i = 0; i < INSERCIONES; i++) { lista.add(0, i); }
return ms(t);
}
private static long purgar(List<Integer> lista) {
long t = System.nanoTime();
lista.removeIf(v -> v % 2 == 0);
escudo += lista.size();
return ms(t);
}
private static long ms(long inicioNano) {
return (System.nanoTime() - inicioNano) / 1_000_000;
}
}Salida típica (los valores absolutos varían mucho según la máquina; lo que importa es la relación):
=== Resultados (N = 100000) === Escenario ArrayList LinkedList ------------------------------------------------------ 1. add al final 3 ms 6 ms 2. recorrer for-each 1 ms 7 ms 3. insertar al inicio 118 ms 2 ms 4. removeIf pares 2 ms 11 ms
Conclusión razonada. LinkedList gana en un solo escenario, el 3, y lo gana por mucho: insertar por el principio es su terreno natural. En los otros tres pierde, y en el recorrido —la operación más frecuente en cualquier programa real— es unas siete veces más lenta con la misma complejidad O(n): la diferencia es puramente localidad de caché.
El escenario 4 es especialmente revelador. removeIf es O(n) en ambas y elimina en O(1) en las dos (por iterador), pero LinkedList sigue perdiendo, porque recorrer los nodos dispersos domina el tiempo total.
Y la observación decisiva: si añades al banco de pruebas un ArrayDeque con addFirst, el escenario 3 baja a aproximadamente 1 ms, batiendo a las dos. Es decir, incluso en el único caso donde LinkedList gana a ArrayList, no es la mejor herramienta disponible. Por eso la recomendación práctica es: ArrayList para listas, ArrayDeque para colas y pilas, LinkedList casi nunca.
Repite la advertencia con cada número que veas: para decisiones de rendimiento reales, mide con JMH sobre tu carga de trabajo concreta.
Conclusión
Ya sabes qué es una lista doblemente enlazada: una cadena de nodos con prev, item y next, sin array, sin capacidad y sin índices. Entiendes lo que eso implica de verdad: ~28 bytes por elemento frente a los ~4 de un ArrayList, y nodos dispersos por el heap que destruyen la localidad de caché y hacen que recorrerla sea varias veces más lento aunque la complejidad sea la misma O(n). Has visto en el código del propio JDK que node(index) recorre la cadena, y con ello has desmontado el mito: insertar y borrar es O(1) solo si ya tienes la posición, lo que ocurre únicamente en los extremos y a través de un iterador; llegar por índice cuesta O(n).
Tienes la tabla comparativa operación por operación y la conclusión honesta que se deriva de ella: ArrayList gana casi siempre, porque System.arraycopy es una instrucción de bloque nativa, porque la caché domina, porque los nodos presionan al recolector y porque las inserciones "en medio" casi nunca lo son. Sabes que el propio coautor del Framework la considera prescindible hoy, y que su nicho real no es ser una List sino ser un Deque, terreno en el que ArrayDeque la supera. También sabes leer el error de rendimiento que más se cuela: un for indexado sobre una LinkedList es O(n²) disfrazado.
Conoces su doble naturaleza List + Deque y toda la familia de operaciones por los extremos, con la distinción entre la variante que lanza excepción (getFirst, removeFirst) y la que devuelve null (peekFirst, pollFirst) —que 05-07 desarrollará—. Y dominas el ListIterator: el cursor que vive entre elementos, el recorrido bidireccional, add y set durante el recorrido, por qué lo insertado no se vuelve a visitar y por qué es la única forma correcta de insertar mientras recorres.
Sabes medir con honestidad: calentar la JVM, consumir los resultados para que el JIT no elimine los bucles, desconfiar de los números absolutos y recurrir a JMH cuando la decisión importe de verdad. Y por encima de todo, has adoptado el criterio profesional: mide antes de optimizar, porque cambiar ArrayList por LinkedList "porque inserta más rápido" es, casi siempre, empeorar el programa.
BiblioTech ha ganado la clase Reserva y una ColaReservas que encola por el final, atiende por el principio, cancela mediante iterador descendente en O(1) real y caduca las reservas antiguas con removeIf. Es el único punto del proyecto donde una lista enlazada estaba justificada, y aun así has visto por qué en 05-07 la reescribirás con ArrayDeque.
Y queda una debilidad que ya se está haciendo insoportable. Catalogo.eliminar(referencia) recorre la lista entera. GestorPrestamos.devolver(referencia, dia) recorre la lista entera. ColaReservas.posicionDe(empleado) recorre la lista entera. Cada vez que BiblioTech tiene que encontrar algo por su identificador, mira uno por uno. Con cinco materiales es gratis; con cincuenta mil, cada búsqueda son cincuenta mil comparaciones, y agrupar préstamos por empleado con bucles anidados es O(n²).
En la lección siguiente, HashMap, eso se acaba. Verás la estructura que encuentra un elemento por su clave en tiempo constante sin importar cuántos haya: cómo funciona la función hash, qué son las cubetas, qué ocurre cuando dos claves colisionan, por qué el factor de carga es 0,75 y qué pasa en un rehash. Y ahí se cumplirá por fin la promesa que se hizo en el módulo 3: verás con una demostración práctica por qué equals y hashCode tenían que ir siempre juntos, y qué le ocurre exactamente a un objeto que entra en un mapa con uno de los dos mal implementado.
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
