Interfaces funcionales de Java 8

Bienvenido al tutorial de ejemplo de interfaces funcionales de Java 8. Java siempre ha sido un lenguaje de programación orientado a objetos. Lo que significa que todo en la programación de Java gira en torno a objetos (excepto algunos tipos primitivos para simplificar). No solo tenemos funciones en Java, son parte de una clase y necesitamos usar la clase/objeto para invocar cualquier función.

Interfaces Funcionales de Java 8

Si observamos otros lenguajes de programación como C++ y JavaScript, se llaman lenguajes de programación funcional porque podemos escribir funciones y usarlas cuando sea necesario. Algunos de estos lenguajes admiten tanto la programación orientada a objetos como la programación funcional. Ser orientado a objetos no es malo, pero aporta mucha verbosidad al programa. Por ejemplo, digamos que tenemos que crear una instancia de Runnable. Por lo general, lo hacemos usando clases anónimas como se muestra a continuación.

Runnable r = new Runnable(){
			@Override
			public void run() {
				System.out.println("My Runnable");
			}};

Si observas el código anterior, la parte real que se utiliza es el código dentro del método run(). Todo lo demás del código es debido a la forma en que están estructurados los programas en Java. Las Interfaces Funcionales de Java 8 y las Expresiones Lambda nos ayudan a escribir un código más pequeño y limpio al eliminar una gran cantidad de código boilerplate.

Interfaz Funcional de Java 8

Una interfaz con exactamente un método abstracto se llama Interfaz Funcional. Se agrega la anotación @FunctionalInterface para marcar una interfaz como interfaz funcional. No es obligatorio usarlo, pero es una buena práctica usarlo con interfaces funcionales para evitar la adición accidental de métodos adicionales. Si la interfaz está anotada con la anotación @FunctionalInterface y tratamos de tener más de un método abstracto, arroja un error de compilador. El mayor beneficio de las interfaces funcionales de Java 8 es que podemos usar expresiones lambda para instanciarlas y evitar el uso de implementaciones de clases anónimas voluminosas. La API de Colecciones de Java 8 ha sido reescrita y se ha introducido la nueva API de Stream que utiliza muchas interfaces funcionales. Java 8 ha definido muchas interfaces funcionales en el paquete java.util.function. Algunas de las interfaces funcionales útiles de Java 8 son Consumer, Supplier, Function y Predicate. Puede encontrar más detalles sobre ellos en Ejemplo de Stream de Java 8. java.lang.Runnable es un gran ejemplo de interfaz funcional con un solo método abstracto run(). El fragmento de código a continuación proporciona una guía para interfaces funcionales:

interface Foo { boolean equals(Object obj); }
// No funcional porque equals ya es un miembro implícito (clase Object)

interface Comparator {
 boolean equals(Object obj);
 int compare(T o1, T o2);
}
// Funcional porque Comparator tiene solo un método abstracto no-Object

interface Foo {
  int m();
  Object clone();
}
// No funcional porque el método Object.clone no es público

interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// Funcional: dos métodos, pero tienen la misma firma

interface X { Iterable m(Iterable arg); }
interface Y { Iterable m(Iterable arg); }
interface Z extends X, Y {}
// Funcional: Y.m es una subfirma y sustituible en el tipo de retorno

interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// No funcional: Ningún método tiene una subfirma de todos los métodos abstractos

interface X { int m(Iterable arg, Class c); }
interface Y { int m(Iterable arg, Class c); }
interface Z extends X, Y {}
// No funcional: Ningún método tiene una subfirma de todos los métodos abstractos

interface X { long m(); }
interface Y { int m(); }
interface Z extends X, Y {}
// Error del compilador: ningún método es sustituible en el tipo de retorno

interface Foo { void m(T arg); }
interface Bar { void m(T arg); }
interface FooBar extends Foo, Bar {}
// Error del compilador: firmas diferentes, misma eliminación

Expresión Lambda

Las expresiones lambda son la forma mediante la cual podemos visualizar la programación funcional en el mundo orientado a objetos de Java. Los objetos son la base del lenguaje de programación Java y nunca podemos tener una función sin un objeto, por eso el lenguaje Java proporciona soporte para el uso de expresiones lambda solo con interfaces funcionales. Dado que solo hay una función abstracta en las interfaces funcionales, no hay confusión al aplicar la expresión lambda al método. La sintaxis de las expresiones lambda es (argumento) -> (cuerpo). Ahora veamos cómo podemos escribir el Runnable anónimo anterior usando una expresión lambda.

Runnable r1 = () -> System.out.println("My Runnable");

Intentemos entender qué está sucediendo en la expresión lambda anterior.

  • La interfaz Runnable es una interfaz funcional, por eso podemos usar una expresión lambda para crear su instancia.
  • Dado que el método run() no recibe argumentos, nuestra expresión lambda tampoco tiene argumentos.
  • Al igual que en los bloques if-else, podemos evitar las llaves ({}) ya que tenemos una sola instrucción en el cuerpo del método. Para múltiples instrucciones, tendríamos que usar llaves como en cualquier otro método.

¿Por qué necesitamos Expresiones Lambda?

  1. Líneas de Código Reducidas Uno de los claros beneficios de usar expresiones lambda es que se reduce la cantidad de código, ya hemos visto lo fácil que es crear una instancia de una interfaz funcional usando una expresión lambda en lugar de usar una clase anónima.

  2. Soporte de ejecución secuencial y paralela Otro beneficio de usar expresiones lambda es que podemos beneficiarnos del soporte de operaciones secuenciales y paralelas de la API de Stream. Para explicarlo, tomemos un ejemplo simple en el que necesitamos escribir un método para probar si un número pasado es un número primo o no. Tradicionalmente, escribiríamos su código de la siguiente manera. El código no está completamente optimizado pero es bueno para el propósito del ejemplo, así que acompáñame en esto.

    //Enfoque tradicional
    private static boolean isPrime(int number) {		
    	if(number < 2) return false;
    	for(int i=2; i<number; i++){
    		if(number % i == 0) return false;
    	}
    	return true;
    }
    

    El problema con el código anterior es que es secuencial en naturaleza, si el número es muy grande entonces tomará una cantidad significativa de tiempo. Otro problema con el código es que hay tantos puntos de salida y no es legible. Veamos cómo podemos escribir el mismo método usando expresiones lambda y la API de Stream.

    //Enfoque declarativo
    private static boolean isPrime(int number) {		
    	return number > 1
    			&& IntStream.range(2, number).noneMatch(
    					index -> number % index == 0);
    }
    

    IntStream es una secuencia de elementos primitivos de tipo int que admite operaciones de agregación secuenciales y paralelas. Esta es la especialización primitiva de tipo int de Stream. Para mayor legibilidad, también podemos escribir el método de la siguiente manera.

    private static boolean isPrime(int number) {
    	IntPredicate isDivisible = index -> number % index == 0;
    	
    	return number > 1
    			&& IntStream.range(2, number).noneMatch(
    					isDivisible);
    }
    

    Si no está familiarizado con IntStream, el método range() devuelve un IntStream ordenado secuencialmente desde startInclusive (inclusive) hasta endExclusive (exclusivo) con un incremento incremental de 1. El método noneMatch() devuelve si no hay elementos en este flujo que coincidan con el predicado proporcionado. Puede que no evalúe el predicado en todos los elementos si no es necesario para determinar el resultado.

  3. Passando comportamientos en métodos Veamos cómo podemos usar expresiones lambda para pasar el comportamiento de un método con un ejemplo simple. Digamos que tenemos que escribir un método para sumar los números en una lista si coinciden con un criterio dado. Podemos usar un Predicado y escribir un método como el siguiente.

    public static int sumarConCondicion(List<Integer> numeros, Predicate<Integer> predicado) {
    	    return numeros.parallelStream()
    	    		.filter(predicado)
    	    		.mapToInt(i -> i)
    	    		.sum();
    	}
    

    Ejemplo de uso:

    //suma de todos los números
    sumarConCondicion(numeros, n -> true)
    //suma de todos los números pares
    sumarConCondicion(numeros, i -> i%2==0)
    //suma de todos los números mayores que 5
    sumarConCondicion(numeros, i -> i>5)
    
  4. Mayor eficiencia con pereza Una ventaja más de usar expresiones lambda es la evaluación perezosa, por ejemplo, supongamos que necesitamos escribir un método para encontrar el número impar máximo en el rango de 3 a 11 y devolver su cuadrado. Normalmente escribiríamos el código para este método así:

    private static int findSquareOfMaxOdd(List<Integer> numbers) {
    		int max = 0;
    		for (int i : numbers) {
    			if (i % 2 != 0 && i > 3 && i < 11 && i > max) {
    				max = i;
    			}
    		}
    		return max * max;
    	}
    

    El programa anterior siempre se ejecutará en orden secuencial, pero podemos usar la API de Streams para lograr esto y obtener el beneficio de la búsqueda de pereza. Veamos cómo podemos reescribir este código de manera funcional usando la API de Streams y expresiones lambda.

    public static int findSquareOfMaxOdd(List<Integer> numbers) {
    		return numbers.stream()
    				.filter(NumberTest::isOdd) 		// El predicado es una interfaz funcional y
    				.filter(NumberTest::isGreaterThan3)	// estamos usando expresiones lambda para inicializarlo
    				.filter(NumberTest::isLessThan11)	// en lugar de clases internas anónimas
    				.max(Comparator.naturalOrder())
    				.map(i -> i * i)
    				.get();
    	}
    
    	public static boolean isOdd(int i) {
    		return i % 2 != 0;
    	}
    	
    	public static boolean isGreaterThan3(int i){
    		return i > 3;
    	}
    	
    	public static boolean isLessThan11(int i){
    		return i < 11;
    	}
    

    Si te sorprende el operador de doble colon (::), se introdujo en Java 8 y se utiliza para referencias de método. El compilador de Java se encarga de hacer coincidir los argumentos con el método llamado. Es una forma abreviada de expresiones lambda i -> isGreaterThan3(i) o i -> NumberTest.isGreaterThan3(i).

Ejemplos de expresiones lambda

A continuación, proporciono algunos fragmentos de código para expresiones lambda con pequeños comentarios que las explican.

() -> {}                     // No parameters; void result

() -> 42                     // No parameters, expression body
() -> null                   // No parameters, expression body
() -> { return 42; }         // No parameters, block body with return
() -> { System.gc(); }       // No parameters, void block body

// Cuerpo de bloque complejo con múltiples retornos
() -> {
  if (true) return 10;
  else {
    int result = 15;
    for (int i = 1; i < 10; i++)
      result *= i;
    return result;
  }
}                          

(int x) -> x+1             // Single declared-type argument
(int x) -> { return x+1; } // same as above
(x) -> x+1                 // Single inferred-type argument, same as below
x -> x+1                   // Parenthesis optional for single inferred-type case

(String s) -> s.length()   // Single declared-type argument
(Thread t) -> { t.start(); } // Single declared-type argument
s -> s.length()              // Single inferred-type argument
t -> { t.start(); }          // Single inferred-type argument

(int x, int y) -> x+y      // Multiple declared-type parameters
(x,y) -> x+y               // Multiple inferred-type parameters
(x, final y) -> x+y        // Illegal: can't modify inferred-type parameters
(x, int y) -> x+y          // Illegal: can't mix inferred and declared types

Referencias a métodos y constructores

A method reference is used to refer to a method without invoking it; a constructor reference is similarly used to refer to a constructor without creating a new instance of the named class or array type. Examples of method and constructor references:

System::getProperty
System.out::println
"abc"::length
ArrayList::new
int[]::new

Eso es todo para el Tutorial de Interfaces Funcionales y Expresiones Lambda de Java 8. Te sugeriría encarecidamente que lo investigaras y lo utilizaras, porque esta sintaxis es nueva en Java y llevará un tiempo entenderla completamente. También deberías echar un vistazo a Características de Java 8 para aprender sobre todas las mejoras y cambios en la versión 8 de Java.

Source:
https://www.digitalocean.com/community/tutorials/java-8-functional-interfaces