JavaRush /Blog Java /Random-ES /Interfaces funcionales en Java

Interfaces funcionales en Java

Publicado en el grupo Random-ES
¡Hola! En la búsqueda de Java Syntax Pro, estudiamos expresiones lambda y dijimos que no son más que una implementación de un método funcional desde una interfaz funcional. En otras palabras, se trata de la implementación de alguna clase anónima (desconocida), su método no realizado. Y si en las conferencias del curso profundizamos en las manipulaciones con expresiones lambda, ahora consideraremos, por así decirlo, el otro lado: es decir, estas mismas interfaces. Interfaces funcionales en Java - 1La octava versión de Java introdujo el concepto de interfaces funcionales . ¿Qué es esto? Una interfaz con un método (abstracto) no implementado se considera funcional. Muchas interfaces listas para usar se incluyen en esta definición, como, por ejemplo, la interfaz analizada anteriormente Comparator. Y también interfaces que creamos nosotros mismos, como por ejemplo:
@FunctionalInterface
public interface Converter<T, N> {
   N convert(T t);
}
Tenemos una interfaz cuya tarea es convertir objetos de un tipo en objetos de otro (una especie de adaptador). La anotación @FunctionalInterfaceno es algo súper complejo o importante, ya que su propósito es decirle al compilador que esta interfaz es funcional y no debe contener más de un método. Si una interfaz con esta anotación tiene más de un método (abstracto) no implementado, el compilador no omitirá esta interfaz, ya que la percibirá como código erróneo. Las interfaces sin esta anotación pueden considerarse funcionales y funcionarán, pero @FunctionalInterfaceesto no es más que un seguro adicional. Volvamos a clase Comparator. Si observa su código (o documentación ), puede ver que tiene muchos más de un método. Entonces te preguntas: ¿cómo, entonces, se puede considerar una interfaz funcional? Las interfaces abstractas pueden tener métodos que no están dentro del alcance de un único método:
  • estático
El concepto de interfaces implica que una determinada unidad de código no puede tener ningún método implementado. Pero a partir de Java 8, fue posible utilizar métodos estáticos y predeterminados en las interfaces. Los métodos estáticos están vinculados directamente a una clase y no requieren que un objeto específico de esa clase llame a dicho método. Es decir, estos métodos encajan armoniosamente en el concepto de interfaces. Como ejemplo, agreguemos un método estático para verificar si un objeto es nulo a la clase anterior:
@FunctionalInterface
public interface Converter<T, N> {

   N convert(T t);

   static <T> boolean isNotNull(T t){
       return t != null;
   }
}
Habiendo recibido este método, el compilador no se quejó, lo que significa que nuestra interfaz sigue funcionando.
  • métodos predeterminados
Antes de Java 8, si necesitábamos crear un método en una interfaz heredada por otras clases, solo podíamos crear un método abstracto que se implementara en cada clase específica. ¿Pero qué pasa si este método es el mismo para todas las clases? En este caso , las clases abstractas fueron las más utilizadas . Pero a partir de Java 8, existe una opción para utilizar interfaces con métodos implementados: los métodos predeterminados. Al heredar una interfaz, puede anular estos métodos o dejar todo como está (dejar la lógica predeterminada). Al crear un método predeterminado, debemos agregar la palabra clave - default:
@FunctionalInterface
public interface Converter<T, N> {

   N convert(T t);

   static <T> boolean isNotNull(T t){
       return t != null;
   }

   default void writeToConsole(T t) {
       System.out.println("Текущий un objeto - " + t.toString());
   }
}
Nuevamente, vemos que el compilador no comenzó a quejarse y no fuimos más allá de las limitaciones de la interfaz funcional.
  • Métodos de clase de objeto
En la conferencia Comparación de objetos , hablamos sobre el hecho de que todas las clases heredan de la clase Object. Esto no se aplica a las interfaces. Pero si tenemos un método abstracto en la interfaz que coincide con la firma con algún método de la clase Object, dicho método (o métodos) no romperá nuestra restricción funcional de la interfaz:
@FunctionalInterface
public interface Converter<T, N> {

   N convert(T t);

   static <T> boolean isNotNull(T t){
       return t != null;
   }

   default void writeToConsole(T t) {
       System.out.println("Текущий un objeto - " + t.toString());
   }

   boolean equals(Object obj);
}
Y nuevamente, nuestro compilador no se queja, por lo que la interfaz Converteraún se considera funcional. Ahora la pregunta es: ¿por qué debemos limitarnos a un método no implementado en una interfaz funcional? Y luego para que podamos implementarlo usando lambdas. Veamos esto con un ejemplo Converter. Para hacer esto, creemos una clase Dog:
public class Dog {
  String name;
  int age;
  int weight;

  public Dog(final String name, final int age, final int weight) {
     this.name = name;
     this.age = age;
     this.weight = weight;
  }
}
Y uno parecido Raccoon(mapache):
public class Raccoon {
  String name;
  int age;
  int weight;

  public Raccoon(final String name, final int age, final int weight) {
     this.name = name;
     this.age = age;
     this.weight = weight;
  }
}
Supongamos que tenemos un objeto Dogy necesitamos crear un objeto basado en sus campos Raccoon. Es decir, Converterconvierte un objeto de un tipo en otro. Cómo se verá:
public static void main(String[] args) {
  Dog dog = new Dog("Bobbie", 5, 3);

  Converter<Dog, Raccoon> converter = x -> new Raccoon(x.name, x.age, x.weight);

  Raccoon raccoon = converter.convert(dog);

  System.out.println("Raccoon has parameters: name - " + raccoon.name + ", age - " + raccoon.age + ", weight - " + raccoon.weight);
}
Cuando lo ejecutamos, obtenemos el siguiente resultado en la consola:

Raccoon has parameters: name - Bobbbie, age - 5, weight - 3
Y esto significa que nuestro método funcionó correctamente.Interfaces funcionales en Java - 2

Interfaces funcionales básicas de Java 8

Bueno, ahora veamos varias interfaces funcionales que nos trajo Java 8 y que se utilizan activamente junto con Stream API.

Predicado

Predicate— una interfaz funcional para comprobar si se cumple una determinada condición. Si se cumple la condición, devuelve true; en caso contrario - false:
@FunctionalInterface
public interface Predicate<T> {
   boolean test(T t);
}
Como ejemplo, considere crear un método Predicateque verifique la paridad de un número de tipo Integer:
public static void main(String[] args) {
   Predicate<Integer> isEvenNumber = x -> x % 2==0;

   System.out.println(isEvenNumber.test(4));
   System.out.println(isEvenNumber.test(3));
}
Salida de consola:

true
false

Consumidor

Consumer(del inglés - "consumidor"): una interfaz funcional que toma un objeto de tipo T como argumento de entrada, realiza algunas acciones, pero no devuelve nada:
@FunctionalInterface
public interface Consumer<T> {
   void accept(T t);
}
Como ejemplo, considere , cuya tarea es enviar un saludo a la consola con el argumento de cadena pasado: Consumer
public static void main(String[] args) {
   Consumer<String> greetings = x -> System.out.println("Hello " + x + " !!!");
   greetings.accept("Elena");
}
Salida de consola:

Hello Elena !!!

Proveedor

Supplier(del inglés - proveedor): una interfaz funcional que no acepta ningún argumento, pero devuelve un objeto de tipo T:
@FunctionalInterface
public interface Supplier<T> {
   T get();
}
Como ejemplo, considere Supplier, que producirá nombres aleatorios a partir de una lista:
public static void main(String[] args) {
   ArrayList<String> nameList = new ArrayList<>();
   nameList .add("Elena");
   nameList .add("John");
   nameList .add("Alex");
   nameList .add("Jim");
   nameList .add("Sara");

   Supplier<String> randomName = () -> {
       int value = (int)(Math.random() * nameList.size());
       return nameList.get(value);
   };

   System.out.println(randomName.get());
}
Y si ejecutamos esto, veremos resultados aleatorios de una lista de nombres en la consola.

Función

Function— esta interfaz funcional toma un argumento T y lo convierte en un objeto de tipo R, que se devuelve como resultado:
@FunctionalInterface
public interface Function<T, R> {
   R apply(T t);
}
Como ejemplo, tomemos , que convierte números del formato de cadena ( ) al formato de número ( ): FunctionStringInteger
public static void main(String[] args) {
   Function<String, Integer> valueConverter = x -> Integer.valueOf(x);
   System.out.println(valueConverter.apply("678"));
}
Cuando lo ejecutamos, obtenemos el siguiente resultado en la consola:

678
PD: si pasamos no solo números, sino también otros caracteres a la cadena, se generará una excepción : NumberFormatException.

Operador unario

UnaryOperator— una interfaz funcional que toma un objeto de tipo T como parámetro, realiza algunas operaciones sobre él y devuelve el resultado de las operaciones en forma de un objeto del mismo tipo T:
@FunctionalInterface
public interface UnaryOperator<T> {
   T apply(T t);
}
UnaryOperator, que utiliza su método applypara elevar al cuadrado un número:
public static void main(String[] args) {
   UnaryOperator<Integer> squareValue = x -> x * x;
   System.out.println(squareValue.apply(9));
}
Salida de consola:

81
Analizamos cinco interfaces funcionales. Esto no es todo lo que tenemos disponible a partir de Java 8: estas son las interfaces principales. El resto de los disponibles son sus complicados análogos. La lista completa se puede encontrar en la documentación oficial de Oracle .

Interfaces funcionales en Stream

Como se mencionó anteriormente, estas interfaces funcionales están estrechamente vinculadas con la API Stream. ¿Cómo, preguntas? Interfaces funcionales en Java - 3Y de tal manera que muchos métodos Streamfuncionan específicamente con estas interfaces funcionales. Veamos cómo se pueden utilizar las interfaces funcionales en Stream.

Método con predicado

Por ejemplo, tomemos el método de clase Stream, filterque toma como argumento Predicatey devuelve Streamsolo aquellos elementos que cumplen la condición Predicate. En el contexto de Stream-a, esto significa que solo pasa por aquellos elementos que se devuelven truecuando se usan en un método testde interfaz Predicate. Así es como se vería nuestro ejemplo Predicate, pero para un filtro de elementos en Stream:
public static void main(String[] args) {
   List<Integer> evenNumbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8)
           .filter(x -> x % 2==0)
           .collect(Collectors.toList());
}
Como resultado, la lista evenNumbersconstará de los elementos {2, 4, 6, 8}. Y, como recordamos, collectrecopilará todos los elementos en una determinada colección: en nuestro caso, en List.

Método con el consumidor

Uno de los métodos en Stream, que utiliza la interfaz funcional Consumer, es peek. Así es como se verá nuestro ejemplo Consumeren Stream:
public static void main(String[] args) {
   List<String> peopleGreetings = Stream.of("Elena", "John", "Alex", "Jim", "Sara")
           .peek(x -> System.out.println("Hello " + x + " !!!"))
           .collect(Collectors.toList());
}
Salida de consola:

Hello Elena !!!
Hello John !!!
Hello Alex !!!
Hello Jim !!!
Hello Sara !!!
Pero como el método peekfunciona con Consumer, la modificación de las cadenas en Streamno ocurrirá, sino que peekregresará Streamcon los elementos originales: los mismos que llegaron a él. Por tanto, la lista peopleGreetingsestará formada por los elementos "Elena", "John", "Alex", "Jim", "Sara". También existe un método de uso común foreach, que es similar al método peek, pero la diferencia es que es final-terminal.

Método con Proveedor

Un ejemplo de un método Streamque utiliza la interfaz funcional Supplieres generate, que genera una secuencia infinita basada en la interfaz funcional que se le pasa. Usemos nuestro ejemplo Supplierpara imprimir cinco nombres aleatorios en la consola:
public static void main(String[] args) {
   ArrayList<String> nameList = new ArrayList<>();
   nameList.add("Elena");
   nameList.add("John");
   nameList.add("Alex");
   nameList.add("Jim");
   nameList.add("Sara");

   Stream.generate(() -> {
       int value = (int) (Math.random() * nameList.size());
       return nameList.get(value);
   }).limit(5).forEach(System.out::println);
}
Y este es el resultado que obtenemos en la consola:

John
Elena
Elena
Elena
Jim
Aquí usamos el método limit(5)para establecer un límite en el método generate; de lo contrario, el programa imprimiría nombres aleatorios en la consola de forma indefinida.

Método con función

Un ejemplo típico de un método con Streamun argumento Functiones un método mapque toma elementos de un tipo, hace algo con ellos y los pasa, pero estos ya pueden ser elementos de otro tipo. Cómo se vería un ejemplo con Functionin Stream:
public static void main(String[] args) {
   List<Integer> values = Stream.of("32", "43", "74", "54", "3")
           .map(x -> Integer.valueOf(x)).collect(Collectors.toList());
}
Como resultado, obtenemos una lista de números, pero en Integer.

Método con OperadorUnario

Como método que se utiliza UnaryOperatorcomo argumento, tomemos un método de clase Stream: iterate. Este método es similar al método generate: también genera una secuencia infinita pero tiene dos argumentos:
  • el primero es el elemento a partir del cual comienza la generación de la secuencia;
  • el segundo es UnaryOperator, que indica el principio de generar nuevos elementos a partir del primer elemento.
Así es como se verá nuestro ejemplo UnaryOperator, pero en el método iterate:
public static void main(String[] args) {
   Stream.iterate(9, x -> x * x)
           .limit(4)
           .forEach(System.out::println);
}
Cuando lo ejecutamos, obtenemos el siguiente resultado en la consola:

9
81
6561
43046721
Es decir, cada uno de nuestros elementos se multiplica por sí mismo, y así sucesivamente con los primeros cuatro números. Interfaces funcionales en Java - 4¡Eso es todo! ¡Sería fantástico si después de leer este artículo estuvieras un paso más cerca de comprender y dominar la API Stream en Java!
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION