1. Transformación de List → Set y viceversa
Recordemos para qué necesitamos transformar colecciones. A menudo, en tareas reales necesitamos:
- Obtener de una lista los elementos únicos (por ejemplo, lista de e-mail → conjunto de direcciones únicas).
- Construir un mapa (Map), por ejemplo, a partir de una lista de nombres obtener el mapa «nombre → longitud del nombre».
- Unir los elementos en una sola cadena (por ejemplo, para una salida legible).
Antes había que escribir mucho código con bucles, condiciones y colecciones temporales. Con Stream API todo se volvió más sencillo y… ¡más elegante!
Ejemplo: obtener un conjunto de nombres únicos a partir de una lista
Supongamos que tenemos una lista de nombres (por si alguien en tu programa se introdujo dos veces — ¡pasa!):
List<String> names = List.of("Anna", "Sergey", "Anna", "Maria", "Ivan", "Sergey");
Nuestra tarea es obtener una colección donde cada nombre aparezca solo una vez, es decir, un conjunto (Set). Con Stream API se hace literalmente en una sola línea:
Set<String> uniqueNames = names.stream()
.collect(Collectors.toSet());
System.out.println(uniqueNames);
Salida:
[Maria, Ivan, Anna, Sergey]
(El orden en Set no está garantizado — no te sorprendas si ves otro orden.)
¿Y si lo necesitamos al revés: Set → List?
A veces necesitamos lo contrario: convertir un conjunto en una lista (por ejemplo, para ordenar o para acceder por índice):
List<String> namesList = uniqueNames.stream()
.collect(Collectors.toList());
System.out.println(namesList);
2. Transformación a Map: Collectors.toMap()
Ejemplo: desde una lista de nombres obtener un Map «nombre → longitud del nombre»
A veces apetece ser no solo programador, sino todo un cartógrafo — ¡construir mapas! Probemos:
List<String> names = List.of("Anna", "Sergey", "Maria", "Ivan");
Map<String, Integer> nameToLength = names.stream()
.collect(Collectors.toMap(
name -> name, // clave: el propio nombre
name -> name.length() // valor: longitud del nombre
));
System.out.println(nameToLength);
Salida:
{Maria=5, Ivan=4, Anna=4, Sergey=6}
Punto importante: claves duplicadas
Si en la lista de origen hay nombres repetidos, al intentar recolectarlos en un Map se producirá un IllegalStateException: Duplicate key. A Java no le gusta que intentes poner dos valores con la misma clave.
¿Cómo gestionar los duplicados?
Se puede indicar qué hacer cuando coinciden las claves — por ejemplo, conservar el primer valor o el último:
List<String> names = List.of("Anna", "Sergey", "Anna", "Maria", "Ivan", "Sergey");
Map<String, Integer> nameToLength = names.stream()
.collect(Collectors.toMap(
name -> name,
name -> name.length(),
(oldValue, newValue) -> oldValue // conservar el primer valor
));
System.out.println(nameToLength);
Ahora el programa no fallará y en el Map entrará solo la primera aparición de cada nombre.
Ejemplo: Map con objetos
Complicamos un poco: tenemos una lista de usuarios y queremos construir un Map «nombre → usuario»:
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " (" + age + ")";
}
}
// Ejemplo de lista de usuarios
List<User> users = List.of(
new User("Anna", 25),
new User("Sergey", 30),
new User("Maria", 22)
);
Map<String, User> nameToUser = users.stream()
.collect(Collectors.toMap(
user -> user.name,
user -> user
));
System.out.println(nameToUser);
Salida:
{Maria=Maria (22), Anna=Anna (25), Sergey=Sergey (30)}
3. Unir en una cadena: Collectors.joining()
A veces no queremos solo recolectar una colección, sino crear una cadena bonita para mostrar al usuario o en el log. Por ejemplo, unir todos los nombres con comas:
List<String> names = List.of("Anna", "Sergey", "Maria", "Ivan");
String result = names.stream()
.collect(Collectors.joining(", "));
System.out.println(result);
Salida:
Anna, Sergey, Maria, Ivan
Se puede añadir prefijo y sufijo
String result = names.stream()
.collect(Collectors.joining(", ", "Lista: [", "]"));
System.out.println(result);
Salida:
Lista: [Anna, Sergey, Maria, Ivan]
4. Operaciones terminales: forEach, collect, count, anyMatch, allMatch, noneMatch
Método forEach
Con forEach ya estamos bien familiarizados: esta operación ejecuta una acción para cada elemento del stream.
names.stream().forEach(name -> System.out.println("Hola, " + name + "!"));
Método collect
Recoge los elementos en una colección, cadena u otra estructura. La operación más habitual es recolectar en List o Set mediante Collectors.toList() y Collectors.toSet().
Método count
Cuenta el número de elementos del stream.
long count = names.stream()
.filter(name -> name.length() > 4)
.count();
System.out.println("Nombres con más de 4 letras: " + count);
Métodos anyMatch, allMatch, noneMatch
Comprueban si una condición se cumple al menos para un elemento (anyMatch), para todos (allMatch) o para ninguno (noneMatch).
boolean hasShortName = names.stream()
.anyMatch(name -> name.length() < 4);
System.out.println("¿Hay un nombre corto? " + hasShortName);
boolean allLong = names.stream()
.allMatch(name -> name.length() > 3);
System.out.println("¿Todos los nombres tienen más de 3 letras? " + allLong);
boolean noneIvan = names.stream()
.noneMatch(name -> name.equals("Ivan"));
System.out.println("¿No hay Ivan? " + noneIvan);
Salida:
¿Hay un nombre corto? false
¿Todos los nombres tienen más de 3 letras? true
¿No hay Ivan? false
5. Operaciones terminales e intermedias: fijemos conceptos
Operaciones intermedias (filter, map, distinct, sorted, limit, skip, peek) — devuelven un nuevo Stream; se pueden encadenar.
Operaciones terminales (forEach, collect, count, anyMatch, allMatch, noneMatch, reduce, findFirst, findAny) — cierran el stream; después ya no habrá más resultado.
Ejemplo de encadenado:
List<String> result = users.stream()
.filter(user -> user.age > 20)
.map(user -> user.name.toUpperCase())
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println(result);
Salida:
[ANNA, IVAN, MARIA, SERGEY]
6. Errores típicos al transformar colecciones con Stream
Error n.º 1: No gestionar duplicados de clave en toMap
Si en la colección de origen aparecen claves duplicadas y utilizas Collectors.toMap() sin un manejador explícito, el programa lanzará una excepción. Para esos casos, especifica siempre una función de fusión:
// Conservar el último valor
.toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)
Error n.º 2: Usar forEach en lugar de collect
A veces los principiantes intentan «recolectar» una colección con forEach, por ejemplo:
List<String> list = new ArrayList<>();
names.stream().forEach(name -> list.add(name)); // Funciona, pero no es el estilo Stream (Stream-way)!
Es mejor usar collect(Collectors.toList()) — es más seguro y más limpio.
Error n.º 3: Intentar reutilizar un stream
Un stream solo se puede usar una vez. Después de una operación terminal (por ejemplo, collect, forEach), intentar seguir trabajando con ese mismo Stream llevará a un IllegalStateException.
Error n.º 4: Violar el principio de «sin efectos secundarios»
Las operaciones intermedias deben ser «puras» (sin modificar variables externas). No conviene cambiar nada fuera del stream dentro de map o filter.
Error n.º 5: No tener en cuenta el orden en Set y Map
Si el orden de los elementos es importante, usa colecciones adecuadas — por ejemplo, LinkedHashSet, TreeMap — y especifica el colector apropiado.
GO TO FULL VERSION