1. ¿Qué es la evaluación perezosa?
La evaluación perezosa, o «cálculos perezosos» (lazy evaluation), es el principio por el cual las operaciones sobre los datos se posponen hasta que el resultado realmente hace falta. En el contexto de Stream API esto significa: si escribes una cadena de transformaciones sobre una colección, Java no las ejecuta enseguida: espera hasta que se invoque una operación terminal. Solo entonces se evalúa toda la cadena.
¿Para qué sirve? Primero, ahorra recursos: los elementos que al final no se necesitan simplemente no se procesan. Segundo, mejora el rendimiento: se pueden construir cadenas largas sin crear múltiples colecciones intermedias. Por último, hay «cálculos cortos»: en cuanto se encuentra el primer elemento adecuado, se detiene el procesamiento.
Analogía: imagina a un camarero perezoso. Dices: «Tráeme la carta, luego un café y luego un pastel». Él asiente, pero no hace nada… hasta que añades: «Y ahora tráelo de verdad». Entonces va a atender el pedido, y puede que traiga solo el café si ya no quedan pasteles. Así funcionan también los streams con evaluación perezosa.
2. Operaciones intermedias y terminales
Intermedias (intermediate):
- filter
- map
- sorted
- distinct
- peek (para depuración)
- y otras
Las operaciones intermedias devuelven un nuevo Stream, pero no inician los cálculos. Solo «construyen el plan» de procesamiento.
Terminales (terminal):
- collect
- forEach
- reduce
- count
- findFirst, findAny
- anyMatch, allMatch, noneMatch
- y otras
Solo una operación terminal inicia la ejecución de toda la cadena.
Ejemplo: no pasa nada sin una operación terminal
List<String> names = List.of("Alicia", "Bob", "Ivan");
names.stream()
.filter(name -> {
System.out.println("Filtrando " + name);
return name.startsWith("A");
});
// No habrá ningún mensaje. El código de arriba solo "construye" la cadena.
Ahora añadimos una operación terminal:
names.stream()
.filter(name -> {
System.out.println("Filtrando " + name);
return name.startsWith("A");
})
.forEach(System.out::println);
// ¡Ahora veremos la salida en la consola!
Resultado:
Filtrando Alicia
Filtrando Bob
Filtrando Ivan
Alicia
3. Ventajas de la evaluación perezosa
Ahorro de recursos
La evaluación perezosa permite no gastar tiempo ni memoria en elementos que no hacen falta. Por ejemplo, si buscas el primer objeto adecuado, el procesamiento se detendrá en la primera coincidencia.
List<String> names = List.of("Alicia", "Bob", "Ivan", "Anna");
String firstA = names.stream()
.filter(name -> {
System.out.println("Comprobando: " + name);
return name.startsWith("A");
})
.findFirst()
.orElse("No encontrado");
System.out.println("Resultado: " + firstA);
Salida:
Comprobando: Alicia
Resultado: Alicia
Fíjate: ¡los demás elementos ni siquiera se comprueban!
Cadenas largas sin colecciones intermedias
Se pueden combinar muchas operaciones (filter, map, sorted, etc.) sin crear colecciones en cada paso.
List<String> names = List.of("Alicia", "Bob", "Ivan", "Anna");
List<String> result = names.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.sorted()
.toList(); // Java 16+, antes — .collect(Collectors.toList())
Cálculos cortos
Si basta con saber «si hay algún elemento adecuado», los demás no se comprobarán:
boolean hasLongName = names.stream()
.anyMatch(name -> {
System.out.println("Comprobando: " + name);
return name.length() > 10;
});
// Si el primer elemento es largo — ¡los demás no se comprobarán!
4. Ejemplos: cómo funciona la evaluación perezosa
Ejemplo 1: no pasa nada sin una operación terminal
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
numbers.stream()
.filter(n -> {
System.out.println("Filtrando " + n);
return n % 2 == 0;
});
// ¡Sin salida!
Ejemplo 2: cadena con operación terminal
numbers.stream()
.filter(n -> {
System.out.println("Filtrando " + n);
return n % 2 == 0;
})
.map(n -> {
System.out.println("Multiplicando " + n);
return n * 10;
})
.forEach(System.out::println);
Salida:
Filtrando 1
Filtrando 2
Multiplicando 2
20
Filtrando 3
Filtrando 4
Multiplicando 4
40
Filtrando 5
Importante: las operaciones se ejecutan elemento a elemento: primero filter, luego map, después forEach, para cada elemento por turno. No son dos pasadas separadas de «primero filtrar todo, luego transformar todo».
Ejemplo 3: uso de peek para depuración
numbers.stream()
.filter(n -> n % 2 == 0)
.peek(n -> System.out.println("Pasó el filtro: " + n))
.map(n -> n * 10)
.peek(n -> System.out.println("Después de map: " + n))
.forEach(System.out::println);
5. Matices útiles
No uses streams con efectos secundarios
La pereza puede jugar una mala pasada si esperas ejecución inmediata. Acciones con efectos secundarios dentro de map, filter o peek (escritura a archivo, modificación de estado externo) pueden ejecutarse en orden distinto, no para todos los elementos o no ejecutarse en absoluto sin una operación terminal.
Filtra lo antes posible
Coloca filter cerca del inicio de la cadena para descartar antes los elementos sobrantes y reducir el trabajo posterior.
¿Solo necesitas el primer resultado? Usa los terminales correspondientes
Si necesitas el primer elemento adecuado, llama a findFirst o findAny. Permitirá que el stream se detenga en cuanto encuentre el resultado.
Los streams no son para modificar la colección original
Los streams no están pensados para añadir/eliminar elementos de la colección de origen. Para modificar la estructura de una colección, usa otros mecanismos.
Visualización del funcionamiento de streams perezosos
List<String> words = List.of("cat", "dog", "elephant", "fox", "giraffe");
words.stream()
.filter(w -> w.length() > 3)
.map(String::toUpperCase)
.forEach(System.out::println);
Cómo sucede:
| Etapa | cat | dog | elephant | fox | giraffe |
|---|---|---|---|---|---|
|
✗ | ✗ | ✓ | ✗ | ✓ |
|
— | — | ELEPHANT | — | GIRAFFE |
|
— | — | imprimir | — | imprimir |
Tabla: comparación de los enfoques eager y lazy
| Enfoque | ¿Cuándo se ejecuta el procesamiento? | Uso de memoria | Rendimiento |
|---|---|---|---|
| Eager (ansioso) | Inmediatamente al invocar | Puede ser alto | A veces lento |
| Lazy (perezoso) | Solo cuando es necesario | Mínimo | Normalmente más rápido |
Enfoque ansioso — por ejemplo, cuando organizas manualmente varias pasadas sobre la colección creando listas intermedias.
Enfoque perezoso — los streams: no se hace nada hasta que se necesita el resultado.
6. Errores típicos al trabajar con streams perezosos
Error n.º 1: esperar un resultado inmediato. Los principiantes piensan que las llamadas a filter o map se ejecutan enseguida. Pero sin una terminal (por ejemplo, collect, forEach) no pasará nada — de ahí «no funciona la depuración», «no se imprime nada».
Error n.º 2: efectos secundarios en operaciones intermedias. Escribir a archivo o cambiar variables externas dentro de map/filter/peek es una mala práctica. Por la pereza y optimizaciones, tales acciones pueden ejecutarse de forma incompleta, en un orden inesperado o no ejecutarse en absoluto.
Error n.º 3: olvidar llamar a una operación terminal. Se escribe una cadena de streams pero no se cierra con collect, forEach, etc. Resultado: «silencio».
Error n.º 4: esperar que todos los elementos se procesen. Operaciones como findFirst o anyMatch interrumpen la tubería en el primer resultado. Los demás elementos no se procesan — de ahí la sorpresa de «¿por qué mi println no se ejecutó para todos?».
Error n.º 5: usar streams para modificar la colección original. Los streams no están diseñados para modificar colecciones de origen (añadir/eliminar elementos). Usa métodos especializados de las colecciones o iteradores.
GO TO FULL VERSION