- Einführung
- Weiterführende Literatur:
- Iterable to Stream in Java
- Verwendung der if / else-Logik in Java 8 Streams
- Lambdas in Java 8
- Funktionale Schnittstellen
- Funktionen
- Primitive Funktionsspezialisierungen
- Two-Arity Function Specializations
- Lieferanten
- Verbraucher
- Prädikate
- Operatoren
- Legacy Functional Interfaces
- Fazit
Einführung
Dieses Tutorial ist eine Anleitung zu verschiedenen funktionalen Schnittstellen in Java 8, sowie ihre allgemeinen Anwendungsfälle und die Verwendung in der Standard-JDK-Bibliothek.
Weiterführende Literatur:
Iterable to Stream in Java
Verwendung der if / else-Logik in Java 8 Streams
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 wir als erstklassiger Sprachbürger handhaben können. Zum Beispiel können wir es an eine Methode übergeben oder von dieser zurückgeben.
Vor Java 8 haben wir normalerweise eine Klasse für jeden Fall erstellt, in dem wir eine einzelne Funktionalität kapseln mussten. Dies implizierte viel unnötigen Boilerplate-Code, um etwas zu definieren, das als primitive Funktionsdarstellung diente.
Der Artikel „Lambda-Ausdrücke und funktionale Schnittstellen: Tipps und Best Practices“ beschreibt detaillierter die funktionalen Schnittstellen und Best Practices der Arbeit mit Lambdas. Dieses Handbuch konzentriert sich auf einige bestimmte funktionale Schnittstellen, die in Java vorhanden sind.util.funktion paket.
Funktionale Schnittstellen
Es wird empfohlen, dass alle funktionalen Schnittstellen eine informative @FunctionalInterface Annotation haben. Dies kommuniziert eindeutig den Zweck der Schnittstelle und ermöglicht es einem Compiler, einen Fehler zu generieren, wenn die kommentierte Schnittstelle die Bedingungen nicht erfüllt.
Jede Schnittstelle mit einer SAM(Single Abstract Method) ist eine funktionale Schnittstelle, und ihre Implementierung kann als Lambda-Ausdrücke behandelt werden.
Beachten Sie, dass die Standardmethoden von Java 8 nicht abstrakt sind und nicht zählen. Wir können dies beobachten, indem wir uns die Dokumentation der Funktion ansehen.
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 die Funktionsschnittstelle dargestellt, die durch die Typen ihres Arguments und einen Rückgabewert parametrisiert wird:
public interface Function<T, R> { … }
Eine der Verwendungen des Funktionstyps in der Standardbibliothek ist die Karte.computeIfAbsent Methode. Diese Methode gibt einen Wert aus einer Zuordnung nach Schlüssel zurück, berechnet jedoch einen Wert, wenn in einer Zuordnung noch kein Schlüssel vorhanden ist. Um einen Wert zu berechnen, wird die übergebene Funktionsimplementierung verwendet:
Map<String, Integer> nameMap = new HashMap<>();Integer value = nameMap.computeIfAbsent("John", s -> s.length());
In diesem Fall berechnen wir einen Wert, indem wir eine Funktion auf einen Schlüssel anwenden, in eine Map einfügen und auch von einem Methodenaufruf zurückgeben. Wir können das Lambda durch eine Methodenreferenz ersetzen, die den übergebenen und zurückgegebenen Werttypen entspricht.
Denken Sie daran, dass ein Objekt, für das wir die Methode aufrufen, tatsächlich das implizite erste Argument einer Methode ist. Auf diese Weise können wir eine Instanzmethode als Referenz auf eine Funktionsschnittstelle umwandeln:
Integer value = nameMap.computeIfAbsent("John", String::length);
Die Funktionsschnittstelle verfügt auch über eine Standard-Compose-Methode, mit der wir mehrere Funktionen zu einer kombinieren und nacheinander ausführen können:
Function<Integer, String> intToString = Object::toString;Function<String, String> quote = s -> "'" + s + "'";Function<Integer, String> quoteIntToString = quote.compose(intToString);assertEquals("'5'", quoteIntToString.apply(5));
Die quoteIntToString-Funktion ist eine Kombination der Quote-Funktion, die auf ein Ergebnis der intToString-Funktion angewendet wird.
Primitive Funktionsspezialisierungen
Da ein primitiver Typ kein generisches Typargument sein kann, gibt es Versionen der Funktionsschnittstelle für die am häufigsten verwendeten primitiven Typen double , int, long und deren Kombinationen in Argument- und Rückgabetypen:
- IntFunction, LongFunction, DoubleFunction: arguments are of specified type, return type is parameterized
- ToIntFunction, ToLongFunction, ToDoubleFunction: return type is of specified type, arguments are parameterized
- DoubleToIntFunction, DoubleToLongFunction, IntToDoubleFunction, IntToLongFunction, LongToIntFunction, LongToDoubleFunction: wenn sowohl Argument als auch Rückgabetyp als primitive Typen definiert sind, wie durch ihre Namen angegeben
Als Beispiel gibt es keine sofort einsatzbereite funktionale Schnittstelle für eine Funktion, die ein Short nimmt und ein Byte zurückgibt, aber nichts hindert uns daran, unsere eigenen zu schreiben:
@FunctionalInterfacepublic interface ShortToByteFunction { byte applyAsByte(short s);}
Jetzt können wir eine Methode schreiben, die ein Array von short in ein Array von Byte umwandelt, indem wir eine durch eine ShortToByteFunction definierte Regel
public byte transformArray(short array, ShortToByteFunction function) { byte transformedArray = new byte; for (int i = 0; i < array.length; i++) { transformedArray = function.applyAsByte(array); } 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);
Two-Arity Function Specializations
Um Lambdas mit zwei Argumenten zu definieren, müssen wir zusätzliche Schnittstellen verwenden, die das Schlüsselwort „Bi“ in ihren Namen enthalten: BiFunction, ToDoubleBiFunction, ToIntBiFunction und ToLongBiFunction.
BiFunction hat sowohl Argumente als auch einen Rückgabetyp verallgemeinert, während ToDoubleBiFunction und andere es uns ermöglichen, einen primitiven Wert zurückzugeben.
Eines der typischen Beispiele für die Verwendung dieser Schnittstelle in der Standard-API ist in der Karte.replaceAll-Methode, mit der alle Werte in einer Map durch einen berechneten Wert ersetzt werden können.
Verwenden wir eine Bifunktionsimplementierung, die einen Schlüssel und einen alten Wert empfängt, um einen neuen Wert für das Gehalt zu berechnen und zurückzugeben.
Map<String, Integer> 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);
Lieferanten
Die Lieferantenfunktionsschnittstelle ist eine weitere Funktionsspezialisierung, die keine Argumente akzeptiert. Wir verwenden es normalerweise für die verzögerte Generierung von Werten. Definieren wir zum Beispiel eine Funktion, die einen doppelten Wert quadriert. Es erhält keinen Wert selbst, sondern einen Lieferanten dieses Wertes:
public double squareLazy(Supplier<Double> lazyValue) { return Math.pow(lazyValue.get(), 2);}
Auf diese Weise können wir das Argument für den Aufruf dieser Funktion mithilfe einer Lieferanten-Implementierung träge generieren. Dies kann nützlich sein, wenn die Generierung des Arguments viel Zeit in Anspruch nimmt. Wir simulieren das mit Guavas sleepUninterruptibly-Methode:
Supplier<Double> lazyValue = () -> { Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS); return 9d;};Double valueSquared = squareLazy(lazyValue);
Ein weiterer Anwendungsfall für den Lieferanten ist die Definition der Logik für die Sequenzgenerierung. Um dies zu demonstrieren, verwenden wir einen statischen Stream.generate-Methode zum Erstellen eines Streams von Fibonacci-Zahlen:
int fibs = {0, 1};Stream<Integer> fibonacci = Stream.generate(() -> { int result = fibs; int fib3 = fibs + fibs; fibs = fibs; fibs = fib3; return result;});
Die Funktion, die wir an den Stream übergeben.die generate-Methode implementiert die funktionale Lieferantenschnittstelle. Beachten Sie, dass der Lieferant normalerweise eine Art externen Zustand benötigt, um als Generator nützlich zu sein. In diesem Fall umfasst sein Zustand die letzten beiden Fibonacci-Sequenznummern.
Um diesen Zustand zu implementieren, verwenden wir ein Array anstelle einiger Variablen, da alle externen Variablen, die innerhalb des Lambda verwendet werden, effektiv final sein müssen.
Andere Spezialisierungen der funktionalen Lieferantenschnittstelle umfassen BooleanSupplier, DoubleSupplier, LongSupplier und IntSupplier , deren Rückgabetypen entsprechende Grundelemente sind.
Verbraucher
Im Gegensatz zum Lieferanten akzeptiert der Verbraucher ein verallgemeinertes Argument und gibt nichts zurück. Es ist eine Funktion, die Nebenwirkungen darstellt.
Lassen Sie uns zum Beispiel alle in einer Namensliste begrüßen, indem wir die Begrüßung in der Konsole drucken. Das Lambda wurde an die Liste übergeben.Die forEach-Methode implementiert die funktionale Verbraucherschnittstelle:
List<String> names = Arrays.asList("John", "Freddy", "Samuel");names.forEach(name -> System.out.println("Hello, " + name));
Es gibt auch spezielle Versionen des Verbrauchers — DoubleConsumer, IntConsumer und LongConsumer —, die primitive Werte als Argumente erhalten. Interessanter ist die BiKonsumer-Schnittstelle. Einer seiner Anwendungsfälle besteht darin, die Einträge einer Map zu durchlaufen:
Map<String, Integer> 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 weiterer Satz spezialisierter BiConsumer-Versionen besteht aus ObjDoubleConsumer , ObjIntConsumer und ObjLongConsumer , die zwei Argumente erhalten; Eines der Argumente ist generifiziert und das andere ist ein primitiver Typ.
Prädikate
In der mathematischen Logik ist ein Prädikat eine Funktion, die einen Wert empfängt und einen booleschen Wert zurückgibt.
Die Prädikatfunktionsschnittstelle ist eine Spezialisierung einer Funktion, die einen generischen Wert empfängt und einen booleschen Wert zurückgibt. Ein typischer Anwendungsfall des Prädikats lambda ist das Filtern einer Sammlung von Werten:
List<String> names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");List<String> namesWithA = names.stream() .filter(name -> name.startsWith("A")) .collect(Collectors.toList());
Im obigen Code filtern wir eine Liste mithilfe der Stream-API und behalten nur die Namen bei, die mit dem Buchstaben „A“ beginnen. Die Prädikatimplementierung kapselt die Filterlogik.
Wie in allen vorherigen Beispielen gibt es IntPredicate, DoublePredicate und LongPredicate Versionen dieser Funktion, die primitive Werte erhalten.
Operatoren
Operatorschnittstellen sind Sonderfälle einer Funktion, die denselben Werttyp empfangen und zurückgeben. Die UnaryOperator Schnittstelle empfängt ein einzelnes Argument. Einer der Anwendungsfälle in der Collections-API besteht darin, alle Werte in einer Liste durch einige berechnete Werte desselben Typs zu ersetzen:
List<String> names = Arrays.asList("bob", "josh", "megan");names.replaceAll(name -> name.toUpperCase());
Die Liste.replaceAll Funktion gibt void, wie es die Werte an Ort und Stelle ersetzt. Um den Zweck zu erfüllen, muss das Lambda, das zum Transformieren der Werte einer Liste verwendet wird, denselben Ergebnistyp zurückgeben, den es empfängt. Deshalb ist der UnaryOperator hier nützlich.
Natürlich statt name -> name.toUpperCase() können wir einfach eine Methodenreferenz verwenden:
names.replaceAll(String::toUpperCase);
Einer der interessantesten Anwendungsfälle eines Binaryoperators ist eine Reduktionsoperation. Angenommen, wir möchten eine Sammlung von Ganzzahlen in einer Summe aller Werte aggregieren. Mit der Stream-API könnten wir dies mit einem Collector tun, aber eine allgemeinere Methode wäre die Verwendung der Reduce-Methode:
List<Integer> values = Arrays.asList(3, 5, 8, 9, 12);int sum = values.stream() .reduce(0, (i1, i2) -> i1 + i2);
Die reduce-Methode erhält einen anfänglichen Akkumulatorwert und eine BinaryOperator-Funktion. Die Argumente dieser Funktion sind ein Wertepaar desselben Typs; die Funktion selbst enthält auch eine Logik, um sie in einem einzigen Wert desselben Typs zu verbinden. Die übergebene Funktion muss assoziativ sein, was bedeutet, dass die Reihenfolge der Werteaggregation 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 einer BinaryOperator-Operatorfunktion ermöglicht es uns, den Reduktionsprozess einfach zu parallelisieren.
Natürlich gibt es auch Spezialisierungen von UnaryOperator und BinaryOperator, die mit primitiven Werten verwendet werden können, nämlich DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator und LongBinaryOperator.
Legacy Functional Interfaces
Nicht alle funktionalen Schnittstellen erschienen in Java 8. Viele Schnittstellen aus früheren Java-Versionen entsprechen den Einschränkungen eines FunctionalInterface , und wir können sie als Lambdas verwenden. Prominente Beispiele sind die ausführbaren und aufrufbaren Schnittstellen, die in Parallelitäts-APIs verwendet werden. In Java 8 sind diese Schnittstellen auch mit einer @FunctionalInterface Annotation markiert. Auf diese Weise können wir den Parallelitätscode erheblich vereinfachen:
Thread thread = new Thread(() -> System.out.println("Hello From Another Thread"));thread.start();