Skip to content

LinkedList: El Tren de los Datos en Java

Introducción: ¿Dónde dejamos los vagones?

¡Hola, futuros arquitectos del software! 🚀

¿Recordáis cuando en el Tema 7 hablamos de las estructuras dinámicas? Vimos que el ArrayList era como una fila de butacas en el cine: todas seguidas, numeradas y muy rápidas de encontrar si tienes la entrada. Pero, ¿qué pasa si llega un grupo de amigos tarde y queremos sentarlos en medio? ¡Un caos! Tendríamos que pedirle a todo el mundo que se levante y se mueva una silla a la derecha. 😫

Aquí es donde entra nuestra estrella de hoy: la LinkedList (Lista Enlazada).

Imaginad una búsqueda del tesoro o un tren de mercancías. Cada vagón (nodo) contiene dos cosas: 1. La carga (tu dato). 2. Un gancho (puntero) que lo une al siguiente vagón y otro al anterior.

No necesitan estar juntos en la memoria del ordenador. Un vagón puede estar en "Cuenca" y el siguiente en "Teruel", pero mientras estén enganchados, el tren funciona. Esto la hace brutalmente eficiente para insertar y borrar datos (solo desenganchas y enganchas), pero un poco más lenta para buscar (tienes que recorrer todos los vagones desde la locomotora hasta encontrar el que buscas).

¿Listos para enganchar código? ¡Viajeros al tren! 🚂💻


Conceptos Fundamentales

Antes de picar código como si no hubiera un mañana, definamos las piezas del puzle.

Definición: Nodo (Node)

Es la unidad básica de una LinkedList. Imagínalo como una caja con tres compartimentos: 1. Item: El dato real (un String, un objeto Cliente, un entero...). 2. Next: Una referencia (flecha) que apunta al siguiente nodo. 3. Prev: Una referencia que apunta al nodo anterior.

Definición: Lista Doblemente Enlazada

Java implementa java.util.LinkedList como una lista doblemente enlazada. Esto significa que puedes recorrerla tanto de principio a fin como de fin a principio. Es decir, el tren puede ir marcha adelante y marcha atrás sin problemas.


Interfaces que implementa

La LinkedList es una "navaja suiza". Implementa List (como ArrayList), Deque (Cola de doble extremo) y Queue (Cola). ¡Sirve para todo!


Java 21 Update

Desde Java 21, LinkedList forma parte de la familia SequencedCollection, estandarizando métodos como addFirst o getLast para todas las colecciones con orden.

Mapa Mental: Estructura de la LinkedList



Desarrollo y Ejemplos Prácticos

1. Creación de LinkedList

Crear una LinkedList es tan fácil como invocar a su constructor. A diferencia de los Arrays, aquí no declaramos tamaño inicial (recuerda, es memoria dinámica, crece y decrece mágicamente).

A) Constructor Vacío (El clásico)

Creamos una lista sin elementos.

import java.util.LinkedList;

public class Main {
    public static void main(String[] args) {
        // Creamos una lista enlazada para guardar nombres de videojuegos
        LinkedList<String> juegos = new LinkedList<>();

        System.out.println("Lista creada: " + juegos); // Salida: []
    }
}

B) Constructor con Colección (La mudanza)

Si ya tienes otra lista (un ArrayList, un Set...) y quieres convertirla en una LinkedList, usas este constructor.

import java.util.LinkedList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        // Imagina que esto viene de una Base de Datos como ArrayList
        List<String> listaOriginal = List.of("Zelda", "Mario", "Metroid");

        // La convertimos a LinkedList
        LinkedList<String> juegosRetro = new LinkedList<>(listaOriginal);

        System.out.println("Lista copiada: " + juegosRetro);
    }
}

Ejercicios Reflexivos

  1. ¿Por qué no le pasamos una capacidad inicial (ej. new LinkedList(10)) como hacíamos con ArrayList? (Pista: piensa en cómo se guardan los datos en memoria).
  2. Si tuvieras que programar un historial de navegación web (Página A -> Página B -> Página C) y permitir al usuario volver atrás, ¿usarías LinkedList o ArrayList? ¿Por qué?
  3. ¿Qué pasaría si intentas inicializar List<String> lista = new LinkedList<>(); en lugar de LinkedList<String>...? ¿Podrías usar métodos como addFirst()?



2. Métodos del LinkedList

Aquí está el arsenal completo. Como desarrolladores Full Stack (o casi), debéis conocer la herramienta para saber cuándo usarla.

Nota: He agrupado los métodos por funcionalidad para que tenga sentido, pero están todos los que habéis pedido.

A. Añadir Elementos (Adding Items)

La LinkedList brilla aquí. Insertar es barato.

add(E e) / addLast(E e) / offer(E e) / offerLast(E e) Todos añaden al final. offer es preferible cuando usamos la lista como Cola (Queue) porque está diseñado para manejar fallos de capacidad (aunque en LinkedList la capacidad es virtualmente infinita).

LinkedList<String> playlist = new LinkedList<>();

// add devuelve true si tiene éxito
boolean agregado = playlist.add("Bohemian Rhapsody"); 

// addLast es void, específico de listas enlazadas/deques
playlist.addLast("Stairway to Heaven");

// offer devuelve true, típico de Colas
playlist.offer("Hotel California");
playlist.offerLast("Imagine");

System.out.println(playlist); 
// [Bohemian Rhapsody, Stairway to Heaven, Hotel California, Imagine]

addFirst(E e) / push(E e) / offerFirst(E e) Añaden al principio. push viene de la terminología de Pilas (Stack).

// addFirst coloca al inicio
playlist.addFirst("Despacito"); 

// push "empuja" al inicio (como poner un plato sobre una pila de platos)
playlist.push("Macarena");

// offerFirst hace lo mismo, devuelve boolean
playlist.offerFirst("Aserejé");

System.out.println(playlist);
// [Aserejé, Macarena, Despacito, Bohemian Rhapsody...]

add(int index, E element) Inserta en una posición específica.

Cuidado con el rendimiento

Aunque insertar el nodo es rápido, Java tiene que llegar hasta el índice index recorriendo la lista uno a uno. Si el índice es alto, esto tarda.

// Insertamos en la posición 1 (segundo lugar)
playlist.add(1, "Waka Waka");

addAll(Collection c) / addAll(int index, Collection c) Añade todo un grupo de elementos.

List<String> nuevasCanciones = List.of("Song 1", "Song 2");
playlist.addAll(nuevasCanciones); // Al final

B. Obtener/Consultar Elementos (Retrieving Items)

Aquí es donde la LinkedList sufre un poco comparada con el ArrayList.

get(int index) Devuelve el elemento en la posición dada.

// Devuelve la canción en la posición 2. 
// Java tiene que saltar desde el nodo 0 -> 1 -> 2.
String cancion = playlist.get(2); 

getFirst() / element() / peek() / peekFirst() Todos miran el primer elemento. * getFirst / element: Lanzan excepción si la lista está vacía. * peek / peekFirst: Devuelven null si está vacía (más seguro).

try {
    System.out.println(playlist.getFirst()); // Si vacía -> NoSuchElementException
    System.out.println(playlist.element());  // Igual que getFirst
} catch (Exception e) { System.out.println("Lista vacía"); }

System.out.println(playlist.peek());      // Si vacía -> null
System.out.println(playlist.peekFirst()); // Si vacía -> null

getLast() / peekLast() Miran el último elemento.

System.out.println(playlist.getLast());   // Lanza excepción si vacía
System.out.println(playlist.peekLast());  // Devuelve null si vacía

indexOf(Object o) / lastIndexOf(Object o) / contains(Object o) Búsqueda lineal. Recorren la lista comparando con .equals().

int pos = playlist.indexOf("Macarena"); // Devuelve índice o -1
boolean existe = playlist.contains("Despacito"); // true/false

C. Eliminar Elementos (Removing Items)

remove() / removeFirst() / pop() / poll() / pollFirst() Todos eliminan la cabeza (el primer elemento). * remove / pop: Lanzan excepción si está vacía. * poll: Devuelve null si está vacía.

String eliminada = playlist.remove(); // Adios al primero
String popC = playlist.pop();         // Saca el primero de la pila
String pollC = playlist.poll();       // Saca el primero (safe)

removeLast() / pollLast() Eliminan la cola (el último).

playlist.removeLast(); // Excepción si vacía
playlist.pollLast();   // Null si vacía

remove(int index) / remove(Object o) Elimina por posición o por objeto.

playlist.remove(2); // Borra el tercero
playlist.remove("Waka Waka"); // Borra la primera ocurrencia de ese objeto

removeFirstOccurrence(Object o) / removeLastOccurrence(Object o) Específico para cuando hay duplicados.

playlist.add("Macarena");
playlist.add("Macarena");
// Tenemos dos Macarenas.
playlist.removeLastOccurrence("Macarena"); // Borra la que añadimos al final

clear() Borra todo. Deja la lista como un solar vacío.

playlist.clear(); // size() ahora es 0

removeAll(Collection c) / retainAll(Collection c) / removeIf(Predicate filter) Operaciones en masa.

LinkedList<Integer> numeros = new LinkedList<>(List.of(1, 2, 3, 4, 5));

// Borrar los que estén en esta lista
numeros.removeAll(List.of(1, 5)); // Queda [2, 3, 4]

// Quedarse SOLO con los que estén en esta lista
numeros.retainAll(List.of(2, 3)); // Queda [2, 3]

// Borrar con condición (lambda)
numeros.removeIf(n -> n % 2 != 0); // Borra impares

D. Modificar e Iterar (Modifying & Iterating)

set(int index, E element) Reemplaza el elemento en una posición.

playlist.set(0, "Nueva Canción"); // Machaca lo que hubiera en index 0

replaceAll(UnaryOperator operator) Reemplaza todos los elementos aplicando una función.

LinkedList<String> frutas = new LinkedList<>(List.of("manzana", "pera"));
frutas.replaceAll(String::toUpperCase); // [MANZANA, PERA]

sort(Comparator c) Ordena la lista. Ojo: LinkedList se ordena más lento que ArrayList debido a los punteros.

frutas.sort(null); // Orden natural (alfabético)

iterator() / listIterator() / descendingIterator() Formas de recorrer la lista.

// Iterador estándar (hacia adelante)
var it = playlist.iterator();
while(it.hasNext()) {
    System.out.println(it.next());
}

// Iterador de lista (permite ir atrás y adelante)
var listIt = playlist.listIterator();
listIt.next(); // Avanza
if(listIt.hasPrevious()) System.out.println(listIt.previous()); // Retrocede

// Iterador inverso (del final al principio)
var descIt = playlist.descendingIterator();
while(descIt.hasNext()) {
    System.out.println("Inverso: " + descIt.next());
}

forEach(Consumer action) La forma moderna (Java 8+) de iterar.

playlist.forEach(cancion -> System.out.println("Reproduciendo: " + cancion));

spliterator() Crea un Spliterator para procesamiento paralelo. En LinkedList no es muy eficiente porque dividir una lista enlazada es costoso (no sabes dónde está la mitad hasta que la recorres).

var spliterator = playlist.spliterator();
// Útil si pasas esto a un Stream paralelo, pero preferible usar ArrayList para eso.

E. Otros Métodos

size() Devuelve el número de elementos (int).

isEmpty() Devuelve true si no hay elementos. Más elegante que poner size() == 0.

clone() Crea una copia superficial (shallow copy) de la lista.

Object copia = playlist.clone(); // Devuelve Object, hay que hacer cast si se necesita

subList(int fromIndex, int toIndex) Devuelve una vista de una parte de la lista. ¡Cuidado! Si modificas la sublista, modificas la original.

List<String> top3 = playlist.subList(0, 3);

toArray() Convierte la lista en un Array clásico.

Object[] array = playlist.toArray();
// O tipado:
String[] arrayString = playlist.toArray(new String[0]);

Tabla Comparativa: Métodos "Peligrosos" vs "Seguros"

En estructuras de tipo Cola/Pila, a menudo tenemos dos versiones de la misma acción.

Acción Lanza Excepción (Peligroso) ⚠️ Devuelve null/false (Seguro) ✅
Insertar add(e), addLast(e) offer(e), offerLast(e)
Borrar remove(), removeFirst() poll(), pollFirst()
Mirar element(), getFirst() peek(), peekFirst()

Consejo Profesional

Si tu lógica de negocio espera que siempre haya datos, usa los métodos de la columna "Peligroso" para detectar errores rápido (Fail Fast). Si es normal que la lista esté vacía a veces, usa los "Seguros" y gestiona el null.

Diagrama de Secuencia: Recorriendo una LinkedList

Fíjate cómo el código pide el "Siguiente" repetidamente.

sequenceDiagram
    participant Main as Código Principal
    participant It as Iterator
    participant N1 as Nodo 1
    participant N2 as Nodo 2

    Main->>It: next()
    It->>N1: Dame tu valor
    N1-->>It: "Valor 1"
    It-->>Main: "Valor 1"

    Main->>It: next()
    Note right of It: El iterador usa el puntero .next del Nodo 1
    It->>N2: Dame tu valor
    N2-->>It: "Valor 2"
    It-->>Main: "Valor 2"



Preguntas para el Examen (y la vida)

  1. Si tienes una LinkedList con 1 millón de registros y haces list.get(500000), ¿qué hace Java internamente? ¿Es eficiente?
  2. Analiza este código: for (int i=0; i < list.size(); i++) { System.out.println(list.get(i)); }. Si list es una LinkedList, ¿por qué este bucle es un crimen contra el rendimiento? (Pista: complejidad cuadrática).
  3. ¿Cuál es la diferencia entre poll() y remove()? ¿Cuál usarías si no estás seguro de si la lista tiene elementos?
  4. ¿Por qué spliterator en una LinkedList podría no ser tan eficiente para paralelizar como en un ArrayList?

Aplicación en el Mundo Real

Vale, profe, ¿pero esto se usa fuera de clase? Sí, y mucho.

  1. Funcionalidad "Deshacer" (Undo) en editores de texto: Imagina Word o tu IDE. Cada vez que escribes, se guarda un estado. Si pulsas Ctrl+Z, vuelves atrás. Esto suele implementarse con pilas (Stacks) o Listas Doblemente Enlazadas, donde puedes moverte fácil hacia atrás y adelante en el historial de cambios.
  2. Reproductores de Música (Spotify/Apple Music): Una lista de reproducción es literalmente una LinkedList. Tienes una canción actual, un puntero a la next y otro a la previous. Si añades una canción a la cola ("Reproducir a continuación"), simplemente se cambia un puntero para intercalarla sin mover todo el resto de canciones de la memoria.
  3. Gestión de tareas en Sistemas Operativos: El planificador de procesos de Linux o Windows a menudo usa colas (implementadas con listas enlazadas) para saber qué programa debe usar la CPU a continuación. Entran por un lado y salen por el otro (FIFO).

Para Saber Más

Aquí tenéis recursos "pata negra" para profundizar. Nada de tutoriales dudosos de 2010.