Colecciones en Java: List, Set y Map explicados fácil

D
DanisCh
• 14 min de lectura
Colecciones en Java: List, Set y Map explicados fácil
Java

Cuando empiezas a programar en Java, los arrays son lo primero que aprendes para guardar varios valores. Funcionan bien para casos simples, pero tienen una limitación importante: su tamaño es fijo. Una vez que creas un array de diez elementos, no puedes agregar un undécimo sin crear uno nuevo.

Para resolver esto, Java ofrece el Collections Framework: un conjunto de clases e interfaces diseñadas para almacenar, organizar y manipular grupos de objetos de forma flexible y eficiente. Las tres más importantes, las que usarás en prácticamente cualquier proyecto, son List, Set y Map.

En este artículo aprenderás qué es cada una, cuándo usarla y cómo trabajar con ella en código real.

El Collections Framework de Java

El Collections Framework es una arquitectura unificada para representar y manipular colecciones. Está organizado en interfaces que definen el comportamiento, y clases que implementan ese comportamiento de distintas formas según las necesidades de rendimiento.

La jerarquía principal se divide en dos grandes ramas:

  • Collection: agrupa elementos individuales. De aquí heredan List, Set y Queue.
  • Map: agrupa pares clave-valor. Es una rama separada porque su modelo de datos es diferente.

Todas las colecciones de Java trabajan con genéricos (los tipos entre los signos <>), lo que permite especificar qué tipo de objetos almacena cada colección y obtener seguridad de tipos en tiempo de compilación.

// Sin genéricos (forma antigua, no recomendada)
List lista = new ArrayList();
lista.add("texto");
lista.add(123); // Java no avisa del error

// Con genéricos (forma correcta)
List<String> lista = new ArrayList<>();
lista.add("texto");
lista.add(123); // Error de compilación: correcto, nos avisa

List: una colección ordenada que permite duplicados

Una List es una colección ordenada de elementos donde cada uno tiene una posición (índice) y se permiten valores duplicados. Es la colección más parecida a un array, pero con tamaño dinámico.

Sus características principales:

  • Mantiene el orden de inserción.
  • Permite elementos duplicados.
  • Permite elementos null.
  • Acceso a elementos por índice (posición).

ArrayList: la implementación más usada

ArrayList almacena los elementos en un array interno que crece automáticamente cuando es necesario. Es la implementación de List que usarás en la mayoría de los casos porque ofrece acceso muy rápido a los elementos por índice.

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

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

    // Crear una lista de strings
    List<String> lenguajes = new ArrayList<>();

    // Agregar elementos
    lenguajes.add("Python");
    lenguajes.add("JavaScript");
    lenguajes.add("Java");
    lenguajes.add("Python"); // duplicado permitido

    System.out.println(lenguajes);
    // [Python, JavaScript, Java, Python]

    // Acceder por índice
    System.out.println(lenguajes.get(0)); // Python
    System.out.println(lenguajes.get(2)); // Java

    // Tamaño de la lista
    System.out.println(lenguajes.size()); // 4

    // Verificar si contiene un elemento
    System.out.println(lenguajes.contains("Java")); // true

    // Obtener la posición de un elemento
    System.out.println(lenguajes.indexOf("Python")); // 0 (primera aparición)

    // Modificar un elemento
    lenguajes.set(1, "TypeScript");
    System.out.println(lenguajes);
    // [Python, TypeScript, Java, Python]

    // Eliminar por índice
    lenguajes.remove(3);
    System.out.println(lenguajes);
    // [Python, TypeScript, Java]

    // Eliminar por valor
    lenguajes.remove("Python");
    System.out.println(lenguajes);
    // [TypeScript, Java]

    // Recorrer con for-each
    for (String lenguaje : lenguajes) {
      System.out.println(lenguaje);
    }
  }
}

LinkedList: cuando necesitas insertar y eliminar rápido

LinkedList almacena los elementos como nodos enlazados donde cada nodo apunta al siguiente. Insertar o eliminar elementos en el medio de la lista es muy eficiente, pero acceder por índice es más lento que en ArrayList porque debe recorrer los nodos uno a uno.

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

List<String> tareas = new LinkedList<>();
tareas.add("Diseñar la base de datos");
tareas.add("Crear los endpoints");
tareas.add("Escribir los tests");

// Insertar en una posición específica
tareas.add(1, "Configurar el proyecto");
System.out.println(tareas);
// [Diseñar la base de datos, Configurar el proyecto, Crear los endpoints, Escribir los tests]

¿Cuándo usar ArrayList y cuándo LinkedList?

  • Usa ArrayList cuando accedes frecuentemente a elementos por índice o simplemente necesitas una lista dinámica.
  • Usa LinkedList cuando insertas o eliminas elementos con frecuencia en el medio de la lista.
  • En la práctica, ArrayList es la elección correcta en la gran mayoría de los casos.

Métodos útiles de List

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

List<Integer> numeros = new ArrayList<>();
numeros.add(5);
numeros.add(2);
numeros.add(8);
numeros.add(1);
numeros.add(9);

// Ordenar
Collections.sort(numeros);
System.out.println(numeros); // [1, 2, 5, 8, 9]

// Ordenar en reversa
Collections.sort(numeros, Collections.reverseOrder());
System.out.println(numeros); // [9, 8, 5, 2, 1]

// Valor mínimo y máximo
System.out.println(Collections.min(numeros)); // 1
System.out.println(Collections.max(numeros)); // 9

// Sublista
List<Integer> sub = numeros.subList(1, 4);
System.out.println(sub); // [8, 5, 2]

// Convertir a array
Integer[] array = numeros.toArray(new Integer[0]);

// Limpiar la lista
numeros.clear();
System.out.println(numeros.isEmpty()); // true

Set: una colección sin duplicados

Un Set es una colección que no permite elementos duplicados. Si intentas agregar un valor que ya existe, la operación simplemente se ignora sin lanzar ningún error. Es la colección perfecta cuando necesitas garantizar unicidad.

Sus características principales:

  • No permite elementos duplicados.
  • No garantiza un orden específico (depende de la implementación).
  • No tiene acceso por índice.
  • Es ideal para verificar existencia de elementos con gran eficiencia.

HashSet: la implementación más rápida

HashSet usa una tabla hash internamente, lo que hace que agregar, eliminar y buscar elementos sean operaciones muy rápidas (tiempo constante en el mejor caso). No garantiza ningún orden particular de los elementos.

import java.util.HashSet;
import java.util.Set;

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

    Set<String> paises = new HashSet<>();

    // Agregar elementos
    paises.add("México");
    paises.add("Argentina");
    paises.add("Colombia");
    paises.add("México"); // duplicado: se ignora silenciosamente
    paises.add("España");

    System.out.println(paises);
    // [Colombia, España, México, Argentina] (orden no garantizado)

    System.out.println(paises.size()); // 4, no 5

    // Verificar existencia
    System.out.println(paises.contains("Colombia")); // true
    System.out.println(paises.contains("Chile"));    // false

    // Eliminar
    paises.remove("España");
    System.out.println(paises.size()); // 3

    // Recorrer
    for (String pais : paises) {
      System.out.println(pais);
    }
  }
}

LinkedHashSet: sin duplicados y con orden de inserción

LinkedHashSet es como HashSet pero mantiene el orden en que se insertaron los elementos.

import java.util.LinkedHashSet;
import java.util.Set;

Set<String> lenguajes = new LinkedHashSet<>();
lenguajes.add("Python");
lenguajes.add("Java");
lenguajes.add("JavaScript");
lenguajes.add("Python"); // duplicado ignorado

System.out.println(lenguajes);
// [Python, Java, JavaScript] (orden de inserción conservado)

TreeSet: sin duplicados y ordenado automáticamente

TreeSet mantiene los elementos ordenados de forma natural (alfabéticamente para strings, numéricamente para números) o con un comparador personalizado.

import java.util.Set;
import java.util.TreeSet;

Set<Integer> numeros = new TreeSet<>();
numeros.add(5);
numeros.add(2);
numeros.add(8);
numeros.add(1);
numeros.add(2); // duplicado ignorado

System.out.println(numeros); // [1, 2, 5, 8] (ordenado automáticamente)

Set<String> nombres = new TreeSet<>();
nombres.add("Carlos");
nombres.add("Ana");
nombres.add("Beatriz");

System.out.println(nombres); // [Ana, Beatriz, Carlos]

Operaciones entre conjuntos

Una de las ventajas de Set es que permite realizar operaciones matemáticas de conjuntos de forma muy sencilla:

import java.util.HashSet;
import java.util.Set;

Set<String> a = new HashSet<>();
a.add("Python");
a.add("Java");
a.add("JavaScript");

Set<String> b = new HashSet<>();
b.add("Java");
b.add("Kotlin");
b.add("JavaScript");

// Unión: todos los elementos de ambos conjuntos
Set<String> union = new HashSet<>(a);
union.addAll(b);
System.out.println("Unión: " + union);
// [Python, Java, JavaScript, Kotlin]

// Intersección: solo los elementos que están en ambos
Set<String> interseccion = new HashSet<>(a);
interseccion.retainAll(b);
System.out.println("Intersección: " + interseccion);
// [Java, JavaScript]

// Diferencia: los de A que no están en B
Set<String> diferencia = new HashSet<>(a);
diferencia.removeAll(b);
System.out.println("Diferencia: " + diferencia);
// [Python]

¿Cuándo usar cada implementación de Set?

  • Usa HashSet cuando solo necesitas garantizar unicidad y no te importa el orden.
  • Usa LinkedHashSet cuando necesitas unicidad y quieres conservar el orden de inserción.
  • Usa TreeSet cuando necesitas unicidad y quieres que los elementos estén siempre ordenados.

Map: pares clave-valor

Un Map almacena pares de clave y valor. Cada clave es única dentro del mapa, y cada clave apunta a exactamente un valor. Es similar a un diccionario en otros lenguajes como Python.

Sus características principales:

  • Cada clave es única. Si agregas una clave que ya existe, el valor anterior se reemplaza.
  • Los valores sí pueden repetirse.
  • No hereda de Collection; es una jerarquía separada.
  • Ideal para búsquedas rápidas por clave.

HashMap: la implementación más usada

HashMap usa una tabla hash para almacenar las entradas, lo que hace que buscar un valor por su clave sea extremadamente rápido. No garantiza ningún orden particular.

import java.util.HashMap;
import java.util.Map;

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

    Map<String, Integer> edades = new HashMap<>();

    // Agregar pares clave-valor
    edades.put("Ana", 25);
    edades.put("Luis", 30);
    edades.put("María", 22);
    edades.put("Carlos", 35);

    // Actualizar un valor existente
    edades.put("Ana", 26); // reemplaza el 25

    System.out.println(edades);
    // {Luis=30, Ana=26, Carlos=35, María=22} (orden no garantizado)

    // Obtener un valor por clave
    System.out.println(edades.get("Luis")); // 30
    System.out.println(edades.get("Pedro")); // null (clave inexistente)

    // Obtener con valor por defecto si la clave no existe
    System.out.println(edades.getOrDefault("Pedro", 0)); // 0

    // Verificar si existe una clave o un valor
    System.out.println(edades.containsKey("María"));   // true
    System.out.println(edades.containsValue(99));       // false

    // Eliminar por clave
    edades.remove("Carlos");

    // Tamaño
    System.out.println(edades.size()); // 3
  }
}

Recorrer un Map

Hay varias formas de recorrer las entradas de un Map:

Map<String, Integer> edades = new HashMap<>();
edades.put("Ana", 25);
edades.put("Luis", 30);
edades.put("María", 22);

// Recorrer con entrySet (la forma más común)
for (Map.Entry<String, Integer> entrada : edades.entrySet()) {
  System.out.println(entrada.getKey() + " tiene " + entrada.getValue() + " años");
}

// Recorrer solo las claves
for (String nombre : edades.keySet()) {
  System.out.println(nombre);
}

// Recorrer solo los valores
for (int edad : edades.values()) {
  System.out.println(edad);
}

// Con forEach y expresión lambda (Java 8+)
edades.forEach((nombre, edad) -> {
  System.out.println(nombre + ": " + edad);
});

LinkedHashMap: pares clave-valor con orden de inserción

import java.util.LinkedHashMap;
import java.util.Map;

Map<String, String> capitales = new LinkedHashMap<>();
capitales.put("Colombia", "Bogotá");
capitales.put("Argentina", "Buenos Aires");
capitales.put("México", "Ciudad de México");
capitales.put("Perú", "Lima");

// El orden de inserción se conserva
for (Map.Entry<String, String> entrada : capitales.entrySet()) {
  System.out.println(entrada.getKey() + " → " + entrada.getValue());
}
// Colombia → Bogotá
// Argentina → Buenos Aires
// México → Ciudad de México
// Perú → Lima

TreeMap: pares clave-valor ordenados por clave

import java.util.Map;
import java.util.TreeMap;

Map<String, Double> precios = new TreeMap<>();
precios.put("Zapatos", 89.99);
precios.put("Camisa", 34.50);
precios.put("Pantalón", 59.00);
precios.put("Sombrero", 22.00);

// Las claves están ordenadas alfabéticamente
for (Map.Entry<String, Double> entrada : precios.entrySet()) {
  System.out.println(entrada.getKey() + ": $" + entrada.getValue());
}
// Camisa: $34.5
// Pantalón: $59.0
// Sombrero: $22.0
// Zapatos: $89.99

Métodos útiles de Map en Java 8+

Map<String, Integer> inventario = new HashMap<>();
inventario.put("manzanas", 10);
inventario.put("naranjas", 5);

// putIfAbsent: solo agrega si la clave no existe
inventario.putIfAbsent("manzanas", 99); // ignorado, ya existe
inventario.putIfAbsent("peras", 20);    // se agrega
System.out.println(inventario.get("manzanas")); // 10
System.out.println(inventario.get("peras"));    // 20

// compute: actualiza el valor aplicando una función
inventario.compute("manzanas", (clave, valorActual) -> valorActual + 5);
System.out.println(inventario.get("manzanas")); // 15

// merge: combina un valor nuevo con el existente
inventario.merge("naranjas", 10, Integer::sum);
System.out.println(inventario.get("naranjas")); // 15

// computeIfAbsent: calcula y agrega solo si la clave no existe
inventario.computeIfAbsent("uvas", k -> 30);
System.out.println(inventario.get("uvas")); // 30

Ejemplo práctico: contar frecuencia de palabras

Un caso de uso clásico que combina todo lo aprendido: contar cuántas veces aparece cada palabra en un texto.

import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

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

    String texto = "java es un lenguaje java es popular java es orientado a objetos";
    String[] palabras = texto.split(" ");

    Map<String, Integer> frecuencia = new HashMap<>();

    for (String palabra : palabras) {
      frecuencia.merge(palabra, 1, Integer::sum);
    }

    // Mostrar ordenado por palabra con TreeMap
    Map<String, Integer> ordenado = new TreeMap<>(frecuencia);

    for (Map.Entry<String, Integer> entrada : ordenado.entrySet()) {
      System.out.println(entrada.getKey() + ": " + entrada.getValue());
    }
  }
}

// a: 1
// es: 3
// java: 3
// lenguaje: 1
// objetos: 1
// orientado: 1
// popular: 1
// un: 1

Ejemplo práctico: eliminar duplicados de una lista

Otro caso muy común: tienes una lista con duplicados y necesitas eliminarlos conservando el orden de inserción.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

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

    List<String> conDuplicados = new ArrayList<>(
      Arrays.asList("Python", "Java", "Python", "JavaScript", "Java", "Kotlin")
    );

    System.out.println("Con duplicados: " + conDuplicados);
    // [Python, Java, Python, JavaScript, Java, Kotlin]

    // Pasar por LinkedHashSet para eliminar duplicados conservando el orden
    Set<String> sinDuplicados = new LinkedHashSet<>(conDuplicados);
    List<String> resultado = new ArrayList<>(sinDuplicados);

    System.out.println("Sin duplicados: " + resultado);
    // [Python, Java, JavaScript, Kotlin]
  }
}

Tabla resumen: cuándo usar cada colección

ColecciónDuplicadosOrdenAcceso por índiceCuándo usarla
ArrayListSíInserciónSíLista general, acceso frecuente por posición
LinkedListSíInserciónSí (lento)Muchas inserciones y eliminaciones en el medio
HashSetNoNingunoNoGarantizar unicidad, verificar existencia rápido
LinkedHashSetNoInserciónNoUnicidad conservando el orden de inserción
TreeSetNoNatural/personalizadoNoUnicidad con elementos siempre ordenados
HashMapNo en clavesNingunoPor claveBúsqueda rápida por clave, uso general
LinkedHashMapNo en clavesInserciónPor claveClave-valor conservando el orden de inserción
TreeMapNo en clavesNatural/personalizadoPor claveClave-valor con claves siempre ordenadas

Buenas prácticas al trabajar con colecciones

Declara siempre con la interfaz, no con la clase concreta

// ❌ Menos flexible
ArrayList<String> lista = new ArrayList<>();

// ✅ Más flexible: puedes cambiar la implementación sin tocar el resto del código
List<String> lista = new ArrayList<>();

Usa el tipo más específico que necesites

Si sabes que no necesitas duplicados, usa Set desde el principio en lugar de usar List y eliminar duplicados después. Si sabes que necesitas buscar por clave, usa Map directamente.

Inicializa con capacidad si conoces el tamaño aproximado

// Si sabes que vas a guardar alrededor de 1000 elementos,
// inicializar con esa capacidad evita redimensionamientos internos
List<String> lista = new ArrayList<>(1000);
Map<String, Integer> mapa = new HashMap<>(1000);

Usa Collections.unmodifiableList() para colecciones de solo lectura

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

List<String> original = new ArrayList<>();
original.add("Java");
original.add("Kotlin");

List<String> soloLectura = Collections.unmodifiableList(original);
soloLectura.add("Python"); // lanza UnsupportedOperationException

Conclusión

Las colecciones de Java son una herramienta fundamental que usarás en absolutamente cualquier proyecto. Entender cuándo usar List, Set o Map, y qué implementación elegir en cada caso, marca una diferencia real en la calidad y eficiencia de tu código.

Para resumir: usa List cuando necesitas una secuencia ordenada que puede tener duplicados, usa Set cuando necesitas garantizar que no haya elementos repetidos, y usa Map cuando necesitas asociar claves con valores y buscar por esas claves rápidamente.

Para seguir aprendiendo Java, te recomendamos los artículos sobre qué es la programación orientada a objetos y sus 4 pilares y sobre cómo crear tu primer programa en Java paso a paso, donde encontrarás la base que necesitas para sacarle el máximo partido a lo que aprendiste aquí.

Etiquetas: Colecciones en Java

¿Te ha gustado esta entrada?

Compártela con tus compañeros para que también sigan aprendiendo.

Comunidad y Comentarios

0 COMENTARIOS

No hay comentarios todavía. Sé el primero en compartir tu opinión.

Escribe tu opinión
Respondiendo a