Saltar al contenido
colecciones.java · devschool

ArrayList y listas en Java

Lección 21 de 26 · 10 min de lectura · Actualizado el

En esta lección
  1. Por qué colecciones
  2. Genéricos: el tipo entre < >
  3. Operaciones básicas de ArrayList
  4. Recorrer una lista
  5. Listas inmutables con List.of
  6. Ordenar listas
  7. LinkedList y cuándo usarla
  8. Programar contra List, no contra la implementación
  9. Array frente a ArrayList
  10. Errores frecuentes
  11. Resumen

Los arrays tienen un problema: su tamaño se fija al crearlos. Si no sabes cuántos alumnos se van a matricular o cuántos productos meterá el cliente en el carrito, un array se queda corto enseguida. Para eso existen las colecciones, y la más usada de todas es ArrayList, una lista que crece y encoge sola. En esta lección aprenderás a crearla, a añadir, consultar y borrar elementos, a recorrerla y a ordenarla.

Por qué colecciones

Imagina una lista de la compra guardada en un array de 5 posiciones. Para añadir un sexto producto tendrías que crear un array nuevo más grande, copiar los elementos y cambiar la referencia. Para borrar uno del medio, mover a mano todos los que van detrás. Es mucho código repetitivo y fácil de estropear.

Java trae un conjunto de clases ya hechas que resuelven esto: el framework de colecciones (paquete java.util). Las principales son:

  • Listas (List): elementos en orden, con posición, admiten repetidos. Es lo que ves en esta lección.
  • Conjuntos (Set) y mapas (Map): sin duplicados o con pares clave-valor. Los verás en HashMap y HashSet.

Genéricos: el tipo entre < >

Al crear una lista indicas entre ángulos qué tipo de elementos va a guardar:

import java.util.ArrayList;

ArrayList<String> tareas = new ArrayList<>();

Esto se llama genéricos. Una clase como ArrayList<T> está escrita para funcionar con cualquier tipo T, y tú eliges cuál al usarla. Así el compilador te protege:

tareas.add("Estudiar Java");   // correcto
tareas.add(42);                // error de compilación: 42 no es un String

Los <> vacíos de la derecha (el operador diamante) significan “el mismo tipo que a la izquierda”; no hace falta repetirlo.

Cuidado: los genéricos solo admiten objetos, no tipos primitivos. No existe ArrayList<int>: se usa la clase envoltorio ArrayList<Integer>, ArrayList<Double>, etc. Java convierte automáticamente entre int e Integer (autoboxing).

Operaciones básicas de ArrayList

Vamos a gestionar una lista de tareas pendientes.

Añadir: add

ArrayList<String> tareas = new ArrayList<>();
tareas.add("Comprar pan");
tareas.add("Estudiar Java");
tareas.add("Ir al gimnasio");
System.out.println(tareas);        // [Comprar pan, Estudiar Java, Ir al gimnasio]
System.out.println(tareas.size()); // 3

tareas.add(1, "Llamar a Ana");     // inserta en la posición 1
System.out.println(tareas);        // [Comprar pan, Llamar a Ana, Estudiar Java, Ir al gimnasio]

add(elemento) añade al final; add(posicion, elemento) inserta en medio y desplaza los demás. Fíjate en que una lista se imprime directamente con println, algo que con un array no funciona.

Consultar y cambiar: get y set

System.out.println(tareas.get(0)); // Comprar pan
tareas.set(2, "Estudiar Java 21"); // sustituye el elemento de la posición 2
System.out.println(tareas);        // [Comprar pan, Llamar a Ana, Estudiar Java 21, Ir al gimnasio]

Las posiciones empiezan en 0, como en los arrays. Si pides una que no existe, salta una excepción: IndexOutOfBoundsException.

Borrar: remove

remove se puede usar de dos formas: por posición o por objeto.

tareas.remove(0);                  // borra la posición 0
System.out.println(tareas);        // [Llamar a Ana, Estudiar Java 21, Ir al gimnasio]

tareas.remove("Ir al gimnasio");   // borra ese elemento (la primera vez que aparece)
System.out.println(tareas);        // [Llamar a Ana, Estudiar Java 21]

Al borrar, los elementos de detrás se desplazan para no dejar huecos.

Cuidado con las listas de Integer: remove(1) borra la posición 1, no el número 1. Para borrar un valor tienes que pasar un objeto Integer:

List<Integer> numeros = new ArrayList<>(List.of(10, 20, 30, 40));
numeros.remove(1);                   // borra la posición 1 (el 20)
System.out.println(numeros);         // [10, 30, 40]
numeros.remove(Integer.valueOf(30)); // borra el valor 30
System.out.println(numeros);         // [10, 40]

Buscar y otras consultas

System.out.println(tareas.contains("Llamar a Ana"));    // true
System.out.println(tareas.indexOf("Estudiar Java 21")); // 1
System.out.println(tareas.indexOf("Cocinar"));          // -1 (no está)
System.out.println(tareas.isEmpty());                   // false

tareas.clear();                                         // vacía la lista
System.out.println(tareas.isEmpty() + " " + tareas.size()); // true 0

contains e indexOf comparan con equals, así que funcionan bien con String. Con tus propias clases, necesitan que hayas sobrescrito equals (lo viste en herencia).

Recorrer una lista

Tienes cuatro formas. Usaremos las notas de un alumno:

List<Double> notas = new ArrayList<>(List.of(6.5, 8.0, 4.5, 9.0));

1. for clásico, cuando necesitas la posición:

for (int i = 0; i < notas.size(); i++) {
    System.out.println("Examen " + (i + 1) + ": " + notas.get(i));
}
// Examen 1: 6.5
// Examen 2: 8.0
// Examen 3: 4.5
// Examen 4: 9.0

2. for-each, la más habitual cuando solo te interesan los valores:

double suma = 0;
for (double n : notas) {
    suma += n;
}
System.out.println("Media: " + suma / notas.size()); // Media: 7.0

3. forEach con una lambda, muy compacta:

notas.forEach(n -> System.out.print(n + " ")); // 6.5 8.0 4.5 9.0

La expresión n -> ... es una lambda: una función corta que se aplica a cada elemento. Las estudiarás a fondo en lambdas y streams.

4. Iterator, necesario para borrar mientras recorres.

Borrar mientras recorres

Si borras elementos dentro de un for-each, Java lanza ConcurrentModificationException: la lista ha cambiado por debajo del bucle.

List<Double> notas = new ArrayList<>(List.of(6.5, 4.0, 8.0, 3.5, 9.0));
for (Double n : notas) {
    if (n < 5) notas.remove(n);   // ConcurrentModificationException
}

La solución clásica es un Iterator, que sabe borrar el elemento actual de forma segura:

Iterator<Double> it = notas.iterator();
while (it.hasNext()) {
    double n = it.next();
    if (n < 5) {
        it.remove();          // borra el último elemento devuelto por next()
    }
}
System.out.println(notas);    // [6.5, 8.0, 9.0]

Y la forma moderna, en una línea, con removeIf:

notas.removeIf(n -> n < 5);   // mismo resultado: [6.5, 8.0, 9.0]

Listas inmutables con List.of

List.of(...) crea una lista rápidamente, pero inmutable: no se puede añadir, borrar ni cambiar nada.

List<String> dias = List.of("lunes", "martes", "miércoles");
dias.add("jueves");   // UnsupportedOperationException

Es perfecta para datos fijos. Si necesitas una lista modificable con valores iniciales, copia su contenido en un ArrayList:

List<String> copia = new ArrayList<>(dias);
copia.add("jueves");
System.out.println(copia); // [lunes, martes, miércoles, jueves]

List.of tampoco admite elementos null.

Ordenar listas

Orden natural

Collections.sort(lista) o lista.sort(null) ordenan según el orden natural del tipo: alfabético para String, de menor a mayor para números.

List<String> frutas = new ArrayList<>(List.of("pera", "manzana", "kiwi", "uva", "plátano"));
Collections.sort(frutas);
System.out.println(frutas); // [kiwi, manzana, pera, plátano, uva]

Ese orden natural lo define la interfaz Comparable que viste en clases abstractas e interfaces.

Cuidado: el orden de String distingue mayúsculas: "Bea" va antes que "ana" porque las mayúsculas tienen códigos menores. Usa lista.sort(String.CASE_INSENSITIVE_ORDER) para ignorarlo.

Otro orden: Comparator

Para ordenar con otro criterio, se pasa un Comparator a lista.sort(...). Lo más cómodo es construirlo con sus métodos de fábrica:

frutas.sort(Comparator.comparing(String::length)); // por longitud
System.out.println(frutas); // [uva, kiwi, pera, manzana, plátano]

frutas.sort(Comparator.reverseOrder());            // alfabético al revés
System.out.println(frutas); // [uva, plátano, pera, manzana, kiwi]

String::length es una referencia a método: significa “usa la longitud de cada elemento como criterio”.

Ordenar objetos

Lo más habitual es ordenar objetos por alguno de sus atributos. Con un record de alumnos:

record Alumno(String nombre, String grupo, double nota) {}

List<Alumno> clase = new ArrayList<>();
clase.add(new Alumno("Lucía", "1B", 8.5));
clase.add(new Alumno("Mario", "1A", 6.0));
clase.add(new Alumno("Aitana", "1B", 9.2));
clase.add(new Alumno("Hugo", "1A", 8.5));
  • comparing: ordena por un campo.
  • reversed(): invierte el orden.
  • thenComparing: criterio de desempate.
// De mayor a menor nota; si empatan, por nombre
clase.sort(Comparator.comparing(Alumno::nota).reversed()
                     .thenComparing(Alumno::nombre));
clase.forEach(a -> System.out.println(a.nombre() + " " + a.nota()));
// Aitana 9.2
// Hugo 8.5
// Lucía 8.5
// Mario 6.0

// Por grupo y, dentro de cada grupo, por nombre
clase.sort(Comparator.comparing(Alumno::grupo).thenComparing(Alumno::nombre));
clase.forEach(a -> System.out.println(a.grupo() + " " + a.nombre()));
// 1A Hugo
// 1A Mario
// 1B Aitana
// 1B Lucía

Collections trae además otras utilidades: Collections.max(lista), Collections.min(lista), Collections.reverse(lista) o Collections.shuffle(lista) para barajar.

LinkedList y cuándo usarla

LinkedList es otra implementación de List. En lugar de guardar los elementos en un array interno, cada elemento apunta al siguiente y al anterior, como los vagones de un tren.

OperaciónArrayListLinkedList
get(i) (acceso por posición)Muy rápidoLento: recorre desde un extremo
Añadir al finalRápidoRápido
Insertar o borrar al principioLento: desplaza todoMuy rápido
MemoriaMenosMás (guarda enlaces)

LinkedList tiene métodos para trabajar por los extremos, útil para una cola de atención:

LinkedList<String> cola = new LinkedList<>();
cola.add("Cliente 1");
cola.add("Cliente 2");
cola.addFirst("Cliente VIP");
System.out.println(cola);               // [Cliente VIP, Cliente 1, Cliente 2]
System.out.println(cola.removeFirst()); // Cliente VIP

En la práctica, ArrayList es la opción correcta en casi todos los casos. Usa LinkedList solo si insertas y borras mucho por el principio.

Programar contra List, no contra la implementación

Habrás visto que a veces escribimos ArrayList<String> x y otras List<String> x. Lo recomendable es lo segundo:

List<String> tareas = new ArrayList<>();

List es una interfaz y ArrayList una clase que la implementa. Si tus variables y parámetros usan List, puedes cambiar a LinkedList tocando una sola línea, y tus métodos aceptan cualquier tipo de lista, incluida una de List.of. Es el polimorfismo en acción.

public static double media(List<Double> notas) { ... }  // acepta cualquier List

Array frente a ArrayList

ArrayArrayList
TamañoFijo al crearloCrece y encoge solo
Tipos primitivosSí (int[])No, usa envoltorios (Integer)
Longitudarray.lengthlista.size()
Leer / escribira[i], a[i] = xget(i), set(i, x)
Añadir o borrarHay que hacerlo a manoadd, remove
ImprimirArrays.toString(a)println(lista)
RendimientoAlgo mejorMuy bueno

Usa un array cuando el tamaño es fijo y conocido (los 12 meses, un tablero de 8x8). Para todo lo demás, una lista.

Errores frecuentes

  • Usar ArrayList<int>. No compila: los genéricos necesitan Integer.
  • Confundir remove(int) y remove(Object) en listas de Integer.
  • Usar length o length() en una lista. Es size().
  • Pedir una posición que no existe con get: IndexOutOfBoundsException. Recuerda que la última es size() - 1.
  • Borrar dentro de un for-each. Usa Iterator o removeIf.
  • Intentar modificar una lista de List.of o de Arrays.asList (esta última no permite añadir ni borrar).
  • Comparar listas con ==. Para saber si tienen los mismos elementos usa lista1.equals(lista2).

Resumen

OperaciónCódigo
CrearList<String> l = new ArrayList<>();
Añadirl.add(x), l.add(i, x)
Leer / cambiarl.get(i), l.set(i, x)
Borrarl.remove(i), l.remove(obj), l.removeIf(...), l.clear()
Consultarl.size(), l.isEmpty(), l.contains(x), l.indexOf(x)
Recorrerfor, for-each, forEach, Iterator
Lista fijaList.of(a, b, c)
OrdenarCollections.sort(l), l.sort(Comparator.comparing(...))

En la siguiente lección conocerás las otras dos grandes colecciones: HashMap y HashSet.

Pon a prueba lo que has aprendido

[Java] ¿Qué imprime este código?
List<Integer> puntos = new ArrayList<>(List.of(5, 1, 3, 1));
puntos.remove(1);
System.out.println(puntos);

[Java] ¿Qué imprime este código?
List<String> l = new ArrayList<>();
l.add("a");
l.add("b");
l.add(0, "c");
l.set(1, "d");
System.out.println(l + " " + l.size());

[Java] ¿Qué ocurre al ejecutar este código?
List<String> dias = List.of("lunes", "martes");
dias.add("miércoles");

[Java] Tienes una lista de productos y quieres ordenarla de mayor a menor precio y, si empatan, por nombre. ¿Qué Comparator usas?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.