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:
- .stream() — creamos un flujo a partir de la lista.
- .filter(name -> name.startsWith("A")) — dejamos solo los nombres que empiezan por "A".
- .map(String::length) — convertimos cada nombre en su longitud.
- .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"))
GO TO FULL VERSION