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.