Guida all'interfaccia Java BiFunction

1. Introduzione

Java 8 ha introdotto la programmazione in stile funzionale, permettendoci di parametrizzare metodi generici passando le funzioni.

Probabilmente abbiamo più familiarità con le interfacce funzionali Java 8 a parametro singolo come Function , Predicate e Consumer .

In questo tutorial, esamineremo le interfacce funzionali che utilizzano due parametri . Tali funzioni sono chiamate funzioni binarie e sono rappresentate in Java con l' interfaccia funzionale BiFunction .

2. Funzioni a parametro singolo

Ricapitoliamo rapidamente come utilizziamo una funzione a parametro singolo o unario, come facciamo negli stream:

List mapped = Stream.of("hello", "world") .map(word -> word + "!") .collect(Collectors.toList()); assertThat(mapped).containsExactly("hello!", "world!");

Come possiamo vedere, la mappa utilizza Function , che prende un singolo parametro e ci permette di eseguire un'operazione su quel valore, restituendo un nuovo valore.

3. Operazioni a due parametri

La libreria Java Stream ci fornisce una funzione di riduzione che ci permette di combinare gli elementi di uno stream . Dobbiamo esprimere come si trasformano i valori che abbiamo accumulato finora aggiungendo l'elemento successivo.

La funzione reduce utilizza l'interfaccia funzionale BinaryOperator , che accetta due oggetti dello stesso tipo come suoi input.

Immaginiamo di voler unire tutti gli elementi nel nostro stream inserendo quelli nuovi in ​​primo piano con un separatore di trattini. Daremo uno sguardo ad alcuni modi per implementarlo nelle sezioni seguenti.

3.1. Utilizzando un Lambda

L'implementazione di un lambda per una BiFunction è preceduta da due parametri, racchiusi tra parentesi:

String result = Stream.of("hello", "world") .reduce("", (a, b) -> b + "-" + a); assertThat(result).isEqualTo("world-hello-");

Come possiamo vedere, i due valori, un e b sono le stringhe . Abbiamo scritto un lambda che li combina per ottenere l'output desiderato, con il secondo per primo e un trattino in mezzo.

Dobbiamo notare che reduce usa un valore iniziale - in questo caso, la stringa vuota. Quindi, finiamo con un trattino finale con il codice sopra, poiché il primo valore del nostro flusso è unito ad esso.

Inoltre, dovremmo notare che l'inferenza del tipo di Java ci consente di omettere i tipi dei nostri parametri la maggior parte delle volte. In situazioni in cui il tipo di lambda non è chiaro dal contesto, possiamo utilizzare i tipi per i nostri parametri:

String result = Stream.of("hello", "world") .reduce("", (String a, String b) -> b + "-" + a);

3.2. Utilizzo di una funzione

E se volessimo fare in modo che l'algoritmo di cui sopra non mettesse un trattino alla fine? Potremmo scrivere più codice nel nostro lambda, ma potrebbe diventare complicato. Estraiamo invece una funzione:

private String combineWithoutTrailingDash(String a, String b) { if (a.isEmpty()) { return b; } return b + "-" + a; }

E poi chiamalo:

String result = Stream.of("hello", "world") .reduce("", (a, b) -> combineWithoutTrailingDash(a, b)); assertThat(result).isEqualTo("world-hello");

Come possiamo vedere, lambda chiama la nostra funzione, che è più facile da leggere che mettere in linea l'implementazione più complessa.

3.3. Utilizzo di un metodo di riferimento

Alcuni IDE ci chiederanno automaticamente di convertire il lambda sopra in un riferimento al metodo, poiché spesso è più chiaro da leggere.

Riscriviamo il nostro codice per utilizzare un metodo di riferimento:

String result = Stream.of("hello", "world") .reduce("", this::combineWithoutTrailingDash); assertThat(result).isEqualTo("world-hello");

I riferimenti ai metodi spesso rendono il codice funzionale più autoesplicativo.

4. Utilizzo di BiFunction

Finora, abbiamo dimostrato come utilizzare le funzioni in cui entrambi i parametri sono dello stesso tipo. L' interfaccia BiFunction ci permette di utilizzare parametri di diverso tipo , con un valore di ritorno di un terzo tipo.

Immaginiamo di creare un algoritmo per combinare due elenchi di uguale dimensione in un terzo elenco eseguendo un'operazione su ciascuna coppia di elementi:

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. Generalizza la funzione

Possiamo generalizzare questa funzione specializzata utilizzando una BiFunction come combinatore:

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; }

Vediamo cosa sta succedendo qui. Esistono tre tipi di parametri: T per il tipo di elemento nel primo elenco, U per il tipo nel secondo elenco e quindi R per qualsiasi tipo restituito dalla funzione di combinazione.

Usiamo la BiFunction fornita a questa funzione chiamando il suo metodo apply per ottenere il risultato.

4.2. Chiamata alla funzione generalizzata

Il nostro combinatore è una BiFunction , che ci permette di iniettare un algoritmo, qualunque sia il tipo di input e output. Proviamolo:

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");

E possiamo usarlo anche per tipi completamente diversi di input e output.

Iniettiamo un algoritmo per determinare se il valore nella prima lista è maggiore del valore nella seconda e produciamo un risultato 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. Un riferimento al metodo BiFunction

Riscriviamo il codice precedente con un metodo estratto e un metodo di riferimento:

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

In questo tutorial, abbiamo esplorato BiFunction e BinaryOperator in termini della libreria Java Streams fornita e delle nostre funzioni personalizzate. Abbiamo esaminato come passare BiFunctions usando lambda e riferimenti a metodi e abbiamo visto come comporre funzioni.

Le librerie Java forniscono solo interfacce funzionali a uno e due parametri. Per le situazioni che richiedono più parametri, consulta il nostro articolo sul curry per ulteriori idee.

Come sempre, gli esempi di codice completi sono disponibili su GitHub.