¡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. La 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 Predicado
Consumidor
Proveedor
Función
Operador unario
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 @FunctionalInterface
no 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 @FunctionalInterface
esto 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
@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
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
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 Converter
aú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 Dog
y necesitamos crear un objeto basado en sus campos Raccoon
. Es decir, Converter
convierte 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 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 Predicate
que 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 ( ): Function
String
Integer
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 apply
para 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? Y de tal manera que muchos métodosStream
funcionan 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 claseStream
, filter
que toma como argumento Predicate
y devuelve Stream
solo 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 true
cuando se usan en un método test
de 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 evenNumbers
constará de los elementos {2, 4, 6, 8}. Y, como recordamos, collect
recopilará todos los elementos en una determinada colección: en nuestro caso, en List
.
Método con el consumidor
Uno de los métodos enStream
, que utiliza la interfaz funcional Consumer
, es peek
. Así es como se verá nuestro ejemplo Consumer
en 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 peek
funciona con Consumer
, la modificación de las cadenas en Stream
no ocurrirá, sino que peek
regresará Stream
con los elementos originales: los mismos que llegaron a él. Por tanto, la lista peopleGreetings
estará 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étodoStream
que utiliza la interfaz funcional Supplier
es generate
, que genera una secuencia infinita basada en la interfaz funcional que se le pasa. Usemos nuestro ejemplo Supplier
para 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 conStream
un argumento Function
es un método map
que 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 Function
in 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 utilizaUnaryOperator
como 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.
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. ¡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!
GO TO FULL VERSION