CodeGym /Cursos /JAVA 25 SELF /Operaciones básicas de Stream API: map, filter, collect

Operaciones básicas de Stream API: map, filter, collect

JAVA 25 SELF
Nivel 30 , Lección 1
Disponible

1. Creamos un flujo

Para usar Stream API, primero hay que obtener un flujo a partir de alguna colección o array.

Ejemplos de creación de un flujo

// Desde una lista
List<String> names = List.of("Anna", "Boris", "Alex", "Alina");
Stream<String> stream = names.stream();

// Desde un array
int[] numbers = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(numbers);

// Desde valores individuales
Stream<String> letters = Stream.of("A", "B", "C");

En resumen:

  • list.stream() — para colecciones
  • Arrays.stream(array) — para arrays
  • Stream.of(...) — para valores individuales

Ejemplo en el contexto de nuestra aplicación

Supongamos que tenemos una lista de usuarios:

List<String> users = List.of("Ivan", "Anna", "Petr", "Alexey");
Stream<String> userStream = users.stream();

Operaciones intermedias y terminales

Punto importante: las operaciones en Stream API se dividen en dos tipos.

  • Operaciones intermedias (por ejemplo, filter, map, distinct) — describen etapas de procesamiento. Devuelven un nuevo flujo, pero por sí solas no inician nada.
  • Operaciones terminales (por ejemplo, collect, forEach, count) — ponen en marcha el pipeline y entregan el resultado.

El flujo funciona «perezosamente»: mientras no se invoque una operación terminal, no se realizará ningún cálculo. Por eso frecuentemente terminamos la cadena con collect(...) — ese es el punto en el que el flujo se convierte de nuevo en una colección u otro resultado.

2. Operación filter: filtramos elementos por una condición

filter es una operación intermedia que deja pasar solo los elementos que cumplen una condición dada.

Firma

Stream<T> filter(Predicate<? super T> predicate); 

Predicate es una interfaz funcional que recibe un elemento y devuelve true (conservar) o false (descartar).

Ejemplo: dejar solo los números pares

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

List<Integer> evenNumbers = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());

System.out.println(evenNumbers); // [2, 4, 6]

¿Qué ocurre?

  • n -> n % 2 == 0 — una expresión lambda que comprueba si el número es divisible entre 2 sin resto.
  • filter deja solo los números pares.

Ejemplo: filtramos nombres que empiezan por «A»

List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");

List<String> aNames = names.stream()
    .filter(name -> name.startsWith("A"))
    .collect(Collectors.toList());

System.out.println(aNames); // [Anna, Alex, Alina]

Punto importante: filter no cambia la colección — crea un nuevo flujo que contiene solo los elementos necesarios.

3. Operación map: transformamos un elemento en otra cosa

map es una operación de transformación. Toma cada elemento del flujo, le aplica una función y devuelve un elemento nuevo.

Firma

<R> Stream<R> map(Function<? super T, ? extends R> mapper)

Function es una interfaz que recibe un elemento y devuelve algo (posiblemente de otro tipo).

Ejemplo: obtener las longitudes de las cadenas

List<String> names = List.of("Anna", "Boris", "Alex");

List<Integer> nameLengths = names.stream()
    .map(name -> name.length())
    .collect(Collectors.toList());

System.out.println(nameLengths); // [4, 5, 4]

¿Qué ocurre?

  • map convierte una cadena en su longitud (name -> name.length()).
  • Como resultado, obtenemos un flujo de números.

Ejemplo: convertir las cadenas a mayúsculas

List<String> names = List.of("Anna", "Boris", "Alex");

List<String> upperNames = names.stream()
    .map(name -> name.toUpperCase())
    .collect(Collectors.toList());

System.out.println(upperNames); // [ANNA, BORIS, ALEX]

4. Operación collect: recogemos el resultado de vuelta en una colección

collect es una operación terminal; finaliza el trabajo del flujo y recopila el resultado en una colección u otro contenedor.

Firma

<R, A> R collect(Collector<? super T, A, R> collector)

¡No te asustes por la firma tan aparatosa! En el 99 % de los casos usarás colectores ya preparados de la clase Collectors.

Collectors es una clase utilitaria con un conjunto de «recolectores». Indica al flujo en qué forma reunir el resultado: lista, conjunto, cadena, etc.

Ejemplos:

  • Collectors.toList() — a List
  • Collectors.toSet() — a Set
  • Collectors.joining(", ") — a una cadena separada por comas

Es decir, Collectors es como un conjunto de cajas de distintas formas en las que empaquetas los elementos del flujo.

Ejemplo: recopilar el resultado en un List

List<String> filtered = names.stream()
    .filter(name -> name.length() > 3)
    .collect(Collectors.toList());

Ejemplo: recopilar el resultado en un Set

Set<String> uniqueNames = names.stream()
    .map(String::toLowerCase)
    .collect(Collectors.toSet());

Ejemplo: unir cadenas separadas por comas

String result = names.stream()
    .collect(Collectors.joining(", "));

System.out.println(result); // Anna, Boris, Alex

5. Cadena de operaciones: filtrado + transformación + recolección del resultado

La mayor fortaleza de Stream API es la posibilidad de encadenar operaciones una tras otra.

Ejemplo: obtener las longitudes de los nombres que empiezan por «A»

List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");

List<Integer> aNameLengths = names.stream()
    .filter(name -> name.startsWith("A"))
    .map(String::length)
    .collect(Collectors.toList());

System.out.println(aNameLengths); // [4, 4, 5]

Paso a paso:

  1. .stream() — creamos un flujo a partir de la lista.
  2. .filter(name -> name.startsWith("A")) — dejamos solo los nombres que empiezan por "A".
  3. .map(String::length) — convertimos cada nombre en su longitud.
  4. .collect(Collectors.toList()) — recopilamos el resultado en una lista.

Código imperativo equivalente

Así se vería lo mismo «a la antigua»:

List<Integer> result = new ArrayList<>();
for (String name : names) {
    if (name.startsWith("A")) {
        result.add(name.length());
    }
}

Compáralo: con Stream API es una sola línea; se lee como «qué hacemos», no «cómo lo hacemos».

6. Práctica: algunas tareas cortas

¡A practicar! Todos los ejemplos se pueden ejecutar en un mismo archivo — simplemente cambia los datos.

Tarea 1: dejar solo los números impares y elevarlos al cuadrado

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

List<Integer> oddSquares = numbers.stream()
    .filter(n -> n % 2 != 0)
    .map(n -> n * n)
    .collect(Collectors.toList());

System.out.println(oddSquares); // [1, 9, 25, 49]

Tarea 2: obtener de una lista de cadenas la lista de sus primeras letras

List<String> names = List.of("Anna", "Boris", "Alex");

List<Character> initials = names.stream()
    .map(name -> name.charAt(0))
    .collect(Collectors.toList());

System.out.println(initials); // [A, B, A]

Tarea 3: filtrar las cadenas de longitud mayor que 3 y recopilarlas en un Set

List<String> words = List.of("cat", "dog", "elephant", "ant", "bear");

Set<String> longWords = words.stream()
    .filter(word -> word.length() > 3)
    .collect(Collectors.toSet());

System.out.println(longWords); // [bear, elephant]

7. Errores típicos al trabajar con filter, map, collect

Error n.º 1: te olvidaste de collect — ¡no hay resultado!
Stream API es perezoso como un gato en el alféizar: mientras no invoques una operación terminal (por ejemplo, collect o forEach), no ocurrirá nada. Si escribes solo users.stream().filter(...).map(...); — no se ejecutará ninguna acción.

Error n.º 2: filter y map intercambiados
A veces los principiantes hacen primero map y luego filter. Por ejemplo, names.stream().map(String::length).filter(len -> len > 3) — eso dará números, no cadenas. Si necesitas cadenas de longitud mayor que 3, filtra primero y luego transforma.

Error n.º 3: olvidar la inmutabilidad
¡Las operaciones de Stream API no modifican la colección original! Devuelven un resultado nuevo. Después de List<String> upper = names.stream().map(String::toUpperCase).collect(Collectors.toList()); — la colección names seguirá igual.

Error n.º 4: intentar usar una lista externa mutable
No conviene hacerlo así:

List<String> result = new ArrayList<>();
names.stream().filter(...).forEach(name -> result.add(name));

Es mejor usar collect — es más seguro y más corto.

Error n.º 5: NullPointerException
Si en la colección puede haber elementos null, invocar name.startsWith("A") sobre null dará un error. Añade un filtro por null cuando sea posible:

.filter(name -> name != null && name.startsWith("A"))
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION