Hallo! In der Java Syntax Pro-Quest haben wir Lambda-Ausdrücke untersucht und festgestellt, dass sie nichts anderes als eine Implementierung einer funktionalen Methode aus einer funktionalen Schnittstelle sind. Mit anderen Worten, dies ist die Implementierung einer anonymen (unbekannten) Klasse, ihrer nicht realisierten Methode. Und wenn wir uns in den Vorlesungen des Kurses mit Manipulationen mit Lambda-Ausdrücken beschäftigt haben, betrachten wir nun sozusagen die andere Seite: nämlich genau diese Schnittstellen. Die achte Version von Java führte das Konzept der funktionalen Schnittstellen ein . Was ist das? Eine Schnittstelle mit einer nicht implementierten (abstrakten) Methode gilt als funktionsfähig. Viele Out-of-the-Box-Schnittstellen fallen unter diese Definition, wie zum Beispiel die zuvor besprochene Schnittstelle Prädikat
Verbraucher
Anbieter
Funktion
UnaryOperator
Comparator
. Und auch Schnittstellen, die wir selbst erstellen, wie zum Beispiel:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
}
Wir haben eine Schnittstelle, deren Aufgabe es ist, Objekte eines Typs in Objekte eines anderen Typs umzuwandeln (eine Art Adapter). Die Annotation @FunctionalInterface
ist nicht besonders komplex oder wichtig, da ihr Zweck darin besteht, dem Compiler mitzuteilen, dass diese Schnittstelle funktionsfähig ist und nicht mehr als eine Methode enthalten sollte. Wenn eine Schnittstelle mit dieser Annotation über mehr als eine nicht implementierte (abstrakte) Methode verfügt, überspringt der Compiler diese Schnittstelle nicht, da er sie als fehlerhaften Code wahrnimmt. Schnittstellen ohne diese Anmerkung können als funktionsfähig betrachtet werden und funktionieren, aber @FunctionalInterface
das ist nichts weiter als eine zusätzliche Versicherung. Gehen wir zurück zum Unterricht Comparator
. Wenn Sie sich den Code (oder die Dokumentation ) ansehen , können Sie erkennen, dass es viel mehr als eine Methode hat. Dann fragen Sie: Wie kann es dann als funktionale Schnittstelle betrachtet werden? Abstrakte Schnittstellen können Methoden haben, die nicht im Rahmen einer einzelnen Methode liegen:
- statisch
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
}
Nach Erhalt dieser Methode hat sich der Compiler nicht beschwert, was bedeutet, dass unsere Schnittstelle immer noch funktionsfähig ist.
- Standardmethoden
default
:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
default void writeToConsole(T t) {
System.out.println("Текущий ein Objekt - " + t.toString());
}
}
Auch hier sehen wir, dass der Compiler nicht angefangen hat, sich zu beschweren, und wir sind nicht über die Einschränkungen der Funktionsschnittstelle hinausgegangen.
- Objektklassenmethoden
Object
. Dies gilt nicht für Schnittstellen. Wenn wir jedoch eine abstrakte Methode in der Schnittstelle haben, die die Signatur mit einer Methode der Klasse übereinstimmt Object
, wird (oder werden) eine solche Methode (oder Methoden) unsere funktionale Schnittstellenbeschränkung nicht durchbrechen:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
default void writeToConsole(T t) {
System.out.println("Текущий ein Objekt - " + t.toString());
}
boolean equals(Object obj);
}
Und auch hier beschwert sich unser Compiler nicht, sodass die Schnittstelle Converter
weiterhin als funktionsfähig gilt. Die Frage ist nun: Warum müssen wir uns auf eine nicht implementierte Methode in einer funktionalen Schnittstelle beschränken? Und dann, damit wir es mithilfe von Lambdas implementieren können. Schauen wir uns das anhand eines Beispiels an Converter
. Erstellen wir dazu eine Klasse Dog
:
public class Dog {
String name;
int age;
int weight;
public Dog(final String name, final int age, final int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
}
Und ein ähnlicher Raccoon
(Waschbär):
public class Raccoon {
String name;
int age;
int weight;
public Raccoon(final String name, final int age, final int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
}
Angenommen Dog
, wir haben ein Objekt und müssen ein Objekt basierend auf seinen Feldern erstellen Raccoon
. Das heißt, Converter
es konvertiert ein Objekt eines Typs in einen anderen. Wie wird es aussehen:
public static void main(String[] args) {
Dog dog = new Dog("Bobbie", 5, 3);
Converter<Dog, Raccoon> converter = x -> new Raccoon(x.name, x.age, x.weight);
Raccoon raccoon = converter.convert(dog);
System.out.println("Raccoon has parameters: name - " + raccoon.name + ", age - " + raccoon.age + ", weight - " + raccoon.weight);
}
Wenn wir es ausführen, erhalten wir die folgende Ausgabe auf der Konsole:
Raccoon has parameters: name - Bobbbie, age - 5, weight - 3
Und das bedeutet, dass unsere Methode korrekt funktioniert hat.
Grundlegende Java 8-Funktionsschnittstellen
Schauen wir uns nun einige funktionale Schnittstellen an, die uns Java 8 gebracht hat und die aktiv in Verbindung mit der Stream-API verwendet werden.Prädikat
Predicate
— eine funktionale Schnittstelle zur Überprüfung, ob eine bestimmte Bedingung erfüllt ist. Wenn die Bedingung erfüllt ist, wird zurückgegeben true
, andernfalls - false
:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
Erwägen Sie beispielsweise die Erstellung einer Datei, Predicate
die die Parität einer Reihe von Typen prüft Integer
:
public static void main(String[] args) {
Predicate<Integer> isEvenNumber = x -> x % 2==0;
System.out.println(isEvenNumber.test(4));
System.out.println(isEvenNumber.test(3));
}
Konsolenausgabe:
true
false
Verbraucher
Consumer
(aus dem Englischen – „Verbraucher“) – eine funktionale Schnittstelle, die ein Objekt vom Typ T als Eingabeargument akzeptiert, einige Aktionen ausführt, aber nichts zurückgibt:
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
Betrachten Sie als Beispiel , dessen Aufgabe es ist, mit dem übergebenen String-Argument eine Begrüßung an die Konsole auszugeben: Consumer
public static void main(String[] args) {
Consumer<String> greetings = x -> System.out.println("Hello " + x + " !!!");
greetings.accept("Elena");
}
Konsolenausgabe:
Hello Elena !!!
Anbieter
Supplier
(aus dem Englischen – Anbieter) – eine funktionale Schnittstelle, die keine Argumente entgegennimmt, aber ein Objekt vom Typ T zurückgibt:
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Betrachten Sie als Beispiel Supplier
, wodurch zufällige Namen aus einer Liste erzeugt werden:
public static void main(String[] args) {
ArrayList<String> nameList = new ArrayList<>();
nameList .add("Elena");
nameList .add("John");
nameList .add("Alex");
nameList .add("Jim");
nameList .add("Sara");
Supplier<String> randomName = () -> {
int value = (int)(Math.random() * nameList.size());
return nameList.get(value);
};
System.out.println(randomName.get());
}
Und wenn wir dies ausführen, sehen wir zufällige Ergebnisse aus einer Namensliste in der Konsole.
Funktion
Function
– Diese funktionale Schnittstelle nimmt ein Argument T und wandelt es in ein Objekt vom Typ R um, das als Ergebnis zurückgegeben wird:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
Nehmen wir als Beispiel , das Zahlen vom String-Format ( ) in das Zahlenformat ( ) konvertiert: Function
String
Integer
public static void main(String[] args) {
Function<String, Integer> valueConverter = x -> Integer.valueOf(x);
System.out.println(valueConverter.apply("678"));
}
Wenn wir es ausführen, erhalten wir die folgende Ausgabe auf der Konsole:
678
PS: Wenn wir nicht nur Zahlen, sondern auch andere Zeichen in den String übergeben, wird eine Ausnahme ausgelöst - NumberFormatException
.
UnaryOperator
UnaryOperator
– eine funktionale Schnittstelle, die ein Objekt vom Typ T als Parameter akzeptiert, einige Operationen daran ausführt und das Ergebnis der Operationen in Form eines Objekts desselben Typs T zurückgibt:
@FunctionalInterface
public interface UnaryOperator<T> {
T apply(T t);
}
UnaryOperator
, das seine Methode apply
zum Quadrieren einer Zahl verwendet:
public static void main(String[] args) {
UnaryOperator<Integer> squareValue = x -> x * x;
System.out.println(squareValue.apply(9));
}
Konsolenausgabe:
81
Wir haben uns fünf funktionale Schnittstellen angesehen. Das ist nicht alles, was uns ab Java 8 zur Verfügung steht – das sind die wichtigsten Schnittstellen. Die übrigen verfügbaren sind ihre komplizierten Analoga. Die vollständige Liste finden Sie in der offiziellen Oracle-Dokumentation .
Funktionale Schnittstellen in Stream
Wie oben erläutert, sind diese Funktionsschnittstellen eng mit der Stream-API verknüpft. Wie, fragen Sie? Und zwar so, dass viele MethodenStream
speziell mit diesen Funktionsschnittstellen arbeiten. Schauen wir uns an, wie funktionale Schnittstellen in verwendet werden können Stream
.
Methode mit Prädikat
Nehmen wir zum Beispiel die KlassenmethodeStream
– filter
die als Argument verwendet Predicate
und Stream
nur die Elemente zurückgibt, die die Bedingung erfüllen Predicate
. Im Kontext von Stream
-a bedeutet dies, dass nur diejenigen Elemente durchlaufen werden, die true
bei Verwendung in einer test
Schnittstellenmethode zurückgegeben werden Predicate
. So würde unser Beispiel aussehen Predicate
, aber für einen Filter von Elementen in Stream
:
public static void main(String[] args) {
List<Integer> evenNumbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8)
.filter(x -> x % 2==0)
.collect(Collectors.toList());
}
Infolgedessen evenNumbers
besteht die Liste aus den Elementen {2, 4, 6, 8}. Und wie wir uns erinnern, collect
werden alle Elemente in einer bestimmten Sammlung gesammelt: in unserem Fall in List
.
Methode mit Verbraucher
Eine der Methoden inStream
, die die funktionale Schnittstelle nutzt Consumer
, ist die peek
. So wird unser Beispiel für Consumer
in aussehen Stream
:
public static void main(String[] args) {
List<String> peopleGreetings = Stream.of("Elena", "John", "Alex", "Jim", "Sara")
.peek(x -> System.out.println("Hello " + x + " !!!"))
.collect(Collectors.toList());
}
Konsolenausgabe:
Hello Elena !!!
Hello John !!!
Hello Alex !!!
Hello Jim !!!
Hello Sara !!!
Da die Methode jedoch peek
mit arbeitet , erfolgt keine Consumer
Änderung der Zeichenfolgen in , sondern es werden die ursprünglichen Elemente zurückgegeben: die gleichen, wie sie vorhanden waren. Daher besteht die Liste aus den Elementen „Elena“, „John“, „Alex“, „Jim“, „Sara“. Es gibt auch eine häufig verwendete Methode , die der Methode ähnelt , der Unterschied jedoch darin besteht, dass sie final ist – terminal.Stream
peek
Stream
peopleGreetings
foreach
peek
Methode mit Lieferant
Ein Beispiel für eine Methode,Stream
die die funktionale Schnittstelle verwendet , Supplier
ist generate
, die eine unendliche Sequenz basierend auf der ihr übergebenen funktionalen Schnittstelle generiert. Lassen Sie uns unser Beispiel verwenden Supplier
, um fünf zufällige Namen auf der Konsole auszugeben:
public static void main(String[] args) {
ArrayList<String> nameList = new ArrayList<>();
nameList.add("Elena");
nameList.add("John");
nameList.add("Alex");
nameList.add("Jim");
nameList.add("Sara");
Stream.generate(() -> {
int value = (int) (Math.random() * nameList.size());
return nameList.get(value);
}).limit(5).forEach(System.out::println);
}
Und das ist die Ausgabe, die wir in der Konsole erhalten:
John
Elena
Elena
Elena
Jim
Hier haben wir die Methode verwendet limit(5)
, um ein Limit für die Methode festzulegen generate
, andernfalls würde das Programm auf unbestimmte Zeit zufällige Namen auf der Konsole ausgeben.
Methode mit Funktion
Ein typisches Beispiel für eine Methode mitStream
Argument Function
ist eine Methode map
, die Elemente eines Typs nimmt, etwas damit macht und sie weitergibt, es kann sich dabei aber auch schon um Elemente eines anderen Typs handeln. So könnte ein Beispiel mit Function
in aussehen Stream
:
public static void main(String[] args) {
List<Integer> values = Stream.of("32", "43", "74", "54", "3")
.map(x -> Integer.valueOf(x)).collect(Collectors.toList());
}
Als Ergebnis erhalten wir eine Liste von Zahlen, jedoch in Integer
.
Methode mit UnaryOperator
Als Methode, dieUnaryOperator
als Argument verwendet, nehmen wir eine Klassenmethode Stream
- iterate
. Diese Methode ähnelt der Methode generate
: Sie generiert ebenfalls eine unendliche Folge, verfügt jedoch über zwei Argumente:
- das erste ist das Element, von dem aus die Sequenzgenerierung beginnt;
- Das zweite ist
UnaryOperator
, was das Prinzip der Generierung neuer Elemente aus dem ersten Element angibt.
UnaryOperator
, aber in der Methode iterate
:
public static void main(String[] args) {
Stream.iterate(9, x -> x * x)
.limit(4)
.forEach(System.out::println);
}
Wenn wir es ausführen, erhalten wir die folgende Ausgabe auf der Konsole:
9
81
6561
43046721
Das heißt, jedes unserer Elemente wird mit sich selbst multipliziert und so weiter für die ersten vier Zahlen. Das ist alles! Es wäre großartig, wenn Sie nach der Lektüre dieses Artikels dem Verständnis und der Beherrschung der Stream-API in Java einen Schritt näher gekommen wären!
GO TO FULL VERSION