Interfaces funcionales y funciones flecha

Anteriormente hemos trabajado con clases anónimas en Java, que nos permiten implementar interfaces o extender clases abstractas directamente en el código, sin necesidad de crear archivos separados. Esto es especialmente útil cuando queremos definir comportamientos específicos en un lugar concreto, usando la palabra clave new seguida del nombre de la interfaz o clase, y luego implementando los métodos necesarios dentro de llaves.

Sin embargo, muchas veces nos encontramos con interfaces muy simples, que solo tienen un único método abstracto. Un ejemplo clásico es la interfaz Runnable, que solo define el método run. Cuando implementamos Runnable de forma anónima, solo necesitamos proporcionar la implementación de ese método, sin preocuparnos por otros.

A partir de Java 8, para estas interfaces que tienen un único método abstracto, conocidas como SAM (Single Abstract Method), podemos usar una sintaxis mucho más concisa llamada funciones flecha o arrow functions. En lugar de escribir toda la estructura tradicional con new Runnable() y el método run, podemos simplemente escribir los paréntesis para los parámetros, luego la flecha -> y finalmente el bloque de código que queremos ejecutar. Por ejemplo:

Runnable r = () -> {
    System.out.println("Hola mundo");
};

Esto es exactamente equivalente a la forma tradicional, pero mucho más limpio y rápido de escribir. En el caso de Runnable, que no recibe parámetros ni devuelve nada, esta sintaxis es especialmente sencilla.

Pero, ¿qué pasa cuando la interfaz funcional tiene parámetros y un valor de retorno? Por ejemplo, la interfaz Predicate<T> es un poco más compleja. Aunque tiene varios métodos, solo uno es abstracto: test(T t), que recibe un parámetro genérico y devuelve un booleano. Los demás métodos son default, es decir, ya tienen implementación dentro de la interfaz, lo que permite que Predicate siga siendo una interfaz funcional.

Para implementar un Predicate, podemos hacerlo de forma tradicional con una clase anónima:

Predicate<String> pred = new Predicate<String>() {
    @Override
    public boolean test(String t) {
        return false;
    }
};

O bien, usando la sintaxis moderna con funciones flecha:

Predicate<String> pred2 = (t) -> {
    return false;
};

Incluso podemos omitir el tipo del parámetro si el compilador puede inferirlo:

Predicate<String> pred3 = t -> {
    return false;
};

Cuando la función solo tiene una expresión que devuelve un valor, podemos simplificar aún más la sintaxis eliminando las llaves y el return, escribiendo directamente la expresión después de la flecha:

Predicate<String> esMayusculas = str -> str.toUpperCase().equals(str);

Este predicado verifica si una cadena está completamente en mayúsculas comparando la cadena original con su versión en mayúsculas. Podemos usarlo en condicionales como:

if (esMayusculas.test("HOLA")) {
    System.out.println("La cadena está en mayúsculas");
}

Esta forma de escribir funciones flecha es muy útil para crear implementaciones concisas de interfaces funcionales, siempre que solo tengan un método abstracto. Si la interfaz tuviera más de un método abstracto, esta sintaxis no sería válida porque no podríamos especificar cada método por separado.

Además, esta sintaxis nos permite, por ejemplo, crear hilos de forma más sencilla:

Thread t = new Thread(() -> {
    System.out.println("Ejecutando en un hilo");
});
t.start();
t.join();

En resumen, las funciones flecha nos ofrecen una manera mucho más limpia y rápida de implementar interfaces funcionales en Java 8 y versiones posteriores, facilitando la escritura de código más legible y menos verboso.

Lista de reproducción
  1. 1
    ¿Cómo funciona Comparator en Java?
    10 minutos
  2. 2
    ¿Para qué sirve Override?
    4 minutos
  3. 3
    Cómo usar la clase Properties en Java
    14 minutos
  4. 4
    Interfaces funcionales y funciones flecha
    8 minutos
  5. 5
    Bloque static de Java: inicializadores estáticos y otros usos
    7 minutos
  6. 6
    import static, ¿para qué sirve?
    3 minutos
  7. 7
    Uso de Optional en Java
    10 minutos
  8. 8
    Introducción al Scanner de Java
    11 minutos
  9. 9
    ¿Para qué sirve el modificador static de Java?
    6 minutos
  10. 10
    Funciones variádicas en Java
    6 minutos
  11. 11
    Inner classes en Java
    8 minutos
  12. 12
    var en Java: una introducción para gente nueva
    7 minutos
  13. 13
    Switch expressions en Java
    10 minutos
  14. 14
    Java: del instanceof al Pattern Matching
    7 minutos
  15. 15
    Sealed classes en Java
    8 minutos
  16. 16
    Getters y setters o atributos públicos en Java, ¿qué es mejor?
    9 minutos
  17. 17
    El String[] args del método main de Java
    8 minutos
  18. 18
    Introducción a clases anónimas en Java
    6 minutos
  19. 19
    Introducción a enum en Java
    6 minutos