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 avisaList: 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()); // trueSet: 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ú → LimaTreeMap: 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.99Mé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")); // 30Ejemplo 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: 1Ejemplo 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ón | Duplicados | Orden | Acceso por índice | Cuándo usarla |
|---|---|---|---|---|
| ArrayList | Sí | Inserción | Sí | Lista general, acceso frecuente por posición |
| LinkedList | Sí | Inserción | Sí (lento) | Muchas inserciones y eliminaciones en el medio |
| HashSet | No | Ninguno | No | Garantizar unicidad, verificar existencia rápido |
| LinkedHashSet | No | Inserción | No | Unicidad conservando el orden de inserción |
| TreeSet | No | Natural/personalizado | No | Unicidad con elementos siempre ordenados |
| HashMap | No en claves | Ninguno | Por clave | Búsqueda rápida por clave, uso general |
| LinkedHashMap | No en claves | Inserción | Por clave | Clave-valor conservando el orden de inserción |
| TreeMap | No en claves | Natural/personalizado | Por clave | Clave-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 UnsupportedOperationExceptionConclusió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í.
No hay comentarios todavía. Sé el primero en compartir tu opinión.