Colecciones en Java: ArrayList, HashMap, Set y más

📅 Actualizado en febrero 2026 ✍️ Ángel López ⏱️ 25 min de lectura ✓ Nivel intermedio ★ ★ ★ ★ ★ (5/5)

📚 ¿Qué son las colecciones en Java?

Las colecciones en Java son estructuras de datos dinámicas que permiten almacenar, organizar y manipular grupos de objetos de forma eficiente. A diferencia de los arrays tradicionales, cuyo tamaño es fijo una vez declarado, las colecciones pueden crecer y reducirse automáticamente según las necesidades del programa. Constituyen una de las herramientas más utilizadas en el desarrollo profesional con Java y resultan imprescindibles para cualquier programador que aspire a escribir código robusto y mantenible.

El Java Collections Framework (JCF) es la arquitectura unificada que proporciona interfaces, implementaciones y algoritmos para trabajar con colecciones. Fue introducido en Java 2 (JDK 1.2) y ha evolucionado significativamente, especialmente con la llegada de los genéricos en Java 5 y los Streams en Java 8. Este framework se encuentra en el paquete java.util y resuelve problemas fundamentales como almacenar listas de elementos, asociar claves a valores, eliminar duplicados o gestionar colas de procesamiento.

💡 ¿Por qué usar colecciones en lugar de arrays? Los arrays son rápidos y ligeros, pero su tamaño es inmutable y carecen de métodos de búsqueda, ordenación o eliminación. Las colecciones ofrecen redimensionamiento automático, una API rica en operaciones, implementaciones optimizadas para distintos patrones de acceso y compatibilidad con genéricos para type safety en tiempo de compilación.

En la práctica, las colecciones son la espina dorsal de casi cualquier aplicación Java: desde gestionar la lista de productos de un carrito de compra hasta indexar millones de registros por clave en un servidor de alto rendimiento. Dominar las colecciones no es opcional para un desarrollador Java profesional: es un requisito fundamental.

🗂️ Jerarquía del Collections Framework

El Collections Framework de Java se organiza en torno a un conjunto de interfaces que definen los contratos de comportamiento, y múltiples clases concretas que los implementan. Comprender esta jerarquía es esencial para elegir la estructura de datos correcta en cada situación.

🔹 Interfaces principales

En la raíz de la jerarquía se encuentra la interfaz Iterable, que permite recorrer cualquier colección con un bucle for-each. De ella desciende Collection, la interfaz madre de la que derivan tres grandes ramas:

Interfaz Descripción Permite duplicados Ordenada Implementaciones principales
List Secuencia ordenada de elementos con acceso posicional ✅ Sí ✅ Por inserción ArrayList, LinkedList, Vector
Set Colección sin elementos duplicados ❌ No Depende HashSet, TreeSet, LinkedHashSet
Queue Cola para procesamiento en orden FIFO o por prioridad ✅ Sí ✅ FIFO/prioridad LinkedList, PriorityQueue, ArrayDeque
Map Asociación de claves únicas a valores Claves: ❌ / Valores: ✅ Depende HashMap, TreeMap, LinkedHashMap
✅ Nota importante: Map no extiende de Collection. Aunque se incluye dentro del Collections Framework, su estructura clave-valor la hace conceptualmente diferente. Sin embargo, se puede obtener una vista de tipo Collection a través de map.values(), map.keySet() o map.entrySet().

🔹 Diagrama conceptual de la jerarquía

Jerarquía del Collections Framework
                    Iterable
                       │
                   Collection              Map
                   /   |   \                │
                 /     |     \          ┌───┴───┐
              List    Set   Queue    HashMap  SortedMap
              / \     / \     |                  │
      ArrayList  │  HashSet  Deque            TreeMap
      LinkedList │  TreeSet  / \
                 │  LinkedHS  ArrayDeque
            SortedSet         PriorityQueue
                │
             TreeSet

📋 La interfaz List: ArrayList y LinkedList

La interfaz List representa una secuencia ordenada de elementos que permite acceso por índice posicional, duplicados y control preciso sobre dónde se inserta cada elemento. Es la estructura más utilizada en la programación Java del día a día.

▶️ ArrayList: el caballo de batalla

ArrayList es la implementación más popular de List. Internamente utiliza un array dinámico que se redimensiona automáticamente cuando se llena (normalmente al 150% de su capacidad actual). Su principal ventaja es el acceso aleatorio en tiempo constante O(1), lo que la convierte en la opción predeterminada para la mayoría de casos.

ArrayList — Operaciones básicas
import java.util.ArrayList;
import java.util.List;

public class EjemploArrayList {
    public static void main(String[] args) {
        // Crear un ArrayList con genéricos (type-safe)
        List<String> lenguajes = new ArrayList<>();

        // Añadir elementos
        lenguajes.add("Java");
        lenguajes.add("Python");
        lenguajes.add("JavaScript");
        lenguajes.add("C++");
        lenguajes.add("Java");  // Los duplicados están permitidos

        System.out.println("Lista completa: " + lenguajes);
        System.out.println("Elemento en índice 2: " + lenguajes.get(2));
        System.out.println("Tamaño: " + lenguajes.size());

        // Modificar un elemento
        lenguajes.set(3, "Go");
        System.out.println("Tras modificar índice 3: " + lenguajes);

        // Eliminar por índice y por objeto
        lenguajes.remove(0);           // Elimina "Java" (el primero)
        lenguajes.remove("Python");    // Elimina la primera ocurrencia
        System.out.println("Tras eliminaciones: " + lenguajes);

        // Buscar elementos
        System.out.println("¿Contiene Go? " + lenguajes.contains("Go"));
        System.out.println("Índice de JavaScript: " + lenguajes.indexOf("JavaScript"));
    }
}
Salida esperada
Lista completa: [Java, Python, JavaScript, C++, Java]
Elemento en índice 2: JavaScript
Tamaño: 5
Tras modificar índice 3: [Java, Python, JavaScript, Go, Java]
Tras eliminaciones: [JavaScript, Go, Java]
¿Contiene Go? true
Índice de JavaScript: 0

▶️ LinkedList: ideal para inserciones frecuentes

LinkedList implementa tanto List como Deque. Utiliza una lista doblemente enlazada donde cada nodo apunta al anterior y al siguiente. Su fortaleza está en las inserciones y eliminaciones en los extremos o en posiciones ya localizadas por un iterador, que se realizan en O(1). Sin embargo, el acceso por índice requiere recorrer la lista, resultando en O(n).

LinkedList — Uso como lista y como cola
import java.util.LinkedList;

public class EjemploLinkedList {
    public static void main(String[] args) {
        LinkedList<String> tareas = new LinkedList<>();

        // Operaciones de List
        tareas.add("Diseñar base de datos");
        tareas.add("Implementar API REST");
        tareas.add("Escribir tests unitarios");

        // Operaciones específicas de Deque
        tareas.addFirst("Definir requisitos");      // Insertar al inicio
        tareas.addLast("Desplegar en producción");   // Insertar al final

        System.out.println("Tareas: " + tareas);
        System.out.println("Primera: " + tareas.getFirst());
        System.out.println("Última: " + tareas.getLast());

        // Usar como cola (FIFO)
        String siguiente = tareas.poll();  // Retira y devuelve el primero
        System.out.println("Procesando: " + siguiente);
        System.out.println("Restantes: " + tareas);
    }
}
Salida esperada
Tareas: [Definir requisitos, Diseñar base de datos, Implementar API REST, Escribir tests unitarios, Desplegar en producción]
Primera: Definir requisitos
Última: Desplegar en producción
Procesando: Definir requisitos
Restantes: [Diseñar base de datos, Implementar API REST, Escribir tests unitarios, Desplegar en producción]
Operación ArrayList LinkedList
Acceso por índice get(i) O(1) O(n) ❌
Inserción al final add(e) O(1) amortizado O(1)
Inserción al inicio add(0, e) O(n) ❌ O(1)
Eliminación por índice O(n) O(n) localizar + O(1) eliminar
Consumo de memoria Menor (array compacto) Mayor (nodos con punteros)
✅ Regla práctica: Usa ArrayList como opción predeterminada. Solo elige LinkedList cuando tu caso de uso implique inserciones o eliminaciones frecuentes en los extremos de la lista y rara vez necesites acceso por índice.

🔵 La interfaz Set: HashSet, TreeSet y LinkedHashSet

La interfaz Set modela el concepto matemático de conjunto: una colección que no permite elementos duplicados. Es la estructura ideal cuando necesitamos garantizar la unicidad de los datos, como un conjunto de identificadores, correos electrónicos o códigos de producto.

▶️ HashSet: máximo rendimiento

HashSet almacena los elementos en una tabla hash (internamente usa un HashMap). Ofrece operaciones de inserción, búsqueda y eliminación en O(1) amortizado, pero no garantiza ningún orden de iteración. Es la implementación más rápida y la más utilizada.

HashSet — Eliminar duplicados y operaciones de conjuntos
import java.util.HashSet;
import java.util.Set;
import java.util.Arrays;

public class EjemploHashSet {
    public static void main(String[] args) {
        // Crear un HashSet e intentar añadir duplicados
        Set<String> tecnologias = new HashSet<>();
        tecnologias.add("Java");
        tecnologias.add("Spring");
        tecnologias.add("Docker");
        tecnologias.add("Java");    // Duplicado: será ignorado
        tecnologias.add("Docker");  // Duplicado: será ignorado

        System.out.println("Tecnologías únicas: " + tecnologias);
        System.out.println("Tamaño: " + tecnologias.size());  // 3

        // Operaciones de conjuntos
        Set<String> backend = new HashSet<>(Arrays.asList("Java", "Spring", "PostgreSQL"));
        Set<String> devops = new HashSet<>(Arrays.asList("Docker", "Kubernetes", "Java"));

        // Unión
        Set<String> union = new HashSet<>(backend);
        union.addAll(devops);
        System.out.println("Unión: " + union);

        // Intersección
        Set<String> interseccion = new HashSet<>(backend);
        interseccion.retainAll(devops);
        System.out.println("Intersección: " + interseccion);

        // Diferencia
        Set<String> soloBackend = new HashSet<>(backend);
        soloBackend.removeAll(devops);
        System.out.println("Solo en backend: " + soloBackend);
    }
}
Salida esperada
Tecnologías únicas: [Java, Docker, Spring]
Tamaño: 3
Unión: [Java, Docker, Spring, PostgreSQL, Kubernetes]
Intersección: [Java]
Solo en backend: [Spring, PostgreSQL]

▶️ TreeSet: elementos ordenados automáticamente

TreeSet mantiene los elementos ordenados de forma natural (orden alfabético para String, numérico para Integer) o según un Comparator proporcionado en el constructor. Internamente usa un árbol rojo-negro, con operaciones en O(log n). No permite null.

TreeSet — Orden natural y métodos de navegación
import java.util.TreeSet;

public class EjemploTreeSet {
    public static void main(String[] args) {
        TreeSet<Integer> calificaciones = new TreeSet<>();
        calificaciones.add(85);
        calificaciones.add(92);
        calificaciones.add(78);
        calificaciones.add(95);
        calificaciones.add(88);

        System.out.println("Ordenadas: " + calificaciones);
        System.out.println("Menor: " + calificaciones.first());
        System.out.println("Mayor: " + calificaciones.last());
        System.out.println("Menores que 90: " + calificaciones.headSet(90));
        System.out.println("Desde 85 en adelante: " + calificaciones.tailSet(85));
        System.out.println("Entre 80 y 92: " + calificaciones.subSet(80, true, 92, true));
    }
}
Salida esperada
Ordenadas: [78, 85, 88, 92, 95]
Menor: 78
Mayor: 95
Menores que 90: [78, 85, 88]
Desde 85 en adelante: [85, 88, 92, 95]
Entre 80 y 92: [85, 88, 92]

▶️ LinkedHashSet: orden de inserción preservado

LinkedHashSet combina la rapidez de HashSet con una lista enlazada interna que preserva el orden de inserción. Es útil cuando necesitas unicidad pero también quieres iterar en el mismo orden en que añadiste los elementos. Su rendimiento es ligeramente inferior al de HashSet debido a la lista enlazada adicional.

Característica HashSet TreeSet LinkedHashSet
Rendimiento O(1) O(log n) O(1)
Orden de iteración No garantizado Orden natural/Comparator Orden de inserción
Permite null ✅ Sí (uno) ❌ No ✅ Sí (uno)
Caso de uso Unicidad sin orden Unicidad + orden natural Unicidad + orden inserción

🗺️ La interfaz Map: HashMap, TreeMap y LinkedHashMap

La interfaz Map almacena pares clave-valor donde cada clave es única y apunta a exactamente un valor. Es el equivalente Java de los diccionarios en Python o los objetos en JavaScript. Los mapas son fundamentales para indexar datos, crear cachés, contadores de frecuencia y cualquier escenario donde necesitemos buscar valores por una clave identificadora.

▶️ HashMap: la implementación estándar

HashMap es la implementación más utilizada de Map. Almacena las entradas en una tabla hash con operaciones en O(1) amortizado. No garantiza orden de iteración. Permite una clave null y múltiples valores null.

HashMap — Sistema de inventario de productos
import java.util.HashMap;
import java.util.Map;

public class Inventario {
    public static void main(String[] args) {
        // Clave: código producto, Valor: cantidad en stock
        Map<String, Integer> stock = new HashMap<>();

        // Añadir productos
        stock.put("LAPTOP-001", 45);
        stock.put("MOUSE-002", 200);
        stock.put("TECLADO-003", 150);
        stock.put("MONITOR-004", 30);

        // Consultar stock
        System.out.println("Stock del monitor: " + stock.get("MONITOR-004"));

        // getOrDefault: evitar NullPointerException
        int stockCable = stock.getOrDefault("CABLE-005", 0);
        System.out.println("Stock del cable: " + stockCable);

        // Actualizar stock (venta de 5 laptops)
        stock.put("LAPTOP-001", stock.get("LAPTOP-001") - 5);

        // Recorrer el mapa completo
        System.out.println("\n--- Inventario completo ---");
        for (Map.Entry<String, Integer> entrada : stock.entrySet()) {
            System.out.printf("%-15s → %d unidades%n",
                entrada.getKey(), entrada.getValue());
        }

        // Verificar existencia de clave
        System.out.println("\n¿Existe LAPTOP-001? " + stock.containsKey("LAPTOP-001"));
        System.out.println("¿Algún producto con 200 unidades? " + stock.containsValue(200));

        // Métodos Java 8+
        stock.putIfAbsent("CABLE-005", 500);  // Solo inserta si no existe
        stock.merge("MOUSE-002", 50, Integer::sum);  // Sumar 50 al stock actual
        System.out.println("\nStock del ratón actualizado: " + stock.get("MOUSE-002"));
    }
}
Salida esperada
Stock del monitor: 30
Stock del cable: 0

--- Inventario completo ---
LAPTOP-001      → 40 unidades
MOUSE-002       → 200 unidades
MONITOR-004     → 30 unidades
TECLADO-003     → 150 unidades

¿Existe LAPTOP-001? true
¿Algún producto con 200 unidades? true

Stock del ratón actualizado: 250

▶️ TreeMap y LinkedHashMap

TreeMap ordena las entradas por la clave de forma natural o mediante un Comparator, con operaciones en O(log n). LinkedHashMap mantiene el orden de inserción (o de acceso, si se configura) y resulta ideal para implementar cachés LRU (Least Recently Used). Ambas alternativas sacrifican algo de rendimiento frente a HashMap a cambio de funcionalidades adicionales.

TreeMap — Ranking de puntuaciones ordenado
import java.util.TreeMap;
import java.util.Map;

public class RankingPuntuaciones {
    public static void main(String[] args) {
        // Orden natural de las claves (alfabético)
        TreeMap<String, Integer> ranking = new TreeMap<>();
        ranking.put("Ana", 950);
        ranking.put("Carlos", 870);
        ranking.put("Elena", 920);
        ranking.put("Bruno", 890);

        System.out.println("Ranking alfabético: " + ranking);
        System.out.println("Primer jugador: " + ranking.firstKey());
        System.out.println("Último jugador: " + ranking.lastKey());

        // Jugadores del rango B-D (inclusive)
        Map<String, Integer> rangoBC = ranking.subMap("B", true, "D", true);
        System.out.println("Jugadores B-D: " + rangoBC);
    }
}
Salida esperada
Ranking alfabético: {Ana=950, Bruno=890, Carlos=870, Elena=920}
Primer jugador: Ana
Último jugador: Elena
Jugadores B-D: {Bruno=890, Carlos=870}

📬 La interfaz Queue y Deque

Las colas modelan el principio FIFO (First In, First Out): el primer elemento que entra es el primero en salir, como una fila en una taquilla. La interfaz Queue proporciona métodos para añadir, consultar y retirar elementos del frente de la cola.

La interfaz Deque (double-ended queue) extiende Queue permitiendo operaciones en ambos extremos: se puede insertar y retirar tanto por el inicio como por el final. Esto permite usarla también como una pila (LIFO).

PriorityQueue — Cola de tareas con prioridad
import java.util.PriorityQueue;
import java.util.ArrayDeque;
import java.util.Deque;

public class EjemploColas {
    public static void main(String[] args) {
        // PriorityQueue: ordena automáticamente por valor natural
        PriorityQueue<Integer> urgencias = new PriorityQueue<>();
        urgencias.add(5);  // Prioridad baja
        urgencias.add(1);  // Prioridad alta
        urgencias.add(3);  // Prioridad media

        System.out.println("--- Cola de prioridad ---");
        while (!urgencias.isEmpty()) {
            System.out.println("Procesando prioridad: " + urgencias.poll());
        }

        // ArrayDeque: usarla como pila (LIFO)
        System.out.println("\n--- Pila con ArrayDeque ---");
        Deque<String> pila = new ArrayDeque<>();
        pila.push("Página 1");
        pila.push("Página 2");
        pila.push("Página 3");

        while (!pila.isEmpty()) {
            System.out.println("Volviendo a: " + pila.pop());
        }
    }
}
Salida esperada
--- Cola de prioridad ---
Procesando prioridad: 1
Procesando prioridad: 3
Procesando prioridad: 5

--- Pila con ArrayDeque ---
Volviendo a: Página 3
Volviendo a: Página 2
Volviendo a: Página 1
⚠️ Cuidado: Aunque Stack existe en Java, está considerada una clase legacy. La documentación oficial recomienda usar ArrayDeque como pila (LIFO) y como cola (FIFO), ya que es más eficiente y no tiene la sobrecarga de sincronización de Stack.
Método Lanza excepción Devuelve valor especial
Insertar add(e) offer(e) → false si no puede
Consultar cabeza element() peek() → null si está vacía
Retirar cabeza remove() poll() → null si está vacía

🔄 Recorrer colecciones: Iterator, for-each y Streams

Java ofrece múltiples mecanismos para iterar sobre los elementos de una colección. Cada uno tiene su contexto óptimo de uso y conocerlos todos permite escribir código más expresivo y eficiente.

▶️ Cuatro formas de recorrer una colección

Formas de iteración en Java
import java.util.*;
import java.util.stream.Collectors;

public class FormasDeIteracion {
    public static void main(String[] args) {
        List<String> ciudades = List.of("Madrid", "Barcelona", "Sevilla", "Valencia");

        // 1. Bucle for clásico (solo para List, acceso por índice)
        System.out.println("--- for clásico ---");
        for (int i = 0; i < ciudades.size(); i++) {
            System.out.println(i + ": " + ciudades.get(i));
        }

        // 2. For-each (para cualquier Iterable)
        System.out.println("\n--- for-each ---");
        for (String ciudad : ciudades) {
            System.out.println(ciudad);
        }

        // 3. Iterator explícito (permite eliminar durante iteración)
        System.out.println("\n--- Iterator ---");
        List<String> copia = new ArrayList<>(ciudades);
        Iterator<String> it = copia.iterator();
        while (it.hasNext()) {
            String ciudad = it.next();
            if (ciudad.startsWith("S")) {
                it.remove();  // Eliminación segura durante iteración
            }
        }
        System.out.println("Sin ciudades con S: " + copia);

        // 4. Streams (Java 8+): estilo funcional declarativo
        System.out.println("\n--- Streams ---");
        List<String> conM = ciudades.stream()
            .filter(c -> c.startsWith("M") || c.startsWith("V"))
            .map(String::toUpperCase)
            .sorted()
            .collect(Collectors.toList());
        System.out.println("Filtradas y en mayúsculas: " + conM);
    }
}
Salida esperada
--- for clásico ---
0: Madrid
1: Barcelona
2: Sevilla
3: Valencia

--- for-each ---
Madrid
Barcelona
Sevilla
Valencia

--- Iterator ---
Sin ciudades con S: [Madrid, Barcelona, Valencia]

--- Streams ---
Filtradas y en mayúsculas: [MADRID, VALENCIA]
⚠️ ConcurrentModificationException: Nunca modifiques una colección directamente dentro de un for-each. Si necesitas eliminar elementos durante la iteración, usa Iterator.remove() o collection.removeIf(predicado) introducido en Java 8.

⚖️ Comparativa: ¿qué colección elegir?

Elegir la colección adecuada es una de las decisiones más importantes que toma un desarrollador Java. La siguiente guía de decisión te ayudará a seleccionar la estructura correcta según tu caso de uso:

Necesidad Colección recomendada Justificación
Lista de elementos con acceso por índice ArrayList O(1) en acceso, mejor localidad de caché
Inserciones/eliminaciones frecuentes en extremos LinkedList / ArrayDeque O(1) en extremos sin desplazamientos
Elementos únicos sin orden HashSet O(1) y máximo rendimiento
Elementos únicos ordenados TreeSet Mantiene orden natural o por Comparator
Diccionario clave → valor rápido HashMap O(1) para get/put, la más usada
Diccionario con claves ordenadas TreeMap Iteración en orden de claves
Cola FIFO ArrayDeque Más eficiente que LinkedList para colas
Cola con prioridad PriorityQueue Extrae siempre el menor elemento
Entorno concurrente (multihilo) ConcurrentHashMap Thread-safe sin bloqueo global

🛠️ La clase Collections: métodos de utilidad

La clase java.util.Collections (nótese la «s» final que la distingue de la interfaz Collection) proporciona métodos estáticos de utilidad para manipular colecciones: ordenar, mezclar, buscar, invertir, crear vistas inmutables y más.

Collections — Métodos de utilidad más usados
import java.util.*;

public class EjemploCollections {
    public static void main(String[] args) {
        List<Integer> numeros = new ArrayList<>(Arrays.asList(34, 12, 56, 7, 89, 23));

        // Ordenar
        Collections.sort(numeros);
        System.out.println("Ordenada: " + numeros);

        // Orden inverso
        Collections.sort(numeros, Collections.reverseOrder());
        System.out.println("Invertida: " + numeros);

        // Mezclar aleatoriamente
        Collections.shuffle(numeros);
        System.out.println("Mezclada: " + numeros);

        // Valor máximo y mínimo
        System.out.println("Máximo: " + Collections.max(numeros));
        System.out.println("Mínimo: " + Collections.min(numeros));

        // Frecuencia de un elemento
        List<String> colores = Arrays.asList("rojo", "azul", "rojo", "verde", "rojo");
        System.out.println("Frecuencia de 'rojo': " + Collections.frequency(colores, "rojo"));

        // Crear lista inmutable (Java 9+)
        List<String> inmutable = List.of("uno", "dos", "tres");
        // inmutable.add("cuatro");  // Lanzaría UnsupportedOperationException

        // Vista no modificable de una lista existente
        List<Integer> soloLectura = Collections.unmodifiableList(numeros);
        System.out.println("Solo lectura: " + soloLectura);
    }
}

🔒 Genéricos y type safety en colecciones

Los genéricos, introducidos en Java 5, permiten parametrizar las colecciones con un tipo concreto, proporcionando seguridad de tipos en tiempo de compilación. Antes de los genéricos, todas las colecciones almacenaban Object y era necesario hacer casting manual al recuperar elementos, con el riesgo de ClassCastException en tiempo de ejecución.

Sin genéricos vs. Con genéricos
// ❌ SIN genéricos (Java 1.4 y anteriores) — código inseguro
List listaRaw = new ArrayList();
listaRaw.add("texto");
listaRaw.add(42);  // Compila, pero mezcla tipos
String s = (String) listaRaw.get(1);  // ClassCastException en tiempo de ejecución

// ✅ CON genéricos (Java 5+) — el compilador previene errores
List<String> listaSegura = new ArrayList<>();
listaSegura.add("texto");
// listaSegura.add(42);  // ¡ERROR DE COMPILACIÓN! No compila.
String s2 = listaSegura.get(0);  // No necesita casting
💡 Operador diamante (<>): Desde Java 7, no es necesario repetir el tipo en el constructor. Basta con escribir new ArrayList<>() en lugar de new ArrayList<String>(). El compilador infiere el tipo automáticamente del lado izquierdo de la asignación.

Una práctica recomendada es programar contra la interfaz, no contra la implementación. Esto significa declarar variables con el tipo de la interfaz y asignar una implementación concreta:

Programar contra la interfaz
// ✅ Buena práctica: tipo interfaz a la izquierda
List<String> nombres = new ArrayList<>();
Set<Integer> ids = new HashSet<>();
Map<String, Double> precios = new HashMap<>();

// ❌ Mala práctica: tipo concreto a la izquierda (acopla el código)
ArrayList<String> nombres2 = new ArrayList<>();

📖 Ejemplo integrador: sistema de gestión de biblioteca

Este ejemplo combina múltiples colecciones en un escenario realista: un sistema de gestión de biblioteca que usa ArrayList para almacenar libros, HashMap para indexar por ISBN, TreeSet para mantener géneros ordenados y LinkedList como cola de reservas.

Sistema de Biblioteca — Uso combinado de colecciones
import java.util.*;
import java.util.stream.Collectors;

class Libro {
    private String isbn;
    private String titulo;
    private String autor;
    private String genero;
    private int anio;
    private boolean disponible;

    public Libro(String isbn, String titulo, String autor, String genero, int anio) {
        this.isbn = isbn;
        this.titulo = titulo;
        this.autor = autor;
        this.genero = genero;
        this.anio = anio;
        this.disponible = true;
    }

    // Getters
    public String getIsbn() { return isbn; }
    public String getTitulo() { return titulo; }
    public String getAutor() { return autor; }
    public String getGenero() { return genero; }
    public int getAnio() { return anio; }
    public boolean isDisponible() { return disponible; }
    public void setDisponible(boolean disponible) { this.disponible = disponible; }

    @Override
    public String toString() {
        return String.format("«%s» de %s (%d) [%s]",
            titulo, autor, anio, disponible ? "Disponible" : "Prestado");
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Libro)) return false;
        return isbn.equals(((Libro) o).isbn);
    }

    @Override
    public int hashCode() { return isbn.hashCode(); }
}

public class SistemaBiblioteca {
    // Catálogo completo (List para mantener orden de registro)
    private List<Libro> catalogo = new ArrayList<>();

    // Índice rápido por ISBN (Map para búsqueda O(1))
    private Map<String, Libro> indicePorIsbn = new HashMap<>();

    // Géneros disponibles (TreeSet para orden alfabético sin duplicados)
    private Set<String> generos = new TreeSet<>();

    // Cola de reservas (LinkedList como Queue FIFO)
    private Queue<String> colaReservas = new LinkedList<>();

    public void agregarLibro(Libro libro) {
        catalogo.add(libro);
        indicePorIsbn.put(libro.getIsbn(), libro);
        generos.add(libro.getGenero());
        System.out.println("✅ Añadido: " + libro);
    }

    public Libro buscarPorIsbn(String isbn) {
        return indicePorIsbn.get(isbn);
    }

    public List<Libro> buscarPorGenero(String genero) {
        return catalogo.stream()
            .filter(l -> l.getGenero().equalsIgnoreCase(genero))
            .collect(Collectors.toList());
    }

    public void prestarLibro(String isbn) {
        Libro libro = indicePorIsbn.get(isbn);
        if (libro != null && libro.isDisponible()) {
            libro.setDisponible(false);
            System.out.println("📕 Prestado: " + libro.getTitulo());
        } else if (libro != null) {
            colaReservas.add(isbn);
            System.out.println("⏳ En cola de reservas: " + libro.getTitulo());
        } else {
            System.out.println("❌ ISBN no encontrado: " + isbn);
        }
    }

    public void mostrarEstadisticas() {
        long disponibles = catalogo.stream().filter(Libro::isDisponible).count();
        System.out.println("\n📊 --- Estadísticas de la biblioteca ---");
        System.out.println("Total de libros: " + catalogo.size());
        System.out.println("Disponibles: " + disponibles);
        System.out.println("Prestados: " + (catalogo.size() - disponibles));
        System.out.println("Géneros: " + generos);
        System.out.println("Reservas pendientes: " + colaReservas.size());
    }

    public static void main(String[] args) {
        SistemaBiblioteca biblioteca = new SistemaBiblioteca();

        // Registrar libros
        biblioteca.agregarLibro(new Libro("978-0-13-468599-1",
            "Effective Java", "Joshua Bloch", "Programación", 2018));
        biblioteca.agregarLibro(new Libro("978-0-596-00712-6",
            "Head First Design Patterns", "Eric Freeman", "Programación", 2020));
        biblioteca.agregarLibro(new Libro("978-84-376-0494-7",
            "Don Quijote de la Mancha", "Miguel de Cervantes", "Literatura", 1605));
        biblioteca.agregarLibro(new Libro("978-0-06-112008-4",
            "To Kill a Mockingbird", "Harper Lee", "Literatura", 1960));

        // Buscar y prestar
        Libro encontrado = biblioteca.buscarPorIsbn("978-0-13-468599-1");
        System.out.println("\nBúsqueda por ISBN: " + encontrado);

        biblioteca.prestarLibro("978-0-13-468599-1");
        biblioteca.prestarLibro("978-0-13-468599-1");  // Ya prestado → cola

        // Buscar por género
        System.out.println("\nLibros de Programación:");
        biblioteca.buscarPorGenero("Programación").forEach(l ->
            System.out.println("  · " + l));

        // Estadísticas
        biblioteca.mostrarEstadisticas();
    }
}
Salida esperada
✅ Añadido: «Effective Java» de Joshua Bloch (2018) [Disponible]
✅ Añadido: «Head First Design Patterns» de Eric Freeman (2020) [Disponible]
✅ Añadido: «Don Quijote de la Mancha» de Miguel de Cervantes (1605) [Disponible]
✅ Añadido: «To Kill a Mockingbird» de Harper Lee (1960) [Disponible]

Búsqueda por ISBN: «Effective Java» de Joshua Bloch (2018) [Disponible]
📕 Prestado: Effective Java
⏳ En cola de reservas: Effective Java

Libros de Programación:
  · «Effective Java» de Joshua Bloch (2018) [Prestado]
  · «Head First Design Patterns» de Eric Freeman (2020) [Disponible]

📊 --- Estadísticas de la biblioteca ---
Total de libros: 4
Disponibles: 3
Prestados: 1
Géneros: [Literatura, Programación]
Reservas pendientes: 1

✏️ Ejercicios prácticos resueltos

🏋️ Ejercicio 1: Contador de frecuencia de palabras

Escribe un programa que reciba una frase y cuente cuántas veces aparece cada palabra, usando un HashMap. Las palabras deben convertirse a minúsculas para que la comparación sea insensible a mayúsculas.

Entrada de ejemplo: "Java es genial y Java es potente y Java es libre"

Salida esperada: {java=3, es=3, genial=1, y=2, potente=1, libre=1}

Ver solución
ContadorPalabras.java
import java.util.HashMap;
import java.util.Map;

public class ContadorPalabras {
    public static void main(String[] args) {
        String frase = "Java es genial y Java es potente y Java es libre";
        String[] palabras = frase.toLowerCase().split("\\s+");

        Map<String, Integer> frecuencia = new HashMap<>();
        for (String palabra : palabras) {
            frecuencia.merge(palabra, 1, Integer::sum);
        }

        System.out.println(frecuencia);

        // Alternativa con Streams
        // Map<String, Long> freq = Arrays.stream(palabras)
        //     .collect(Collectors.groupingBy(p -> p, Collectors.counting()));
    }
}

🏋️ Ejercicio 2: Eliminar duplicados preservando orden

Dada una lista de números enteros con duplicados, escribe un programa que devuelva una nueva lista sin duplicados pero conservando el orden original de aparición. Usa la colección más apropiada.

Entrada: [4, 2, 7, 2, 4, 9, 7, 1, 4]

Salida esperada: [4, 2, 7, 9, 1]

Ver solución
EliminarDuplicados.java
import java.util.*;

public class EliminarDuplicados {
    public static void main(String[] args) {
        List<Integer> original = Arrays.asList(4, 2, 7, 2, 4, 9, 7, 1, 4);

        // LinkedHashSet: elimina duplicados y preserva orden de inserción
        Set<Integer> sinDuplicados = new LinkedHashSet<>(original);
        List<Integer> resultado = new ArrayList<>(sinDuplicados);

        System.out.println("Original: " + original);
        System.out.println("Sin duplicados: " + resultado);
    }
}

🏋️ Ejercicio 3: Agenda de contactos con TreeMap

Implementa una agenda de contactos que almacene nombres (clave) y números de teléfono (valor), mantenga los contactos ordenados alfabéticamente y permita buscar por prefijo (por ejemplo, todos los contactos cuyo nombre empiece por «M»).

Ver solución
AgendaContactos.java
import java.util.TreeMap;
import java.util.Map;

public class AgendaContactos {
    public static void main(String[] args) {
        TreeMap<String, String> agenda = new TreeMap<>();

        agenda.put("Ana García", "+34 612 345 678");
        agenda.put("Miguel López", "+34 698 765 432");
        agenda.put("María Fernández", "+34 655 111 222");
        agenda.put("Carlos Ruiz", "+34 677 333 444");
        agenda.put("Marta Sánchez", "+34 633 555 666");

        // Mostrar agenda ordenada
        System.out.println("--- Agenda completa ---");
        agenda.forEach((nombre, tel) ->
            System.out.printf("%-20s %s%n", nombre, tel));

        // Buscar por prefijo "M" (desde "M" hasta "N" exclusivo)
        System.out.println("\n--- Contactos con M ---");
        Map<String, String> conM = agenda.subMap("M", "N");
        conM.forEach((nombre, tel) ->
            System.out.printf("%-20s %s%n", nombre, tel));
    }
}

🏋️ Ejercicio 4: Sistema de votación con colecciones

Simula un sistema de votación donde se registran votos para diferentes candidatos. Usa un HashMap para contar los votos y un TreeMap para mostrar los resultados ordenados por nombre. Finalmente, determina el ganador usando Streams.

Ver solución
SistemaVotacion.java
import java.util.*;

public class SistemaVotacion {
    public static void main(String[] args) {
        String[] votos = {"Ana", "Carlos", "Ana", "Elena",
                          "Carlos", "Ana", "Elena", "Carlos",
                          "Ana", "Elena", "Elena", "Elena"};

        // Contar votos con HashMap
        Map<String, Integer> conteo = new HashMap<>();
        for (String voto : votos) {
            conteo.merge(voto, 1, Integer::sum);
        }

        // Mostrar resultados ordenados por nombre (TreeMap)
        TreeMap<String, Integer> resultados = new TreeMap<>(conteo);
        System.out.println("--- Resultados ---");
        resultados.forEach((candidato, numVotos) ->
            System.out.printf("%-10s → %d votos%n", candidato, numVotos));

        // Determinar ganador con Streams
        Map.Entry<String, Integer> ganador = conteo.entrySet().stream()
            .max(Map.Entry.comparingByValue())
            .orElseThrow();

        System.out.printf("%n🏆 Ganador: %s con %d votos%n",
            ganador.getKey(), ganador.getValue());
    }
}

⚠️ Errores frecuentes con colecciones en Java

Incluso los programadores experimentados cometen errores al trabajar con colecciones. Conocerlos te ahorrará horas de depuración y bugs difíciles de rastrear.

❌ Error 1: Modificar una colección durante un for-each

ConcurrentModificationException
// ❌ INCORRECTO: modificar la lista dentro de un for-each
List<String> nombres = new ArrayList<>(Arrays.asList("Ana", "Bob", "Carlos"));
for (String nombre : nombres) {
    if (nombre.startsWith("B")) {
        nombres.remove(nombre);  // ¡ConcurrentModificationException!
    }
}

// ✅ CORRECTO: usar removeIf (Java 8+)
nombres.removeIf(nombre -> nombre.startsWith("B"));

// ✅ CORRECTO: usar Iterator.remove()
Iterator<String> it = nombres.iterator();
while (it.hasNext()) {
    if (it.next().startsWith("B")) {
        it.remove();
    }
}

❌ Error 2: No implementar hashCode() y equals() para objetos en HashSet/HashMap

Objetos duplicados en HashSet
// ❌ Clase sin hashCode/equals: HashSet no detecta duplicados
class Producto {
    String codigo;
    Producto(String codigo) { this.codigo = codigo; }
}
Set<Producto> set = new HashSet<>();
set.add(new Producto("A001"));
set.add(new Producto("A001"));
System.out.println(set.size());  // Imprime 2, ¡debería ser 1!

// ✅ Solución: implementar equals() y hashCode()
class ProductoCorregido {
    String codigo;
    ProductoCorregido(String codigo) { this.codigo = codigo; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof ProductoCorregido)) return false;
        return codigo.equals(((ProductoCorregido) o).codigo);
    }

    @Override
    public int hashCode() { return codigo.hashCode(); }
}

❌ Error 3: Usar el tipo concreto en lugar de la interfaz

Acoplamiento innecesario
// ❌ INCORRECTO: declarar con el tipo concreto acopla el código
ArrayList<String> lista = new ArrayList<>();
HashMap<String, Integer> mapa = new HashMap<>();

// ✅ CORRECTO: programar contra la interfaz permite cambiar la implementación
List<String> lista = new ArrayList<>();       // Fácil cambiar a LinkedList
Map<String, Integer> mapa = new HashMap<>();  // Fácil cambiar a TreeMap

❌ Error 4: Confundir Collections.sort() con elementos no comparables

ClassCastException al ordenar
// ❌ INCORRECTO: intentar ordenar objetos que no implementan Comparable
class Empleado {
    String nombre;
    int edad;
    Empleado(String nombre, int edad) { this.nombre = nombre; this.edad = edad; }
}
List<Empleado> empleados = new ArrayList<>();
empleados.add(new Empleado("Ana", 30));
empleados.add(new Empleado("Carlos", 25));
Collections.sort(empleados);  // ¡ClassCastException! Empleado no es Comparable

// ✅ CORRECTO: proporcionar un Comparator
Collections.sort(empleados, (e1, e2) -> e1.nombre.compareTo(e2.nombre));

// ✅ O bien: implementar Comparable en la clase
class EmpleadoComparable implements Comparable<EmpleadoComparable> {
    String nombre;
    int edad;
    EmpleadoComparable(String nombre, int edad) { this.nombre = nombre; this.edad = edad; }

    @Override
    public int compareTo(EmpleadoComparable otro) {
        return this.nombre.compareTo(otro.nombre);
    }
}
✅ Regla de oro: Antes de usar una clase propia como clave de un HashMap/HashSet, implementa siempre hashCode() y equals(). Y antes de ordenarla, asegúrate de que implementa Comparable o proporciona un Comparator.

📌 Artículos relacionados

❓ Preguntas frecuentes sobre Colecciones en Java: ArrayList, HashMap, Set y más

Las dudas más comunes respondidas de forma clara y directa.

ArrayList utiliza internamente un array dinámico redimensionable, lo que ofrece acceso aleatorio en tiempo constante O(1) pero inserciones y eliminaciones en posiciones intermedias en O(n). LinkedList usa una lista doblemente enlazada, con inserciones y eliminaciones en O(1) si se tiene el iterador posicionado, pero acceso por índice en O(n). En la mayoría de casos prácticos, ArrayList es más eficiente por la localidad de caché.
Usa HashMap cuando necesites máximo rendimiento en operaciones de inserción, búsqueda y eliminación (O(1) amortizado) y no necesites que las claves estén ordenadas. Usa TreeMap cuando necesites que las claves se mantengan ordenadas de forma natural o mediante un Comparator personalizado, aceptando un coste de O(log n) por operación.
List permite elementos duplicados y mantiene el orden de inserción, permitiendo acceso por índice posicional. Set no permite duplicados y, salvo LinkedHashSet, no garantiza un orden concreto. Se elige List cuando importa el orden y se pueden repetir elementos, y Set cuando se necesita garantizar unicidad.
Las colecciones basadas en hash (HashMap, HashSet) usan hashCode() para calcular la posición del elemento en la tabla interna, y equals() para resolver colisiones y verificar igualdad. Si no se implementan correctamente, dos objetos lógicamente iguales podrían tratarse como distintos, provocando duplicados en un HashSet o claves perdidas en un HashMap.
Las implementaciones estándar (ArrayList, HashMap, HashSet) no son thread-safe. Para entornos concurrentes se pueden usar las clases del paquete java.util.concurrent como ConcurrentHashMap o CopyOnWriteArrayList, o envolver colecciones existentes con Collections.synchronizedList(), Collections.synchronizedMap(), etc.
Los Streams, introducidos en Java 8, permiten realizar operaciones funcionales (filtrar, mapear, reducir) sobre los elementos de una colección de forma declarativa. No modifican la colección original, sino que generan un nuevo resultado. Se obtienen con el método stream() disponible en cualquier colección que implemente Collection.
No directamente. Las colecciones solo aceptan objetos, no tipos primitivos. Sin embargo, Java realiza autoboxing automático, convirtiendo int en Integer, double en Double, etc. Esto permite escribir código como list.add(42) sin necesidad de hacer el boxing manualmente, aunque hay un pequeño coste de rendimiento.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Colecciones en Java: ArrayList, HashMap, Set y más? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!