JavaRush /Java-Blog /Random-DE /Funktionale Schnittstellen in Java

Funktionale Schnittstellen in Java

Veröffentlicht in der Gruppe Random-DE
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. Funktionale Schnittstellen in Java - 1Die 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 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 @FunctionalInterfaceist 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 @FunctionalInterfacedas 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
Das Konzept der Schnittstellen impliziert, dass für eine bestimmte Codeeinheit keine Methoden implementiert sein können. Aber ab Java 8 wurde es möglich, statische und Standardmethoden in Schnittstellen zu verwenden. Statische Methoden sind direkt an eine Klasse gebunden und erfordern kein bestimmtes Objekt dieser Klasse, um eine solche Methode aufzurufen. Das heißt, diese Methoden fügen sich harmonisch in das Konzept der Schnittstellen ein. Als Beispiel fügen wir der vorherigen Klasse eine statische Methode zum Überprüfen eines Objekts auf Null hinzu:
@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
Wenn wir vor Java 8 eine Methode in einer Schnittstelle erstellen mussten, die von anderen Klassen geerbt wurde, konnten wir nur eine abstrakte Methode erstellen, die in jeder spezifischen Klasse implementiert wurde. Was aber, wenn diese Methode für alle Klassen gleich ist? In diesem Fall wurden am häufigsten abstrakte Klassen verwendet . Aber ab Java 8 gibt es die Möglichkeit, Schnittstellen mit implementierten Methoden zu verwenden – den Standardmethoden. Wenn Sie eine Schnittstelle erben, können Sie diese Methoden überschreiben oder alles unverändert lassen (die Standardlogik beibehalten). Beim Erstellen einer Standardmethode müssen wir das Schlüsselwort - hinzufügen 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
In der Vorlesung Objekte vergleichen haben wir darüber gesprochen, dass alle Klassen von der Klasse erben 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 Converterweiterhin 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, Converteres 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.Funktionale Schnittstellen in Java - 2

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, Predicatedie 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: FunctionStringInteger
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 applyzum 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? Funktionale Schnittstellen in Java - 3Und zwar so, dass viele Methoden Streamspeziell 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 Klassenmethode Streamfilterdie als Argument verwendet Predicateund Streamnur die Elemente zurückgibt, die die Bedingung erfüllen Predicate. Im Kontext von Stream-a bedeutet dies, dass nur diejenigen Elemente durchlaufen werden, die truebei Verwendung in einer testSchnittstellenmethode 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 evenNumbersbesteht die Liste aus den Elementen {2, 4, 6, 8}. Und wie wir uns erinnern, collectwerden alle Elemente in einer bestimmten Sammlung gesammelt: in unserem Fall in List.

Methode mit Verbraucher

Eine der Methoden in Stream, die die funktionale Schnittstelle nutzt Consumer, ist die peek. So wird unser Beispiel für Consumerin 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 peekmit 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.StreampeekStreampeopleGreetingsforeachpeek

Methode mit Lieferant

Ein Beispiel für eine Methode, Streamdie die funktionale Schnittstelle verwendet , Supplierist 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 mit StreamArgument Functionist 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 Functionin 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, die UnaryOperatorals 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.
So wird unser Beispiel aussehen 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. Funktionale Schnittstellen in Java - 4Das 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!
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION