Bienvenido al tutorial de ejemplo de interfaces funcionales de Java 8. Java siempre ha sido un lenguaje de programación orientado a objetos . Esto significa que todo en la programación de Java gira en torno a objetos (excepto algunos tipos primitivos para simplificar). En Java no solo tenemos funciones, sino que son parte de una clase y necesitamos usar la clase/objeto para invocar cualquier función.
Interfaces funcionales de Java 8
Si analizamos otros lenguajes de programación como C++ y JavaScript, se denominan lenguajes de programación funcionales porque podemos escribir funciones y usarlas cuando sea necesario. Algunos de estos lenguajes admiten la programación orientada a objetos, así como la programación funcional. Estar orientado a objetos no es malo, pero aporta mucha verbosidad al programa. Por ejemplo, digamos que tenemos que crear una instancia de Runnable. Normalmente lo hacemos utilizando clases anónimas como la siguiente.
Runnable r = new Runnable(){@Overridepublic void run() {System.out.println("My Runnable");}};
Si observa el código anterior, la parte que realmente se utiliza es el código dentro del método run(). El resto del código se debe a la forma en que se estructuran los programas Java. Las interfaces funcionales y las expresiones Lambda de Java 8 nos ayudan a escribir código más pequeño y más claro al eliminar una gran cantidad de código repetitivo.
Interfaz funcional de Java 8
Una interfaz con exactamente un método abstracto se llama Interfaz funcional. @FunctionalInterfaceSe agrega la anotación para que podamos marcar una interfaz como interfaz funcional. No es obligatorio usarla, pero es una buena práctica usarla con interfaces funcionales para evitar la adición de métodos adicionales accidentalmente. Si la interfaz está anotada con @FunctionalInterfaceuna anotación e intentamos tener más de un método abstracto, arroja un error de compilación. El principal beneficio de las interfaces funcionales de Java 8 es que podemos usar expresiones lambda para instanciarlas y evitar usar una implementación de clase anónima voluminosa. La API de colecciones de Java 8 se ha reescrito y se introdujo la nueva API Stream que usa muchas interfaces funcionales. Java 8 ha definido muchas interfaces funcionales en java.util.functionel paquete. Algunas de las interfaces funcionales de Java 8 útiles son Consumer, y . Puede encontrar más detalles sobre ellas en Ejemplo de flujo de Java 8 . es un gran ejemplo de interfaz funcional con un solo método abstracto . El siguiente fragmento Supplierde código proporciona algunas pautas para las interfaces funcionales:FunctionPredicatejava.lang.Runnablerun()
interface Foo { boolean equals(Object obj); }// Not functional because equals is already an implicit member (Object class)interface ComparatorT { boolean equals(Object obj); int compare(T o1, T o2);}// Functional because Comparator has only one abstract non-Object methodinterface Foo { int m(); Object clone();}// Not functional because method Object.clone is not publicinterface X { int m(IterableString arg); }interface Y { int m(IterableString arg); }interface Z extends X, Y {}// Functional: two methods, but they have the same signatureinterface X { Iterable m(IterableString arg); }interface Y { IterableString m(Iterable arg); }interface Z extends X, Y {}// Functional: Y.m is a subsignature return-type-substitutableinterface X { int m(IterableString arg); }interface Y { int m(IterableInteger arg); }interface Z extends X, Y {}// Not functional: No method has a subsignature of all abstract methodsinterface X { int m(IterableString arg, Class c); }interface Y { int m(Iterable arg, Class? c); }interface Z extends X, Y {}// Not functional: No method has a subsignature of all abstract methodsinterface X { long m(); }interface Y { int m(); }interface Z extends X, Y {}// Compiler error: no method is return type substitutableinterface FooT { void m(T arg); }interface BarT { void m(T arg); }interface FooBarX, Y extends FooX, BarY {}// Compiler error: different signatures, same erasure
Expresión Lambda
Las expresiones Lambda son la forma a través de 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, es por eso que el lenguaje Java proporciona soporte para usar 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 la expresión lambda.
Runnable r1 = () - System.out.println("My Runnable");
Intentemos comprender qué está sucediendo en la expresión lambda anterior.
- Runnable es una interfaz funcional, por eso podemos usar la expresión lambda para crear su instancia.
- Dado que el método run() no toma argumentos, nuestra expresión lambda tampoco tiene argumentos.
- Al igual que los bloques if-else, podemos evitar las llaves ({}) ya que tenemos una sola declaración en el cuerpo del método. Para múltiples declaraciones, tendríamos que usar llaves como cualquier otro método.
¿Por qué necesitamos la expresión Lambda?
-
Líneas de código reducidas Uno de los beneficios claros de usar expresiones lambda es que se reduce la cantidad de código, ya hemos visto con qué facilidad podemos crear una instancia de una interfaz funcional usando expresiones lambda en lugar de usar una clase anónima.
-
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 Stream API. Para explicar esto, tomemos un ejemplo simple donde 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 como se muestra a continuación. El código no está completamente optimizado, pero es bueno para el propósito del ejemplo, así que tengan paciencia.
//Traditional approachprivate static boolean isPrime(int number) {if(number 2) return false;for(int i=2; inumber; i++){if(number % i == 0) return false;}return true;}El problema con el código anterior es que es de naturaleza secuencial, si el número es muy grande, tomará una cantidad significativa de tiempo. Otro problema con el código es que hay muchos puntos de salida y no es legible. Veamos cómo podemos escribir el mismo método usando expresiones lambda y la API de flujo.
//Declarative approachprivate static boolean isPrime(int number) {return number 1 IntStream.range(2, number).noneMatch(index - number % index == 0);}IntStreames una secuencia de elementos primitivos con valores int que admiten operaciones de agregación secuenciales y paralelas. Esta es la especialización primitiva int deStream. Para mayor legibilidad, también podemos escribir el método como se muestra a continuación.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, su método range() devuelve un IntStream ordenado secuencialmente desde startInclusive (inclusive) hasta endExclusive (exclusive) en un paso incremental de 1. El método noneMatch() devuelve si ningún elemento de este flujo coincide con el predicado proporcionado. Es posible que no evalúe el predicado en todos los elementos si no es necesario para determinar el resultado.
-
Pasar comportamientos a 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 de una lista si coinciden con un criterio dado. Podemos usar Predicate y escribir un método como el siguiente.
public static int sumWithCondition(ListInteger numbers, PredicateInteger predicate) { return numbers.parallelStream() .filter(predicate) .mapToInt(i - i) .sum();}Ejemplo de uso:
//sum of all numberssumWithCondition(numbers, n - true)//sum of all even numberssumWithCondition(numbers, i - i%2==0)//sum of all numbers greater than 5sumWithCondition(numbers, i - i5) -
Mayor eficiencia con pereza Otra ventaja de usar expresiones lambda es la evaluación perezosa. Por ejemplo, digamos que necesitamos escribir un método para averiguar el número impar máximo en el rango de 3 a 11 y devolver el cuadrado del mismo. Por lo general, escribiremos el código para este método de la siguiente manera:
private static int findSquareOfMaxOdd(ListInteger 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 Stream API para lograrlo y aprovechar el beneficio de Laziness-seeking. Veamos cómo podemos reescribir este código en forma de programación funcional usando Stream API y expresiones lambda.
public static int findSquareOfMaxOdd(ListInteger numbers) {return numbers.stream().filter(NumberTest::isOdd) //Predicate is functional interface and.filter(NumberTest::isGreaterThan3)// we are using lambdas to initialize it.filter(NumberTest::isLessThan11)// rather than anonymous inner classes.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 dos puntos (::), se introdujo en Java 8 y se utiliza para referencias de métodos . El compilador de Java se encarga de asignar los argumentos al método llamado. Es la forma abreviada de expresiones lambda
i - isGreaterThan3(i)oi - 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// Complex block body with multiple returns() - { 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 belowx - x+1 // Parenthesis optional for single inferred-type case(String s) - s.length() // Single declared-type argument(Thread t) - { t.start(); } // Single declared-type arguments - s.length() // Single inferred-type argumentt - { 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 de métodos y constructores
Una referencia a un método se utiliza para hacer referencia a un método sin invocarlo; una referencia a un constructor se utiliza de forma similar para hacer referencia a un constructor sin crear una nueva instancia de la clase o del tipo de matriz nombrados. Ejemplos de referencias a métodos y constructores:
System::getPropertySystem.out::println"abc"::lengthArrayList::newint[]::new
Eso es todo por lo que respecta al tutorial de expresiones Lambda e interfaces funcionales de Java 8. Recomiendo encarecidamente que se familiarice con su uso, ya que esta sintaxis es nueva en Java y le llevará algún tiempo comprenderla. También debería consultar las características de Java 8 para conocer todas las mejoras y los cambios en la versión de Java 8.