Anleitung zur Java BiFunction-Schnittstelle

1. Einleitung

Java 8 führte die Programmierung des Funktionsstils ein, mit der wir Allzweckmethoden durch Übergabe von Funktionen parametrisieren können.

Wir sind wahrscheinlich am besten mit den Java 8-Funktionsschnittstellen mit nur einem Parameter wie Function , Predicate und Consumer vertraut .

In diesem Tutorial werden wir uns funktionale Schnittstellen ansehen, die zwei Parameter verwenden . Solche Funktionen werden als Binärfunktionen bezeichnet und in Java mit der BiFunction- Funktionsschnittstelle dargestellt.

2. Einzelparameterfunktionen

Lassen Sie uns kurz zusammenfassen, wie wir eine einzelne Parameter- oder unäre Funktion verwenden, wie wir es in Streams tun:

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

Wie wir sehen können, verwendet die Karte die Funktion , die einen einzelnen Parameter verwendet und es uns ermöglicht, eine Operation für diesen Wert auszuführen und einen neuen Wert zurückzugeben.

3. Zwei-Parameter-Operationen

Die Java Stream-Bibliothek bietet uns eine Reduktionsfunktion , mit der wir die Elemente eines Streams kombinieren können . Wir müssen ausdrücken, wie die Werte, die wir bisher gesammelt haben, durch Hinzufügen des nächsten Elements transformiert werden.

Die Reduktionsfunktion verwendet die Funktionsschnittstelle BinaryOperator , die zwei Objekte des gleichen Typs als Eingaben verwendet.

Stellen wir uns vor, wir möchten alle Elemente in unserem Stream verbinden, indem wir die neuen mit einem Strich-Trennzeichen vorne platzieren. In den folgenden Abschnitten werden einige Möglichkeiten zur Implementierung beschrieben.

3.1. Mit einem Lambda

Der Implementierung eines Lambda für eine BiFunktion werden zwei Parameter vorangestellt, die in Klammern stehen:

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

Wie wir sehen können, sind die beiden Werte, a und b sind Strings . Wir haben ein Lambda geschrieben, das sie kombiniert, um die gewünschte Ausgabe zu erzielen, mit der zweiten zuerst und einem Strich dazwischen.

Wir sollten beachten, dass reduct einen Startwert verwendet - in diesem Fall die leere Zeichenfolge. Daher erhalten wir einen abschließenden Strich mit dem obigen Code, da der erste Wert aus unserem Stream damit verbunden wird.

Wir sollten auch beachten, dass die Typinferenz von Java es uns ermöglicht, die Typen unserer Parameter die meiste Zeit wegzulassen. In Situationen, in denen der Typ eines Lambda nicht aus dem Kontext hervorgeht, können wir Typen für unsere Parameter verwenden:

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

3.2. Verwenden einer Funktion

Was wäre, wenn wir den obigen Algorithmus dazu bringen wollten, am Ende keinen Strich zu setzen? Wir könnten mehr Code in unser Lambda schreiben, aber das könnte chaotisch werden. Lassen Sie uns stattdessen eine Funktion extrahieren:

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

Und dann nenne es:

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

Wie wir sehen können, ruft das Lambda unsere Funktion auf, die leichter zu lesen ist, als die komplexere Implementierung inline zu setzen.

3.3. Verwenden einer Methodenreferenz

Einige IDEs fordern uns automatisch auf, das obige Lambda in eine Methodenreferenz umzuwandeln, da das Lesen häufig klarer ist.

Schreiben wir unseren Code neu, um eine Methodenreferenz zu verwenden:

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

Methodenreferenzen machen den Funktionscode häufig selbsterklärender.

4. Verwenden von BiFunction

Bisher haben wir gezeigt, wie Funktionen verwendet werden, bei denen beide Parameter vom gleichen Typ sind. Über die BiFunction- Schnittstelle können Parameter verschiedener Typen mit einem Rückgabewert eines dritten Typs verwendet werden.

Stellen wir uns vor, wir erstellen einen Algorithmus, um zwei gleich große Listen zu einer dritten Liste zu kombinieren, indem wir für jedes Elementpaar eine Operation ausführen:

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. Verallgemeinern Sie die Funktion

Wir können diese spezielle Funktion mit einer BiFunktion als Kombinierer verallgemeinern :

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

Mal sehen, was hier los ist. Es gibt drei Arten von Parametern: T für den Elementtyp in der ersten Liste, U für den Typ in der zweiten Liste und R für den Typ, den die Kombinationsfunktion zurückgibt.

Wir verwenden die bifunktionellen auf diese Funktion zur Verfügung gestellt durch den Aufruf seiner Anwendung Methode das Ergebnis zu erhalten.

4.2. Aufruf der Generalized Function

Unser Kombinierer ist eine BiFunktion , mit der wir einen Algorithmus unabhängig von der Art der Eingabe und Ausgabe einfügen können. Probieren wir es aus:

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

Und wir können dies auch für völlig unterschiedliche Arten von Ein- und Ausgängen verwenden.

Lassen Sie uns einen Algorithmus einfügen, um festzustellen, ob der Wert in der ersten Liste größer als der Wert in der zweiten ist, und ein boolesches Ergebnis erzeugen :

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. Eine BiFunction Method Reference

Schreiben wir den obigen Code mit einer extrahierten Methode und einer Methodenreferenz neu:

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 diesem Tutorial haben wir BiFunction und BinaryOperator im Hinblick auf die bereitgestellte Java Streams-Bibliothek und unsere eigenen benutzerdefinierten Funktionen untersucht. Wir haben uns angesehen, wie BiFunctions mithilfe von Lambdas und Methodenreferenzen übergeben werden, und wir haben gesehen, wie Funktionen erstellt werden.

Die Java-Bibliotheken bieten nur Funktionsschnittstellen mit einem oder zwei Parametern. Weitere Ideen zu Situationen, in denen mehr Parameter erforderlich sind, finden Sie in unserem Artikel zum Curry.

Wie immer sind die vollständigen Codebeispiele auf GitHub verfügbar.