1. Errores con expresiones lambda: captura de variables
En Java, las expresiones lambda pueden utilizar variables del contexto externo. Pero existe una restricción: dichas variables deben ser final o «efectivamente finales» (effectively final), es decir, no deben modificarse después de su inicialización.
Ejemplo de error
int sum = 0;
List<Integer> list = List.of(1, 2, 3, 4, 5);
list.forEach(n -> sum += n); // ¡Error de compilación!
¿Por qué?
El compilador protesta: la variable se usa en la lambda, por lo tanto debe ser final o effectively final, y sum se modifica dentro de la lambda.
¿Cómo evitarlo?
- Usa operaciones terminales de streams que no requieran variables externas: mapToInt + sum().
- En casos extremos, contenedores como AtomicInteger o un array de un solo elemento (pero esto es más bien un truco).
int sum = list.stream().mapToInt(Integer::intValue).sum();
Analogía
Imagina que una lambda es un «viajero en el tiempo»: «recuerda» el valor de la variable en el momento de su creación y no puede observar cómo cambia. Intentar modificarlo es la «paradoja del abuelo»; el compilador no permitirá compilar el programa.
2. Errores con el ámbito y this
En una expresión lambda, la palabra clave this se refiere al objeto externo, y no a una clase anónima (como ocurriría con clases anónimas).
Ejemplo
public class Example {
int value = 42;
void foo() {
Runnable r = () -> {
System.out.println(this.value); // this hace referencia a Example, no a Runnable!
};
r.run();
}
}
Importante: al reescribir código de clases anónimas a lambdas, la lógica de this cambia; tenlo en cuenta para no obtener un resultado inesperado.
3. Problemas con estado mutable (efectos secundarios)
El enfoque funcional recomienda la ausencia de efectos secundarios: las funciones no cambian el estado externo ni mutan colecciones/variables externas.
List<String> names = new ArrayList<>(List.of("Ana", "Boris", "Vika"));
List<String> newNames = new ArrayList<>();
names.forEach(name -> {
if (name.startsWith("A")) {
newNames.add(name); // ¡Efecto secundario!
}
});
El código «funciona», pero es menos predecible y peligroso al usar parallelStream() (riesgo de condiciones de carrera y excepciones). Es más difícil de probar y mantener.
Correcto: usa operaciones que formen explícitamente un nuevo resultado sin modificar el estado externo.
List<String> newNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
4. Errores con tipos y genéricos
Java es un lenguaje fuertemente tipado. A veces el compilador no es capaz de inferir tipos a partir de lambdas o cadenas demasiado complejas.
Ejemplo
List<Object> objects = List.of(1, "cadena", 3.14);
List<String> strings = objects.stream()
.filter(obj -> obj instanceof String)
.map(obj -> (String) obj)
.collect(Collectors.toList());
Parece lógico, pero cualquier error tipográfico o conversión incorrecta puede provocar un error de compilación o, peor aún, un ClassCastException en tiempo de ejecución.
¿Cómo evitarlo?
- Añade tipos explícitos cuando la inferencia de tipos «tropiece».
- No temas escribir <String> o parametrizar lambdas: (String s) -> ....
- Comprueba la compatibilidad de tipos en las transformaciones.
Caso típico con Optional
Optional<String> opt = Optional.of("hello");
opt.map(s -> s.length()); // el resultado es Optional<Integer>
Si esperabas Optional<String> y obtuviste Optional<Integer>, comprueba qué devuelve tu función.
5. Efectos secundarios en lambdas y paralelismo
Los streams paralelos (parallelStream()) más efectos secundarios son una combinación peligrosa.
Ejemplo
List<Integer> numbers = IntStream.range(0, 1000).boxed().collect(Collectors.toList());
List<Integer> results = new ArrayList<>();
numbers.parallelStream().forEach(n -> results.add(n)); // ¡PELIGROSO!
¿Qué puede ocurrir?
- Pérdida o duplicación de datos.
- ConcurrentModificationException o bugs «misteriosos».
¿Cómo hacerlo bien?
- Usar colecciones seguras para hilos: ConcurrentLinkedQueue, CopyOnWriteArrayList.
- Aún mejor: evitar por completo los efectos secundarios y recolectar el resultado con collect(...).
List<Integer> results = numbers.parallelStream()
.map(n -> n)
.collect(Collectors.toList());
6. Pérdida de legibilidad: «espagueti de streams» y cadenas largas
El estilo funcional es bueno hasta que la cadena se convierte en «ticket de hipermercado».
List<String> result = list.stream()
.filter(s -> s.length() > 2)
.map(String::trim)
.map(s -> s.toUpperCase())
.filter(s -> s.contains("JAVA"))
.sorted()
.distinct()
.collect(Collectors.toList());
Consejos:
- Divide las cadenas en bloques lógicos.
- Extrae lambdas complejas a métodos separados con nombres claros.
- Añade comentarios si es necesario, incluso en código de Stream.
7. Nombres poco adecuados de variables y funciones
Los nombres excesivamente cortos (x, y, z) dificultan la comprensión.
list.stream()
.map(x -> x.trim())
.filter(y -> y.length() > 3)
.map(z -> z.toUpperCase())
.forEach(System.out::println);
Usa nombres significativos, especialmente si la lambda es multilínea o expresa una lógica no trivial.
8. Errores con null y Optional
Stream API y los interfaces funcionales no «aman» los valores null. Pasar null a una lambda o a un stream es una causa frecuente de NullPointerException.
List<String> list = Arrays.asList("a", null, "b");
list.stream()
.map(String::toUpperCase) // ¡Boom! NPE en el segundo elemento
.forEach(System.out::println);
¿Cómo hacerlo correctamente?
- Filtra los null de antemano: .filter(Objects::nonNull).
- Usa Optional para representar explícitamente la ausencia de valor.
9. Problemas con el tipo de retorno en funciones combinadas
Al usar compose y andThen es fácil confundir el orden de aplicación de funciones y los tipos esperados.
Function<String, Integer> parse = Integer::parseInt;
Function<Integer, Integer> square = x -> x * x;
Function<String, Integer> parseAndSquare = parse.andThen(square);
// Funciona: primero parse, luego square
Function<String, Integer> squareThenParse = parse.compose(square);
// ¡Error! square recibe Integer y parse espera String
Moraleja: comprueba siempre el orden de aplicación y la compatibilidad de tipos.
10. Problemas con excepciones comprobadas (checked) en lambdas
Los interfaces funcionales del paquete java.util.function no permiten lanzar excepciones comprobadas (checked) (por ejemplo, IOException). Si dentro de una lambda necesitas código que las lance, maneja la excepción manualmente.
Function<String, String> readFile = path -> {
try {
return Files.readString(Path.of(path));
} catch (IOException e) {
throw new RuntimeException(e); // O manejarlo de otra forma
}
};
De lo contrario, el compilador no permitirá usar esa función en un stream o colección.
GO TO FULL VERSION