1. Introducción
Java 8 introdujo la programación de estilo funcional, lo que nos permitió parametrizar métodos de propósito general pasando funciones.
Probablemente estemos más familiarizados con las interfaces funcionales de Java 8 de un solo parámetro como Function , Predicate y Consumer .
En este tutorial, veremos interfaces funcionales que usan dos parámetros . Estas funciones se denominan funciones binarias y se representan en Java con la interfaz funcional BiFunction .
2. Funciones de un solo parámetro
Recapitulemos rápidamente cómo usamos una función unaria o de un solo parámetro, como lo hacemos en los flujos:
List mapped = Stream.of("hello", "world") .map(word -> word + "!") .collect(Collectors.toList()); assertThat(mapped).containsExactly("hello!", "world!");
Como podemos ver, el mapa usa Function , que toma un solo parámetro y nos permite realizar una operación sobre ese valor, devolviendo un nuevo valor.
3. Operaciones de dos parámetros
La biblioteca Java Stream nos proporciona una función de reducción que nos permite combinar los elementos de una secuencia . Necesitamos expresar cómo se transforman los valores que hemos acumulado hasta ahora agregando el siguiente elemento.
La función de reducción utiliza la interfaz funcional BinaryOperator , que toma dos objetos del mismo tipo como entradas.
Imaginemos que queremos unir todos los elementos de nuestra secuencia colocando los nuevos al frente con un separador de guiones. Veremos algunas formas de implementar esto en las siguientes secciones.
3.1. Usando una Lambda
La implementación de una lambda para una BiFunction tiene como prefijo dos parámetros, rodeados de corchetes:
String result = Stream.of("hello", "world") .reduce("", (a, b) -> b + "-" + a); assertThat(result).isEqualTo("world-hello-");
Como podemos ver, los dos valores, un y b son cadenas . Hemos escrito una lambda que los combina para producir la salida deseada, con la segunda en primer lugar y un guión en el medio.
Debemos tener en cuenta que reduce usa un valor inicial, en este caso, la cadena vacía. Por lo tanto, terminamos con un guión final con el código anterior, ya que el primer valor de nuestra secuencia se une a él.
Además, debemos tener en cuenta que la inferencia de tipos de Java nos permite omitir los tipos de nuestros parámetros la mayor parte del tiempo. En situaciones donde el tipo de una lambda no está claro en el contexto, podemos usar tipos para nuestros parámetros:
String result = Stream.of("hello", "world") .reduce("", (String a, String b) -> b + "-" + a);
3.2. Usando una función
¿Qué pasaría si quisiéramos que el algoritmo anterior no pusiera un guión al final? Podríamos escribir más código en nuestra lambda, pero eso podría complicarse. En su lugar, extraigamos una función:
private String combineWithoutTrailingDash(String a, String b) { if (a.isEmpty()) { return b; } return b + "-" + a; }
Y luego llámalo:
String result = Stream.of("hello", "world") .reduce("", (a, b) -> combineWithoutTrailingDash(a, b)); assertThat(result).isEqualTo("world-hello");
Como podemos ver, el lambda llama a nuestra función, que es más fácil de leer que poner la implementación más compleja en línea.
3.3. Usar una referencia de método
Algunos IDE nos pedirán automáticamente que convierta el lambda anterior en una referencia de método, ya que a menudo es más claro de leer.
Reescribamos nuestro código para usar una referencia de método:
String result = Stream.of("hello", "world") .reduce("", this::combineWithoutTrailingDash); assertThat(result).isEqualTo("world-hello");
Las referencias a métodos a menudo hacen que el código funcional sea más autoexplicativo.
4. Uso de BiFunction
Hasta ahora, hemos demostrado cómo usar funciones donde ambos parámetros son del mismo tipo. La interfaz BiFunction nos permite utilizar parámetros de diferentes tipos , con un valor de retorno de un tercer tipo.
Imaginemos que estamos creando un algoritmo para combinar dos listas de igual tamaño en una tercera lista realizando una operación en cada par de elementos:
List list1 = Arrays.asList("a", "b", "c"); List list2 = Arrays.asList(1, 2, 3); List result = new ArrayList(); for (int i=0; i < list1.size(); i++) { result.add(list1.get(i) + list2.get(i)); } assertThat(result).containsExactly("a1", "b2", "c3");
4.1. Generalizar la función
Podemos generalizar esta función especializada usando una BiFunction como combinador:
private static List listCombiner( List list1, List list2, BiFunction combiner) { List result = new ArrayList(); for (int i = 0; i < list1.size(); i++) { result.add(combiner.apply(list1.get(i), list2.get(i))); } return result; }
Veamos qué está pasando aquí. Hay tres tipos de parámetros: T para el tipo de elemento en la primera lista, U para el tipo en la segunda lista y luego R para cualquier tipo que devuelva la función de combinación.
Usamos la BiFunction proporcionada a esta función llamando a su método de aplicación para obtener el resultado.
4.2. Llamar a la función generalizada
Nuestro combinador es una BiFunction , que nos permite inyectar un algoritmo, sean cuales sean los tipos de entrada y salida. Probémoslo:
List list1 = Arrays.asList("a", "b", "c"); List list2 = Arrays.asList(1, 2, 3); List result = listCombiner(list1, list2, (a, b) -> a + b); assertThat(result).containsExactly("a1", "b2", "c3");
Y también podemos usar esto para tipos completamente diferentes de entradas y salidas.
Inyectemos un algoritmo para determinar si el valor en la primera lista es mayor que el valor en la segunda y produzcamos un resultado booleano :
List list1 = Arrays.asList(1.0d, 2.1d, 3.3d); List list2 = Arrays.asList(0.1f, 0.2f, 4f); List result = listCombiner(list1, list2, (a, b) -> a > b); assertThat(result).containsExactly(true, true, false);
4.3. Una referencia de método BiFunction
Reescribamos el código anterior con un método extraído y una referencia de método:
List list1 = Arrays.asList(1.0d, 2.1d, 3.3d); List list2 = Arrays.asList(0.1f, 0.2f, 4f); List result = listCombiner(list1, list2, this::firstIsGreaterThanSecond); assertThat(result).containsExactly(true, true, false); private boolean firstIsGreaterThanSecond(Double a, Float b) { return a > b; }
We should note that this makes the code a little easier to read, as the method firstIsGreaterThanSecond describes the algorithm injected as a method reference.
4.4. BiFunction Method References Using this
Let's imagine we want to use the above BiFunction-based algorithm to determine if two lists are equal:
List list1 = Arrays.asList(0.1f, 0.2f, 4f); List list2 = Arrays.asList(0.1f, 0.2f, 4f); List result = listCombiner(list1, list2, (a, b) -> a.equals(b)); assertThat(result).containsExactly(true, true, true);
We can actually simplify the solution:
List result = listCombiner(list1, list2, Float::equals);
This is because the equals function in Float has the same signature as a BiFunction. It takes an implicit first parameter of this, an object of type Float. The second parameter, other, of type Object, is the value to compare.
5. Composing BiFunctions
What if we could use method references to do the same thing as our numeric list comparison example?
List list1 = Arrays.asList(1.0d, 2.1d, 3.3d); List list2 = Arrays.asList(0.1d, 0.2d, 4d); List result = listCombiner(list1, list2, Double::compareTo); assertThat(result).containsExactly(1, 1, -1);
This is close to our example but returns an Integer, rather than the original Boolean. This is because the compareTo method in Double returns Integer.
We can add the extra behavior we need to achieve our original by using andThen to compose a function. This produces a BiFunction that first does one thing with the two inputs and then performs another operation.
Next, let's create a function to coerce our method reference Double::compareTo into a BiFunction:
private static BiFunction asBiFunction(BiFunction function) { return function; }
A lambda or method reference only becomes a BiFunction after it has been converted by a method invocation. We can use this helper function to convert our lambda into the BiFunction object explicitly.
Now, we can use andThen to add behavior on top of the first function:
List list1 = Arrays.asList(1.0d, 2.1d, 3.3d); List list2 = Arrays.asList(0.1d, 0.2d, 4d); List result = listCombiner(list1, list2, asBiFunction(Double::compareTo).andThen(i -> i > 0)); assertThat(result).containsExactly(true, true, false);
6. Conclusion
En este tutorial, hemos explorado BiFunction y BinaryOperator en términos de la biblioteca Java Streams proporcionada y nuestras propias funciones personalizadas. Hemos visto cómo pasar BiFunctions usando lambdas y referencias de métodos, y hemos visto cómo componer funciones.
Las bibliotecas de Java solo proporcionan interfaces funcionales de uno y dos parámetros. Para situaciones que requieren más parámetros, consulte nuestro artículo sobre preparación para obtener más ideas.
Como siempre, las muestras de código completas están disponibles en GitHub.