Los arrays en Java son rígidos. Cuando los aprendes, todo parece bien: defines el tamaño, metes elementos, listo. El problema llega cuando necesitas añadir un elemento más y el array ya está lleno, o cuando quieres eliminar uno del medio sin montarte una pesadilla con índices.
Para eso existen las colecciones. Son estructuras de datos que Java ya tiene listas para que no tengas que gestionar el tamaño ni mover elementos a mano. Aquí vamos a ver las que vas a usar el 90% del tiempo: ArrayList, HashMap y HashSet.
Antes de empezar necesitas:
El problema con los arrays
Imagina que estás guardando los nombres de los participantes de un evento. Al principio creas un array de 50 posiciones. Luego se apuntan 60 personas. Problema.
Con un array normal en Java tienes que declarar el tamaño de antemano y no puedes cambiarlo:
// Array normal: tamaño fijo desde el principio
String[] participantes = new String[50];
// Si se apuntan 51 personas → ArrayIndexOutOfBoundsException
// Tienes que crear un array nuevo, copiar todo... un lío
Las colecciones resuelven exactamente esto. Son objetos que gestionan el tamaño internamente. Añades elementos con add(), los eliminas con remove(), y la colección se encarga del resto.
ArrayList: la lista que crece sola
ArrayList es básicamente un array que se redimensiona automáticamente. Mantiene el orden en que añades los elementos, permite duplicados y puedes acceder a cualquier posición por su índice, igual que con un array normal.
Para usarlo necesitas importarlo. IntelliJ lo hace automáticamente cuando escribes el nombre y pulsas Alt+Enter, pero si ves un error de compilación revisa que tienes el import arriba:
import java.util.ArrayList;
import java.util.List;
public class EjemploArrayList {
public static void main(String[] args) {
// Crear un ArrayList de Strings
List<String> participantes = new ArrayList<>();
// Añadir elementos
participantes.add("María");
participantes.add("Carlos");
participantes.add("Ana");
// Ver cuántos hay
System.out.println("Participantes: " + participantes.size()); // 3
// Acceder por índice (empieza en 0, igual que los arrays)
System.out.println(participantes.get(0)); // María
// Eliminar por índice
participantes.remove(1); // Elimina "Carlos"
// Comprobar si contiene un elemento
boolean tieneAna = participantes.contains("Ana"); // true
// Recorrer con for-each
for (String nombre : participantes) {
System.out.println(nombre);
}
}
}
⚠️ El error más común con ArrayList:
Usar remove(1) cuando quieres eliminar el número 1 como valor. Si tienes un List<Integer> y llamas remove(1), Java interpreta que quieres eliminar el elemento en el índice 1, no el valor 1. Para eliminar por valor usa remove(Integer.valueOf(1)). Esto confunde a todo el mundo la primera vez.
Declarar con List, no con ArrayList
Fíjate que en el ejemplo la variable está declarada como List<String> aunque se instancia con new ArrayList<>(). Es un hábito bueno desde el principio: programar contra la interfaz, no la implementación. Si algún día decides cambiar ArrayList por LinkedList, solo tienes que cambiar una línea. Lo verás mucho en código de empresa.
Métodos que vas a usar siempre
List<String> lista = new ArrayList<>();
lista.add("elemento"); // Añade al final
lista.add(0, "primero"); // Inserta en posición específica
lista.get(0); // Lee el elemento en índice 0
lista.set(0, "nuevo"); // Reemplaza el elemento en índice 0
lista.remove(0); // Elimina por índice
lista.remove("elemento"); // Elimina por valor
lista.size(); // Número de elementos
lista.isEmpty(); // true si está vacía
lista.contains("elemento"); // true si existe el valor
lista.indexOf("elemento"); // Índice del elemento (-1 si no existe)
lista.clear(); // Vacía la lista
HashMap: guardar datos en pares clave-valor
Un ArrayList es una lista ordenada. Pero a veces no quieres una lista, quieres un diccionario: dado un nombre, dame su teléfono. Dado un DNI, dame el empleado. Dado un código de producto, dame el precio.
Para eso existe HashMap. Funciona con pares clave-valor: tú defines la clave (que tiene que ser única) y el valor asociado. Buscar por clave es muy rápido, independientemente de cuántos elementos tenga el mapa.
import java.util.HashMap;
import java.util.Map;
public class EjemploHashMap {
public static void main(String[] args) {
// Mapa: String (nombre) → Integer (edad)
Map<String, Integer> edades = new HashMap<>();
// Añadir pares clave-valor
edades.put("María", 28);
edades.put("Carlos", 34);
edades.put("Ana", 25);
// Obtener un valor por su clave
int edadMaria = edades.get("María");
System.out.println("Edad de María: " + edadMaria); // 28
// Comprobar si existe una clave
if (edades.containsKey("Pedro")) {
System.out.println("Pedro está en el mapa");
}
// Eliminar una entrada
edades.remove("Carlos");
// Recorrer todas las entradas
for (Map.Entry<String, Integer> entrada : edades.entrySet()) {
System.out.println(entrada.getKey() + " tiene " + entrada.getValue() + " años");
}
}
}
⚠️ HashMap no garantiza orden:
Si añades tres elementos y los recorres, pueden salir en cualquier orden. No es un bug, es el diseño. HashMap prioriza velocidad sobre orden. Si necesitas que los elementos mantengan el orden de inserción, usa LinkedHashMap. Si necesitas orden alfabético por clave, usa TreeMap.
getOrDefault: evitar NullPointerException
Esto es lo que más duele al principio: llamas a get() con una clave que no existe y obtienes null. Si después intentas usar ese valor, NullPointerException. Para evitarlo, usa getOrDefault:
// ❌ Peligroso si "Pedro" no existe
int edad = edades.get("Pedro"); // NullPointerException
// ✅ Seguro: devuelve 0 si la clave no existe
int edadSegura = edades.getOrDefault("Pedro", 0);
Métodos principales de HashMap
Map<String, Integer> mapa = new HashMap<>();
mapa.put("clave", 42); // Añade o sobreescribe
mapa.get("clave"); // Obtiene valor (null si no existe)
mapa.getOrDefault("clave", 0); // Obtiene valor o el default
mapa.containsKey("clave"); // true si existe esa clave
mapa.containsValue(42); // true si existe ese valor
mapa.remove("clave"); // Elimina la entrada
mapa.size(); // Número de entradas
mapa.isEmpty(); // true si está vacío
mapa.keySet(); // Set con todas las claves
mapa.values(); // Collection con todos los valores
mapa.entrySet(); // Set de pares clave-valor
HashSet: cuando no quieres duplicados
Imagina que estás recogiendo las categorías de los productos de una tienda. Los productos pueden repetirse, pero las categorías no deben hacerlo. Usar un ArrayList te obliga a comprobar cada vez si el elemento ya existe antes de añadirlo. Con un HashSet eso es automático: simplemente ignora los duplicados.
import java.util.HashSet;
import java.util.Set;
public class EjemploHashSet {
public static void main(String[] args) {
Set<String> categorias = new HashSet<>();
categorias.add("Electrónica");
categorias.add("Ropa");
categorias.add("Electrónica"); // Duplicado, se ignora
categorias.add("Alimentación");
System.out.println(categorias.size()); // 3, no 4
for (String cat : categorias) {
System.out.println(cat);
}
// Comprobar si contiene un elemento
boolean tieneRopa = categorias.contains("Ropa"); // true
}
}
Como HashMap, el HashSet tampoco garantiza orden. Si necesitas un Set ordenado alfabéticamente, usa TreeSet.
Iterator: cuando necesitas eliminar mientras recorres
Aquí es donde la mayoría se atasca. Si intentas eliminar elementos de una lista mientras la recorres con un for-each, Java lanza ConcurrentModificationException. No es un bug tuyo, es una protección que tiene Java para evitar comportamientos impredecibles.
// ❌ Esto falla en tiempo de ejecución
List<String> nombres = new ArrayList<>();
nombres.add("María");
nombres.add("test");
nombres.add("Ana");
for (String nombre : nombres) {
if (nombre.equals("test")) {
nombres.remove(nombre); // ConcurrentModificationException
}
}
La solución es usar Iterator, que está diseñado exactamente para esto:
import java.util.Iterator;
// ✅ Correcto: usar Iterator para eliminar mientras se recorre
Iterator<String> it = nombres.iterator();
while (it.hasNext()) {
String nombre = it.next();
if (nombre.equals("test")) {
it.remove(); // remove() del Iterator, no de la lista
}
}
// Alternativa más moderna con removeIf (Java 8+)
nombres.removeIf(nombre -> nombre.equals("test"));
El removeIf con lambda es la opción más limpia si estás en Java 8 o superior. Internamente hace lo mismo que el Iterator pero con menos código.
Cuándo usar cada colección
Esta es la pregunta que todo el mundo tiene y que los tutoriales suelen responder con una tabla que nadie recuerda. Intento hacerlo más concreto:
Guía rápida para elegir:
- ¿Necesitas orden y acceso por índice? →
ArrayList. Es el 70% de los casos. - ¿Buscas por una clave, no por posición? →
HashMap. Ideal para cachés, configuraciones, contadores por clave. - ¿No puedes tener duplicados? →
HashSet. También es muy rápido para comprobar si un elemento existe. - ¿Necesitas orden de inserción en un mapa? →
LinkedHashMap. - ¿Necesitas orden alfabético? →
TreeMapoTreeSet. - ¿Insertas/eliminas mucho en el medio de la lista? →
LinkedList. Aunque en la prácticaArrayListsuele ser suficiente.
Proyecto: agenda de contactos por consola
Nada como construir algo concreto para que todo esto se asiente. Vamos a hacer una agenda básica donde puedes añadir contactos, buscarlos por nombre y listar todos los que hay.
Crea un archivo Agenda.java en tu proyecto de IntelliJ:
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
public class Agenda {
// El mapa almacena nombre → teléfono
private final Map<String, String> contactos = new HashMap<>();
public void añadir(String nombre, String telefono) {
contactos.put(nombre, telefono);
System.out.println("Contacto añadido: " + nombre);
}
public void buscar(String nombre) {
String telefono = contactos.getOrDefault(nombre, "No encontrado");
System.out.println(nombre + ": " + telefono);
}
public void listarTodos() {
if (contactos.isEmpty()) {
System.out.println("La agenda está vacía");
return;
}
System.out.println("--- Contactos ---");
for (Map.Entry<String, String> entrada : contactos.entrySet()) {
System.out.println(entrada.getKey() + ": " + entrada.getValue());
}
}
public static void main(String[] args) {
Agenda agenda = new Agenda();
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("\n1. Añadir 2. Buscar 3. Listar 4. Salir");
System.out.print("Opción: ");
String opcion = scanner.nextLine();
switch (opcion) {
case "1" -> {
System.out.print("Nombre: ");
String nombre = scanner.nextLine();
System.out.print("Teléfono: ");
String tel = scanner.nextLine();
agenda.añadir(nombre, tel);
}
case "2" -> {
System.out.print("Nombre a buscar: ");
agenda.buscar(scanner.nextLine());
}
case "3" -> agenda.listarTodos();
case "4" -> {
System.out.println("Hasta luego");
return;
}
default -> System.out.println("Opción no válida");
}
}
}
}
El switch con -> es la sintaxis moderna de Java 14+. Si tienes Java 21 instalado ya puedes usarlo. Es más limpio que el switch clásico con break.
Cómo ejecutarlo en IntelliJ:
- Haz clic derecho en
Agenda.javaen el panel de la izquierda - Selecciona "Run 'Agenda.main()'"
- La consola aparece abajo: escribe los números para navegar por el menú
- Ojo: IntelliJ a veces no muestra el texto de
System.out.print()hasta que pulsas Enter. Es normal.
Colecciones con tus propias clases
Hasta ahora hemos usado String e Integer, pero en proyectos reales normalmente vas a guardar objetos de tus propias clases. Funciona igual:
import java.util.ArrayList;
import java.util.List;
class Producto {
String nombre;
double precio;
Producto(String nombre, double precio) {
this.nombre = nombre;
this.precio = precio;
}
}
public class Tienda {
public static void main(String[] args) {
List<Producto> catalogo = new ArrayList<>();
catalogo.add(new Producto("Teclado", 45.99));
catalogo.add(new Producto("Ratón", 29.50));
catalogo.add(new Producto("Monitor", 320.00));
// Mostrar productos por encima de 30€
for (Producto p : catalogo) {
if (p.precio > 30) {
System.out.println(p.nombre + ": " + p.precio + "€");
}
}
}
}
Este patrón, lista de objetos propios con filtrado, es exactamente lo que hace Spring Boot cuando devuelve una lista de entidades desde la base de datos. Ya empiezas a ver cómo encajan las piezas. Si te suena raro crear una clase Producto así de simple, puede que te falte revisar cómo funciona la POO en Java con ejemplos reales antes de seguir.
¿Qué aprender ahora?
Con colecciones controladas, el siguiente paso en Java es aprender a crear APIs reales que guarden y recuperen esos datos.
👉 Spring Boot: tu primera aplicación web profesional