Skip to content

Tema 7 - ArrayList – Tu Mochila Infinita en Java

Introducción: Del "Casillero" al "Inventario RPG"

¿Recordáis en el Tema 7 cuando hablamos de las estructuras de datos dinámicas? Bien, hoy vamos a conocer a la estrella del rock de las colecciones en Java: el ArrayList.

Para entenderlo, pensad en los Arrays clásicos (los que vimos hace meses) como esos casilleros de instituto o de gimnasio. Tienen un tamaño fijo. Si te dan el casillero 5, solo te cabe lo que quepa ahí. Si quieres meter tu casco de moto, la mochila, los libros y el patinete... mala suerte, no estira. Tienes que pedir un casillero nuevo más grande y moverlo todo a mano. ¡Un dolor de cabeza!

El ArrayList, en cambio, es como el Inventario de un videojuego RPG (tipo Zelda, Skyrim o Baldur's Gate). Empiezas con poco espacio, pero a medida que recoges objetos (loot), la bolsa se "expande" mágicamente. No te preocupas por cuántos huecos quedan; simplemente metes la poción, la espada y esa armadura legendaria. Java se encarga de gestionar el espacio por ti.

En el mundo laboral, el 90% de las veces que necesites una lista de cosas, usarás un ArrayList. Es la herramienta por defecto para "montones de datos" que cambian de tamaño.


Conceptos Fundamentales

Antes de tirar líneas de código como si no hubiera un mañana, definamos qué tenemos entre manos.

Definición: ArrayList

Es una clase de la librería estándar de Java (java.util) que implementa una matriz redimensionable (un array dinámico). A diferencia de un array normal ([]), el ArrayList crece y decrece automáticamente a medida que añadimos o borramos elementos.

Definición: Generics (< T >)

Verás mucho el símbolo <String>, <Integer>, etc. Esto son Genéricos. Sirven para obligar a la lista a aceptar solo un tipo de dato. Es como poner una etiqueta en tu tupper que dice "SOLO LENTEJAS". Si intentas meter macarrones (otro tipo de objeto), el compilador te gritará antes de ejecutar.

Aquí tienes un mapa mental de dónde estamos ubicados en la jerarquía de Java:



Desarrollo y Ejemplos Prácticos

1. Creación de ArrayList

En Java 21, tenemos varias formas de instanciar un ArrayList. La evolución del lenguaje nos ha permitido escribir menos código para hacer lo mismo (lo que llamamos "Syntactic Sugar" 🍬).

Forma Clásica vs. Forma Moderna

La forma más común utiliza el constructor vacío. Por defecto, Java crea una lista con una capacidad inicial pequeña (normalmente 10 huecos invisibles).

import java.util.ArrayList;
import java.util.List;

public class CreacionListas {
    public static void main(String[] args) {

        // FORMA CLÁSICA (Antes de Java 7) - Muy verbosa
        // ArrayList<String> listaNombres = new ArrayList<String>();

        // FORMA ESTÁNDAR (Operador Diamond <>)
        // Recomendado: Usar la interfaz 'List' a la izquierda (Polimorfismo)
        List<String> estudiantes = new ArrayList<>();

        // FORMA JAVA 10+ (Inferencia de tipos con 'var')
        // El compilador deduce que es un ArrayList<String>
        var videojuegos = new ArrayList<String>();

        videojuegos.add("Elden Ring");
        videojuegos.add("Hollow Knight");

        System.out.println("Mis juegos: " + videojuegos);
    }
}

Creación con valores iniciales (List.of)

A veces ya tienes los datos y no quieres hacer 20 add().

Cuidado con List.of()

List.of("A", "B") crea una lista INMUTABLE (no se puede modificar). Si intentas añadir algo después, el programa explotará (UnsupportedOperationException). Para hacerla mutable, pásala al constructor del ArrayList.

import java.util.ArrayList;
import java.util.List;

public class CreacionAvanzada {
    public static void main(String[] args) {

        // 1. Crear una lista inmutable (Java 9+)
        List<String> inmutable = List.of("HTML", "CSS", "JS");
        // inmutable.add("JAVA"); // ¡ERROR! Boom 💥

        // 2. Crear ArrayList mutable a partir de unos valores (TRUCO PRO)
        // Creamos un ArrayList nuevo y le volcamos el contenido de List.of
        List<String> tecnologiasWeb = new ArrayList<>(List.of("HTML", "CSS", "JS"));

        tecnologiasWeb.add("JAVA"); // ¡Ahora sí funciona!
        tecnologiasWeb.add("SQL");

        System.out.println("Stack Fullstack: " + tecnologiasWeb);
    }
}

Diferencias Clave en la Creación

Método Mutabilidad Permite Nulos Caso de Uso
new ArrayList<>() (Total) Cuando empezarás vacío e irás llenando.
Arrays.asList(...) Parcial (Tamaño fijo) Puente entre Arrays clásicos y Listas.
List.of(...) No (Inmutable) No Constantes, listas de configuración.
new ArrayList<>(List.of(...)) (Total) No (en origen) Inicializar lista mutable con datos.

Question

  1. Si estás creando una lista de los días de la semana para usarla en una validación, ¿usarías new ArrayList o List.of?
  2. ¿Por qué crees que se recomienda declarar la variable como List (interfaz) en lugar de ArrayList (clase)? Pista: Piensa en si mañana quieres cambiar a LinkedList.



2. Métodos del ArrayList

A continuación, vamos a diseccionar la clase ArrayList. He agrupado los métodos para que tengan sentido lógico, aunque cubriremos todos los solicitados.

Imagínate que gestionamos una Playlist de Spotify colaborativa.

A. Añadir Elementos (add, addAll)

Capacidad vs Tamaño

Capacity: Cuántos huecos tiene reservados en memoria (interno). Size: Cuántos elementos reales has metido.

var playlist = new ArrayList<String>();

// add(E e): Añade al final. Devuelve boolean (siempre true en ArrayList).
playlist.add("Bohemian Rhapsody"); 
playlist.add("Despacito");

// add(int index, E element): Inserta en una posición, desplazando el resto a la derecha.
playlist.add(0, "Imagine"); // Se pone la primera

// addAll(Collection<? extends E> c): Añade toda otra lista al final.
var rockClassics = List.of("Stairway to Heaven", "Hotel California");
boolean cambiado = playlist.addAll(rockClassics);

// addAll(int index, Collection c): Inserta una colección en una posición específica.
playlist.addAll(1, List.of("Song 2", "Wonderwall"));

System.out.println(playlist);

B. Información Básica (size, isEmpty, contains)

// size(): Devuelve el número de elementos (int).
System.out.println("Canciones totales: " + playlist.size());

// isEmpty(): Devuelve true si no hay elementos.
if (!playlist.isEmpty()) {
    System.out.println("¡A bailar!");
}

// contains(Object o): Busca si existe el elemento (usa equals()). Devuelve boolean.
// Es muy útil para evitar duplicados manuales.
if (playlist.contains("Despacito")) {
    System.out.println("Oh no, esa canción otra vez no...");
}

C. Acceso y Búsqueda (get, indexOf, lastIndexOf)

Danger

Cuidado con get(index). Si pides un índice que no existe (ej: índice 10 en lista de 5), obtendrás un IndexOutOfBoundsException. ¡Es el error clásico del novato!

// get(int index): Devuelve el elemento en esa posición (T).
String primeraCancion = playlist.get(0);

// indexOf(Object o): Devuelve la posición de la PRIMERA aparición. -1 si no está.
int posImagine = playlist.indexOf("Imagine");

// lastIndexOf(Object o): Devuelve la posición de la ÚLTIMA aparición.
playlist.add("Imagine"); // La añadimos otra vez para probar
int ultimaPos = playlist.lastIndexOf("Imagine");

System.out.println("Primera aparición: " + posImagine + ", Última: " + ultimaPos);

D. Modificación y Ordenación (set, replaceAll, sort)

import java.util.Comparator;

// set(int index, E element): Reemplaza el elemento en la posición. Devuelve el antiguo.
// Cambiamos "Despacito" (si estuviera en pos 2) por algo mejor.
playlist.set(2, "Thunderstruck");

// replaceAll(UnaryOperator<E> operator): Reemplaza TODOS los elementos según una función lambda.
// Ejemplo: Poner todo en mayúsculas.
playlist.replaceAll(cancion -> cancion.toUpperCase());

// sort(Comparator<? super E> c): Ordena la lista.
// null -> usa el orden natural (alfabético para Strings).
playlist.sort(null); 

// Orden inverso usando un Comparator
playlist.sort(Comparator.reverseOrder());

E. Eliminación (clear, remove, removeAll, removeIf, retainAll)

Aquí es donde la gestión dinámica brilla. Borrar en un array normal es una pesadilla (huecos vacíos). Aquí es automático.

graph LR
    A[Lista: A, B, C, D] -- remove(1) --> B[Elimina 'B']
    B -- Desplaza Izq --> C[Lista: A, C, D]
    style B fill:#f96,stroke:#333
// remove(int index): Borra por posición. Devuelve el elemento borrado.
String borrada = playlist.remove(0); 

// remove(Object o): Borra la primera aparición del objeto. Devuelve boolean.
boolean fueBorrada = playlist.remove("WONDERWALL");

// removeAll(Collection<?> c): Borra de mi lista TODO lo que aparezca en la otra lista.
var cancionesProhibidas = List.of("DESPACITO", "MACARENA");
playlist.removeAll(cancionesProhibidas);

// removeIf(Predicate<? super E> filter): Borra si cumple una condición (Lambda).
// Ejemplo: Borrar canciones muy cortas o que empiecen por 'T'.
playlist.removeIf(cancion -> cancion.startsWith("T"));

// retainAll(Collection<?> c): "Quédate SOLO con esto". Lo contrario a removeAll.
// Borra todo lo que NO esté en la colección pasada.
var soloRock = List.of("BOHEMIAN RHAPSODY", "THUNDERSTRUCK");
playlist.retainAll(soloRock); // Si 'Imagine' estaba, se borra.

// clear(): Borra TODO. Reseteo de fábrica.
// playlist.clear();

F. Iteración (forEach, iterator, listIterator, spliterator)

¿Cómo recorremos la lista? Tienes muchas opciones.

sequenceDiagram
    participant App
    participant Iterator
    participant ArrayList

    App->>ArrayList: iterator()
    ArrayList-->>Iterator: crea nuevo
    loop Mientras hasNext()
        App->>Iterator: hasNext()
        Iterator-->>App: true
        App->>Iterator: next()
        Iterator->>ArrayList: obtiene elemento
        ArrayList-->>Iterator: elemento
        Iterator-->>App: elemento
    end
// forEach(Consumer<? super E> action): La forma moderna y elegante (Lambda).
playlist.forEach(cancion -> System.out.println("Reproduciendo: " + cancion));

// iterator(): Devuelve un objeto Iterator estándar.
// Útil si necesitas borrar elementos MIENTRAS recorres (aunque removeIf es mejor ahora).
var it = playlist.iterator();
while(it.hasNext()) {
    String c = it.next();
    System.out.println("Iterando: " + c);
}

// listIterator(): Un iterador más potente. Permite ir hacia atrás y adelante.
var listIt = playlist.listIterator();
while(listIt.hasNext()) listIt.next(); // Vamos al final
while(listIt.hasPrevious()) { // Vamos marcha atrás
    System.out.println("Rewind: " + listIt.previous());
}

// spliterator(): "Splitable Iterator". Diseñado para procesamiento paralelo.
// Raro de usar manualmente en FP, pero es la base de los Streams paralelos.
var split = playlist.spliterator();
// tryAdvance intenta procesar un elemento, devuelve false si no quedan.
split.tryAdvance(c -> System.out.println("Spliterator vio: " + c));

G. Gestión Avanzada y Conversión (clone, ensureCapacity, subList, toArray, trimToSize)

Métodos para optimizar memoria o interactuar con APIs antiguas.

// ensureCapacity(int minCapacity): Pre-reserva memoria.
// Si sabes que vas a meter 1 millón de datos, haz esto antes para evitar
// que Java tenga que redimensionar el array interno 20 veces.
ArrayList<Integer> numeros = new ArrayList<>();
numeros.ensureCapacity(1000); 

// trimToSize(): Reduce la capacidad interna al tamaño actual.
// Si reservaste 1000 huecos pero solo usas 5, esto libera memoria.
numeros.trimToSize();

// subList(int fromIndex, int toIndex): Crea una VISTA de una parte de la lista.
// ¡OJO! No es una copia. Si modificas la sublista, modificas la original.
// Rango: [from, to) -> Incluye el inicio, excluye el final.
List<String> top3 = playlist.subList(0, Math.min(3, playlist.size()));

// clone(): Crea una copia superficial (Shallow Copy) del ArrayList.
// Devuelve Object, así que hay que castear.
Object copiaObjeto = playlist.clone();
ArrayList<String> copiaLista = (ArrayList<String>) playlist.clone();

// toArray(): Convierte la lista en un array clásico [].
// Opción 1: Devuelve Object[]
Object[] arraySimple = playlist.toArray();

// Opción 2: Devuelve T[] (Tipado). Es la recomendada.
// Pasamos un array de tamaño 0 para que Java cree uno nuevo del tamaño correcto.
String[] arrayTipado = playlist.toArray(new String[0]);

Copia Superficial (Clone)

clone() copia la estructura de la lista, pero NO duplica los objetos que contiene. Si la lista contiene objetos complejos (ej: Persona), ambas listas apuntarán a las mismas personas en memoria. Si cambias el nombre de una persona en la lista clonada, ¡cambia en la original!



3. Comparativa: ArrayList vs Array

Característica Array Clásico String[] ArrayList ArrayList<String>
Tamaño Fijo (nace y muere igual). Dinámico (crece y decrece).
Rendimiento Extremadamente rápido. Ligeramente más lento (por la gestión interna).
Tipos Primitivos (int) y Objetos. Solo Objetos (usa Integer, no int).
Métodos Casi ninguno (solo length). Cientos (add, remove, sort...).
Uso en FP Algoritmos de bajo nivel. Desarrollo de Apps, Web, Servicios.

Question

  1. ¿Qué pasaría si usas subList para obtener una vista, y luego borras un elemento de la lista original? (Pista: ConcurrentModificationException).
  2. ¿Por qué ensureCapacity no cambia el size() de la lista?
  3. Investiga: ¿Qué complejidad Big O tiene el método contains()? ¿Es eficiente para buscar en listas de 1 millón de elementos?



Aplicación en el Mundo Real

El ArrayList es el "martillo" en la caja de herramientas de cualquier desarrollador Java. Lo usarás para todo:

  1. Carritos de la Compra (E-commerce): Cuando añades productos en Amazon, se guardan en una lista asociada a tu sesión. Si borras uno, usas remove().
  2. Feeds de Redes Sociales: Instagram o TikTok cargan los posts en lotes. Empiezan con 10 (una lista). Cuando haces scroll al fondo, piden 10 más al servidor y hacen un addAll() a tu lista local para que sigas viendo contenido sin cortes.
  3. Gestión de Datos JDBC: Cuando pides 500 clientes a una base de datos SQL (Tema 2), el driver JDBC recorre los resultados y te devuelve... ¡exacto!, un ArrayList<Cliente>.

Para Saber Más

Aquí tienes recursos seleccionados de alta calidad para profundizar (y no, no son de relleno):