Volver a Labs
Intermedio

Colecciones y Streams en Java

2–3 horas Actualizado Junio 2026
Java Collections Streams Backend

Introducción

Los arrays en Java tienen un tamaño fijo: una vez creados, no puedes agregar ni quitar elementos. Para la mayoría de aplicaciones reales necesitas estructuras más flexibles. Aquí entran las colecciones.

El Java Collections Framework es un conjunto de clases e interfaces que resuelven el problema de agrupar objetos. Con él puedes crear listas que crecen dinámicamente, diccionarios clave-valor, conjuntos sin duplicados y más — todo con una API coherente.

¿Qué vas a aprender en este lab?

  • Usar ArrayList para listas dinámicas
  • Usar HashMap para almacenar pares clave-valor
  • Usar HashSet para colecciones sin duplicados
  • Iterar colecciones con for-each e iteradores
  • Transformar y filtrar datos con la API de Streams

Requisitos previos: fundamentos de Java (variables, bucles, métodos) y nociones básicas de POO (clases, objetos).


1. ArrayList — listas dinámicas

ArrayList implementa una lista que cambia de tamaño automáticamente. Es el reemplazo directo de los arrays cuando no conoces el tamaño de antemano.

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

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

// Agregar elementos
frutas.add("manzana");
frutas.add("naranja");
frutas.add("pera");

// Acceder por índice
System.out.println(frutas.get(0)); // manzana

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

// Eliminar
frutas.remove("naranja");
System.out.println(frutas); // [manzana, pera]

// Verificar si existe
System.out.println(frutas.contains("pera")); // true

Iterar un ArrayList

// for-each (la forma más común)
for (String fruta : frutas) {
    System.out.println(fruta);
}

// for clásico con índice (cuando necesitas la posición)
for (int i = 0; i < frutas.size(); i++) {
    System.out.println(i + ": " + frutas.get(i));
}

Operaciones útiles

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

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

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

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

Ejercicio 1

Crea una lista de nombres de estudiantes. Escribe métodos para:

  • Agregar un estudiante si no está ya en la lista
  • Eliminar un estudiante por nombre
  • Imprimir la lista ordenada alfabéticamente
  • Buscar si un nombre contiene cierta cadena (pista: String.contains())

2. HashMap — diccionarios clave-valor

HashMap almacena pares clave → valor. Cada clave es única; si insertas la misma clave dos veces, el segundo valor reemplaza al primero. El acceso es casi instantáneo sin importar cuántos elementos haya.

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

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

// Insertar
edades.put("Julian", 25);
edades.put("Ana",    30);
edades.put("Carlos", 22);

// Leer
System.out.println(edades.get("Ana"));      // 30
System.out.println(edades.get("Unknown"));  // null

// Leer con valor por defecto (evita NullPointerException)
System.out.println(edades.getOrDefault("Unknown", -1)); // -1

// Verificar existencia
System.out.println(edades.containsKey("Julian")); // true
System.out.println(edades.containsValue(22));     // true

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

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

Iterar un HashMap

// Iterar entradas (clave + valor)
for (Map.Entry<String, Integer> entry : edades.entrySet()) {
    System.out.println(entry.getKey() + " tiene " + entry.getValue() + " años");
}

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

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

Caso de uso: contador de palabras

String texto = "java es genial java es potente java";
String[] palabras = texto.split(" ");

Map<String, Integer> contador = new HashMap<>();
for (String palabra : palabras) {
    contador.put(palabra, contador.getOrDefault(palabra, 0) + 1);
}

System.out.println(contador);
// {es=2, genial=1, java=3, potente=1}

Ejercicio 2

Crea un sistema de notas de estudiantes usando HashMap<String, List<Double>> donde la clave es el nombre y el valor es la lista de sus notas. Implementa métodos para:

  • Agregar una nota a un estudiante (creando su lista si no existe)
  • Calcular el promedio de un estudiante
  • Obtener el estudiante con el mejor promedio

3. HashSet — conjuntos sin duplicados

HashSet almacena elementos únicos sin un orden definido. Es ideal cuando necesitas saber si algo existe, pero no te importa en qué posición está.

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

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

paises.add("Colombia");
paises.add("México");
paises.add("Colombia"); // duplicado — se ignora silenciosamente

System.out.println(paises.size());             // 2
System.out.println(paises.contains("México")); // true

Operaciones de conjuntos

Set<Integer> setA = new HashSet<>(Set.of(1, 2, 3, 4));
Set<Integer> setB = new HashSet<>(Set.of(3, 4, 5, 6));

// Intersección (elementos en A y en B)
Set<Integer> interseccion = new HashSet<>(setA);
interseccion.retainAll(setB);
System.out.println(interseccion); // [3, 4]

// Unión (elementos en A o en B)
Set<Integer> union = new HashSet<>(setA);
union.addAll(setB);
System.out.println(union); // [1, 2, 3, 4, 5, 6]

// Diferencia (en A pero no en B)
Set<Integer> diferencia = new HashSet<>(setA);
diferencia.removeAll(setB);
System.out.println(diferencia); // [1, 2]

Eliminar duplicados de una lista

List<String> conDuplicados = List.of("a", "b", "a", "c", "b", "d");
Set<String> sinDuplicados  = new HashSet<>(conDuplicados);
System.out.println(sinDuplicados); // [a, b, c, d] (orden no garantizado)

4. Cuándo usar cada colección

Colección Úsala cuando...
ArrayList Necesitas acceso por índice o el orden importa
LinkedList Insertas/eliminas mucho en el medio de la lista
HashMap Necesitas asociar claves con valores y acceso rápido
TreeMap Igual que HashMap pero necesitas que las claves estén ordenadas
HashSet Solo necesitas saber si algo existe, sin duplicados
TreeSet Igual que HashSet pero ordenado

5. API de Streams

La API de Streams (Java 8+) permite procesar colecciones de forma declarativa: dices qué quieres hacer, no cómo hacerlo. El resultado es código más legible y conciso.

Un Stream es una secuencia de elementos sobre la que aplicas operaciones encadenadas. No modifica la colección original.

Operaciones intermedias (transforman el stream)

List<Integer> numeros = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// filter — conserva solo los que cumplen la condición
List<Integer> pares = numeros.stream()
    .filter(n -> n % 2 == 0)
    .toList();
System.out.println(pares); // [2, 4, 6, 8, 10]

// map — transforma cada elemento
List<Integer> cuadrados = numeros.stream()
    .map(n -> n * n)
    .toList();
System.out.println(cuadrados); // [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

// sorted — ordena
List<Integer> ordenados = List.of(5, 2, 8, 1).stream()
    .sorted()
    .toList();
System.out.println(ordenados); // [1, 2, 5, 8]

// limit / skip — paginar resultados
List<Integer> pagina = numeros.stream()
    .skip(3)   // salta los primeros 3
    .limit(4)  // toma los siguientes 4
    .toList();
System.out.println(pagina); // [4, 5, 6, 7]

// distinct — elimina duplicados
List<Integer> sinRep = List.of(1, 2, 2, 3, 3, 3).stream()
    .distinct()
    .toList();
System.out.println(sinRep); // [1, 2, 3]

Operaciones terminales (producen un resultado)

List<Integer> numeros = List.of(1, 2, 3, 4, 5);

// count
long cantidad = numeros.stream().filter(n -> n > 3).count();
System.out.println(cantidad); // 2

// sum, min, max
int suma = numeros.stream().mapToInt(Integer::intValue).sum();
System.out.println(suma); // 15

int maximo = numeros.stream().mapToInt(Integer::intValue).max().getAsInt();
System.out.println(maximo); // 5

// findFirst
Optional<Integer> primero = numeros.stream().filter(n -> n > 3).findFirst();
primero.ifPresent(n -> System.out.println("Primero mayor a 3: " + n)); // 4

// anyMatch / allMatch / noneMatch
boolean hayPares    = numeros.stream().anyMatch(n -> n % 2 == 0);  // true
boolean todosPares  = numeros.stream().allMatch(n -> n % 2 == 0);  // false
boolean ningunNeg   = numeros.stream().noneMatch(n -> n < 0);      // true

Streams con objetos

record Producto(String nombre, double precio, String categoria) {}

List<Producto> productos = List.of(
    new Producto("Laptop",   1200.0, "Electrónica"),
    new Producto("Mouse",      25.0, "Electrónica"),
    new Producto("Libro Java", 45.0, "Libros"),
    new Producto("Teclado",    80.0, "Electrónica"),
    new Producto("Novel",      18.0, "Libros")
);

// Nombres de electrónicos con precio > 50, ordenados
List<String> resultado = productos.stream()
    .filter(p -> p.categoria().equals("Electrónica"))
    .filter(p -> p.precio() > 50)
    .sorted((a, b) -> Double.compare(a.precio(), b.precio()))
    .map(Producto::nombre)
    .toList();

System.out.println(resultado); // [Teclado, Laptop]

// Precio total de libros
double totalLibros = productos.stream()
    .filter(p -> p.categoria().equals("Libros"))
    .mapToDouble(Producto::precio)
    .sum();

System.out.println("Total libros: $" + totalLibros); // Total libros: $63.0

Collectors — recopilar resultados

import java.util.stream.Collectors;

// Agrupar por categoría
Map<String, List<Producto>> porCategoria = productos.stream()
    .collect(Collectors.groupingBy(Producto::categoria));

porCategoria.forEach((cat, lista) -> {
    System.out.println(cat + ": " + lista.stream().map(Producto::nombre).toList());
});
// Electrónica: [Laptop, Mouse, Teclado]
// Libros: [Libro Java, Novel]

// Unir nombres en un String
String nombres = productos.stream()
    .map(Producto::nombre)
    .collect(Collectors.joining(", "));
System.out.println(nombres); // Laptop, Mouse, Libro Java, Teclado, Novel

Ejercicio 3

Dado un List<Producto> con al menos 10 productos de distintas categorías y precios:

  1. Filtra los productos con precio entre 20 y 100
  2. Ordénalos de mayor a menor precio
  3. Agrúpalos por categoría usando Collectors.groupingBy
  4. Para cada categoría calcula el precio promedio con Collectors.averagingDouble

6. Errores Comunes

Error 1 — ConcurrentModificationException al modificar mientras iteras

List<String> lista = new ArrayList<>(List.of("a", "b", "c"));

// Incorrecto: modificar la lista durante un for-each lanza excepción
for (String s : lista) {
    if (s.equals("b")) lista.remove(s); // ConcurrentModificationException
}

// Correcto: usa removeIf
lista.removeIf(s -> s.equals("b"));
System.out.println(lista); // [a, c]

Error 2 — NullPointerException con get() en HashMap

Map<String, Integer> mapa = new HashMap<>();
mapa.put("clave", 42);

// Peligroso: si la clave no existe, devuelve null
int valor = mapa.get("otra"); // NullPointerException al desempaquetar

// Seguro: usa getOrDefault
int valor = mapa.getOrDefault("otra", 0); // 0

Error 3 — Confundir == con .equals() en colecciones

List<String> a = new ArrayList<>(List.of("x", "y"));
List<String> b = new ArrayList<>(List.of("x", "y"));

System.out.println(a == b);       // false — distintas referencias
System.out.println(a.equals(b));  // true  — mismo contenido

Error 4 — Olvidar que toList() devuelve una lista no modificable

// toList() de Stream devuelve una lista inmutable
List<Integer> inmutable = Stream.of(1, 2, 3).toList();
inmutable.add(4); // UnsupportedOperationException

// Si necesitas modificarla, usa collect(Collectors.toList())
List<Integer> modificable = Stream.of(1, 2, 3)
    .collect(Collectors.toCollection(ArrayList::new));
modificable.add(4); // OK

Resumen

Herramienta Caso de uso
ArrayList Lista ordenada y de tamaño variable
HashMap Asociar claves únicas a valores
HashSet Conjunto de elementos únicos
Stream.filter Seleccionar elementos
Stream.map Transformar elementos
Stream.collect Agrupar, unir o acumular resultados

Próximos pasos

  • Excepciones: gestiona errores en tus colecciones (índice fuera de rango, clave nula) con try-catch
  • Genéricos: entiende por qué List<T> funciona para cualquier tipo
  • Patrones de diseño: usa el patrón Iterator, Builder y Factory con tus propias colecciones