CodeGym /Cursos /JAVA 25 SELF /Análisis de errores en la programación funcional

Análisis de errores en la programación funcional

JAVA 25 SELF
Nivel 49 , Lección 4
Disponible

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.

1
Tarea
JAVA 25 SELF, nivel 49, lección 4
Bloqueada
Cálculo de los ingresos totales de la startup "Constructores Cósmicos" 🚀
Cálculo de los ingresos totales de la startup "Constructores Cósmicos" 🚀
1
Tarea
JAVA 25 SELF, nivel 49, lección 4
Bloqueada
Moderación de mensajes en "Foro Galáctico" 💬
Moderación de mensajes en "Foro Galáctico" 💬
1
Cuestionario/control
Programación funcional, nivel 49, lección 4
No disponible
Programación funcional
Programación funcional
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION