Funktionale Schnittstellen in Java 8

Funktionsschnittstellen in Java 8

1. Einführung

Dieser Artikel enthält eine Anleitung zu verschiedenen Funktionsschnittstellen in Java 8, ihren allgemeinen Anwendungsfällen und ihrer Verwendung in der Standard-JDK-Bibliothek.

Weitere Lektüre:

Iterierbar zum Streaming in Java

In diesem Artikel wird erläutert, wie ein Iterable in Stream konvertiert wird und warum die Iterable-Oberfläche dies nicht direkt unterstützt.

Read more

Verwendung von if / else Logic in Java 8-Streams

Erfahren Sie, wie Sie if / else-Logik auf Java 8-Streams anwenden.

Read more

2. Lambdas in Java 8

Java 8 brachte eine mächtige neue syntaktische Verbesserung in Form von Lambda-Ausdrücken. Ein Lambda ist eine anonyme Funktion, die als erstklassiger Sprachbürger behandelt werden kann, beispielsweise an eine Methode übergeben oder von dieser zurückgegeben werden kann.

Vor Java 8 haben Sie normalerweise eine Klasse für jeden Fall erstellt, in dem Sie eine einzelne Funktionalität kapseln mussten. Dies implizierte eine Menge unnötigen Boilerplate-Codes, um etwas zu definieren, das als primitive Funktionsdarstellung diente.

Lambdas, funktionale Schnittstellen und Best Practices für die Arbeit mit ihnen im Allgemeinen sind im Artikel“Lambda Expressions and Functional Interfaces: Tips and Best Practices” beschrieben. Dieses Handbuch konzentriert sich auf einige bestimmte Funktionsschnittstellen, die im Paketjava.util.functionenthalten sind.

3. Funktionale Schnittstellen

Für alle Funktionsschnittstellen wird eine informative@FunctionalInterface-Anmerkung empfohlen. Dies kommuniziert nicht nur klar den Zweck dieser Schnittstelle, sondern ermöglicht es einem Compiler auch, einen Fehler zu erzeugen, wenn die mit Annotationen versehene Schnittstelle die Bedingungen nicht erfüllt.

Any interface with a SAM(Single Abstract Method) is a functional interface und seine Implementierung können als Lambda-Ausdrücke behandelt werden.

Beachten Sie, dass diedefault-Methoden von Java 8 nichtabstract sind und nicht zählen: Eine funktionale Schnittstelle verfügt möglicherweise noch über mehreredefault-Methoden. Sie können dies beobachten, indem Sie sichFunction’sdocumentation ansehen.

4. Funktionen

Der einfachste und allgemeinste Fall eines Lambda ist eine funktionale Schnittstelle mit einer Methode, die einen Wert empfängt und einen anderen zurückgibt. Diese Funktion eines einzelnen Arguments wird durch dieFunction-Schnittstelle dargestellt, die durch die Typen seines Arguments und einen Rückgabewert parametrisiert wird:

public interface Function { … }

Eine der Verwendungen des TypsFunction in der Standardbibliothek ist die MethodeMap.computeIfAbsent, die einen Wert von einer Karte nach Schlüssel zurückgibt, aber einen Wert berechnet, wenn ein Schlüssel nicht bereits in einer Karte vorhanden ist. Um einen Wert zu berechnen, wird die übergebene Funktionsimplementierung verwendet:

Map nameMap = new HashMap<>();
Integer value = nameMap.computeIfAbsent("John", s -> s.length());

In diesem Fall wird ein Wert berechnet, indem eine Funktion auf eine Taste angewendet, in eine Map eingefügt und auch von einem Methodenaufruf zurückgegeben wird. Übrigenswe may replace the lambda with a method reference that matches passed and returned value types.

Denken Sie daran, dass ein Objekt, für das die Methode aufgerufen wird, tatsächlich das implizite erste Argument einer Methode ist, mit dem der Verweis einer Instanzmethodelengthauf eineFunction-Schnittstelle umgewandelt werden kann:

Integer value = nameMap.computeIfAbsent("John", String::length);

DieFunction-Schnittstelle verfügt auch über eine Standardmethode (compose), mit der mehrere Funktionen zu einer kombiniert und nacheinander ausgeführt werden können:

Function intToString = Object::toString;
Function quote = s -> "'" + s + "'";

Function quoteIntToString = quote.compose(intToString);

assertEquals("'5'", quoteIntToString.apply(5));

Die FunktionquoteIntToString ist eine Kombination der Funktionquote, die auf ein Ergebnis der FunktionintToString angewendet wird.

5. Primitive Funktionsspezialisierungen

Da ein primitiver Typ kein generisches Typargument sein kann, gibt es Versionen derFunction-Schnittstelle für die am häufigsten verwendeten primitiven Typendouble,int,long und deren Kombinationen in Argument- und Rückgabetypen:

  • IntFunction,LongFunction,DoubleFunction: Argumente sind vom angegebenen Typ, der Rückgabetyp ist parametrisiert

  • ToIntFunction,ToLongFunction,ToDoubleFunction: Rückgabetyp ist vom angegebenen Typ, Argumente werden parametrisiert

  • DoubleToIntFunction,DoubleToLongFunction,IntToDoubleFunction,IntToLongFunction,LongToIntFunction,LongToDoubleFunction - wobei sowohl Argument als auch Rückgabetyp als primitive Typen definiert sind, wie durch angegeben ihre Namen

Es gibt keine sofort einsatzbereite Funktionsschnittstelle für beispielsweise eine Funktion, dieshort benötigt undbyte zurückgibt, aber nichts hindert Sie daran, Ihre eigene zu schreiben:

@FunctionalInterface
public interface ShortToByteFunction {

    byte applyAsByte(short s);

}

Jetzt können wir eine Methode schreiben, die ein Array vonshort in ein Array vonbyte transformiert, indem wir eine durchShortToByteFunction definierte Regel verwenden:

public byte[] transformArray(short[] array, ShortToByteFunction function) {
    byte[] transformedArray = new byte[array.length];
    for (int i = 0; i < array.length; i++) {
        transformedArray[i] = function.applyAsByte(array[i]);
    }
    return transformedArray;
}

So können wir ein Array von Shorts in ein Array von Bytes multipliziert mit 2 umwandeln:

short[] array = {(short) 1, (short) 2, (short) 3};
byte[] transformedArray = transformArray(array, s -> (byte) (s * 2));

byte[] expectedArray = {(byte) 2, (byte) 4, (byte) 6};
assertArrayEquals(expectedArray, transformedArray);

6. Spezialisierungen für Zwei-Arien-Funktionen

Um Lambdas mit zwei Argumenten zu definieren, müssen wir zusätzliche Schnittstellen verwenden, deren Namen das Schlüsselwort „Bi” enthalten:BiFunction,ToDoubleBiFunction,ToIntBiFunction undToLongBiFunction .

InBiFunction werden sowohl Argumente als auch ein Rückgabetyp generiert, während inToDoubleBiFunction und anderen ein primitiver Wert zurückgegeben werden kann.

Eines der typischen Beispiele für die Verwendung dieser Schnittstelle in der Standard-API ist die MethodeMap.replaceAll, mit der alle Werte in einer Karte durch einen berechneten Wert ersetzt werden können.

Verwenden wir eineBiFunction-Implementierung, die einen Schlüssel und einen alten Wert erhält, um einen neuen Wert für das Gehalt zu berechnen und zurückzugeben.

Map salaries = new HashMap<>();
salaries.put("John", 40000);
salaries.put("Freddy", 30000);
salaries.put("Samuel", 50000);

salaries.replaceAll((name, oldValue) ->
  name.equals("Freddy") ? oldValue : oldValue + 10000);

7. Lieferanten

Die Funktionsschnittstelle vonSupplierist eine weitere Spezialisierung vonFunction, die keine Argumente akzeptiert. Es wird normalerweise für die verzögerte Generierung von Werten verwendet. Definieren wir beispielsweise eine Funktion, die den Wert vondoublequadriert. Es erhält keinen Wert selbst, sondernSupplier dieses Wertes:

public double squareLazy(Supplier lazyValue) {
    return Math.pow(lazyValue.get(), 2);
}

Auf diese Weise können wir das Argument für den Aufruf dieser Funktion mithilfe der Implementierung vonSupplierträge generieren. Dies kann nützlich sein, wenn die Generierung dieses Arguments viel Zeit in Anspruch nimmt. Wir simulieren dies mit dersleepUninterruptibly-Methode von Guava:

Supplier lazyValue = () -> {
    Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS);
    return 9d;
};

Double valueSquared = squareLazy(lazyValue);

Ein weiterer Anwendungsfall für den Lieferanten ist die Definition einer Logik zur Sequenzgenerierung. Um dies zu demonstrieren, verwenden wir eine statischeStream.generate-Methode, umStream von Fibonacci-Zahlen zu erstellen:

int[] fibs = {0, 1};
Stream fibonacci = Stream.generate(() -> {
    int result = fibs[1];
    int fib3 = fibs[0] + fibs[1];
    fibs[0] = fibs[1];
    fibs[1] = fib3;
    return result;
});

Die Funktion, die an die MethodeStream.generateübergeben wird, implementiert die Funktionsschnittstelle vonSupplier. Beachten Sie, dassSupplier normalerweise einen externen Status benötigen, um als Generator nützlich zu sein. In diesem Fall besteht sein Zustand aus zwei letzten Fibonacci-Folgenummern.

Um diesen Zustand zu implementieren, verwenden wir ein Array anstelle einiger Variablen, daall external variables used inside the lambda have to be effectively final.

Andere Spezialisierungen der Funktionsschnittstelle vonSupplierumfassenBooleanSupplier,DoubleSupplier,LongSupplier undIntSupplier, deren Rückgabetypen entsprechende Grundelemente sind.

8. Verbraucher

Im Gegensatz zuSupplier akzeptiertConsumer ein generiertes Argument und gibt nichts zurück. Es ist eine Funktion, die Nebenwirkungen darstellt.

Begrüßen wir beispielsweise alle Teilnehmer in einer Namensliste, indem Sie die Begrüßung in der Konsole ausdrucken. Das an die MethodeList.forEachübergebene Lambda implementiert die Funktionsschnittstelle vonConsumer:

List names = Arrays.asList("John", "Freddy", "Samuel");
names.forEach(name -> System.out.println("Hello, " + name));

Es gibt auch spezielle Versionen derConsumer -DoubleConsumer,IntConsumer undLongConsumer -, die primitive Werte als Argumente erhalten. Interessanter ist dieBiConsumer-Schnittstelle. Einer seiner Anwendungsfälle ist das Durchlaufen der Einträge einer Karte:

Map ages = new HashMap<>();
ages.put("John", 25);
ages.put("Freddy", 24);
ages.put("Samuel", 30);

ages.forEach((name, age) -> System.out.println(name + " is " + age + " years old"));

Ein anderer Satz spezialisierterBiConsumer-Versionen besteht ausObjDoubleConsumer,ObjIntConsumer undObjLongConsumer, die zwei Argumente erhalten, von denen eines generiert wird und eines ein primitiver Typ ist.

9. Prädikate

In der mathematischen Logik ist ein Prädikat eine Funktion, die einen Wert empfängt und einen Booleschen Wert zurückgibt.

Die Funktionsschnittstelle vonPredicateist eine Spezialisierung vonFunction, die einen generierten Wert empfängt und einen Booleschen Wert zurückgibt. Ein typischer Anwendungsfall für das LambdaPredicate ist das Filtern einer Sammlung von Werten:

List names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");

List namesWithA = names.stream()
  .filter(name -> name.startsWith("A"))
  .collect(Collectors.toList());

Im obigen Code filtern wir eine Liste mithilfe derStream-API und behalten nur Namen bei, die mit dem Buchstaben "A" beginnen. Die Filterlogik ist in der Implementierung vonPredicategekapselt.

Wie in allen vorherigen Beispielen gibt es Versionen dieser FunktionIntPredicate,DoublePredicate undLongPredicate, die primitive Werte erhalten.

10. Betreiber

Operator Schnittstellen sind Sonderfälle einer Funktion, die denselben Werttyp empfangen und zurückgeben. DieUnaryOperator-Schnittstelle erhält ein einzelnes Argument. Einer seiner Anwendungsfälle in der Collections-API besteht darin, alle Werte in einer Liste durch einige berechnete Werte desselben Typs zu ersetzen:

List names = Arrays.asList("bob", "josh", "megan");

names.replaceAll(name -> name.toUpperCase());

Die FunktionList.replaceAll gibtvoid zurück, da sie die vorhandenen Werte ersetzt. Um den Zweck zu erfüllen, muss das zur Transformation der Werte einer Liste verwendete Lambda den gleichen Ergebnistyp zurückgeben, den es empfängt. Aus diesem Grund istUnaryOperator hier nützlich.

Anstelle vonname → name.toUpperCase() können Sie natürlich einfach eine Methodenreferenz verwenden:

names.replaceAll(String::toUpperCase);

Einer der interessantesten Anwendungsfälle vonBinaryOperator ist eine Reduktionsoperation. Angenommen, wir möchten eine Auflistung von Ganzzahlen in einer Summe aller Werte zusammenfassen. Mit derStream-API könnten wir dies mit einem Kollektor, tun, aber eine allgemeinere Methode ist die Verwendung derreduce-Methode:

List values = Arrays.asList(3, 5, 8, 9, 12);

int sum = values.stream()
  .reduce(0, (i1, i2) -> i1 + i2);

Die Methodereduce empfängt einen anfänglichen Akkumulatorwert und eineBinaryOperator-Funktion. Die Argumente dieser Funktion sind ein Paar von Werten desselben Typs, und eine Funktion selbst enthält eine Logik, um sie zu einem einzigen Wert desselben Typs zusammenzufügen. Passed function must be associative, was bedeutet, dass die Reihenfolge der Wertaggregation keine Rolle spielt, d.h. Die folgende Bedingung sollte gelten:

op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)

Die assoziative Eigenschaft der Operatorfunktion vonBinaryOperatorermöglicht eine einfache Parallelisierung des Reduktionsprozesses.

Natürlich gibt es auch Spezialisierungen vonUnaryOperator undBinaryOperator, die mit primitiven Werten verwendet werden können, nämlichDoubleUnaryOperator,IntUnaryOperator,LongUnaryOperator,DoubleBinaryOperator,IntBinaryOperator undLongBinaryOperator.

11. Legacy-Funktionsschnittstellen

In Java 8 wurden nicht alle funktionalen Schnittstellen angezeigt. Viele Schnittstellen aus früheren Java-Versionen entsprechen den Einschränkungen vonFunctionalInterface und können als Lambdas verwendet werden. Ein prominentes Beispiel sind die SchnittstellenRunnable undCallable, die in Parallelitäts-APIs verwendet werden. In Java 8 sind diese Schnittstellen auch mit einer@FunctionalInterface-Annotation gekennzeichnet. Dadurch können wir den Parallelitätscode erheblich vereinfachen:

Thread thread = new Thread(() -> System.out.println("Hello From Another Thread"));
thread.start();

12. Fazit

In diesem Artikel haben wir verschiedene Funktionsschnittstellen beschrieben, die in der Java 8-API vorhanden sind und als Lambda-Ausdrücke verwendet werden können. Der Quellcode für den Artikel istover on GitHub verfügbar.