CodeGym /Cursos /JAVA 25 SELF /Transformación de colecciones mediante Stream

Transformación de colecciones mediante Stream

JAVA 25 SELF
Nivel 30 , Lección 4
Disponible

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.

1
Cuestionario/control
Fundamentos de Stream API, nivel 30, lección 4
No disponible
Fundamentos de Stream API
Fundamentos de Stream API
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION