Java 8 Funktionsinterfaces

Willkommen zum Tutorial über funktionale Schnittstellen in Java 8. Java war schon immer eine objektorientierte Programmiersprache. Das bedeutet, dass sich alles in der Java-Programmierung um Objekte dreht (mit Ausnahme einiger primitiver Typen zur Vereinfachung). In Java haben wir nicht nur Funktionen, sie sind Teil einer Klasse und wir müssen die Klasse/das Objekt verwenden, um eine Funktion aufzurufen.

Funktionale Schnittstellen in Java 8

Wenn wir uns einige andere Programmiersprachen wie C++ oder JavaScript ansehen; sie werden als funktionale Programmiersprachen bezeichnet, weil wir Funktionen schreiben und sie bei Bedarf verwenden können. Einige dieser Sprachen unterstützen sowohl objektorientierte als auch funktionale Programmierung. Objektorientiert zu sein ist nicht schlecht, bringt aber viel Verbosity in das Programm. Zum Beispiel, sagen wir, wir müssen eine Instanz von Runnable erstellen. Normalerweise machen wir das mit anonymen Klassen, wie unten gezeigt.

Runnable r = new Runnable(){
			@Override
			public void run() {
				System.out.println("My Runnable");
			}};

Wenn Sie sich den obigen Code ansehen, ist der eigentliche nützliche Teil der Code innerhalb der run()-Methode. Der Rest des Codes ist auf die Struktur von Java-Programmen zurückzuführen. Funktionale Schnittstellen und Lambda-Ausdrücke in Java 8 helfen uns, kleineren und saubereren Code zu schreiben, indem sie eine Menge von Boilerplate-Code entfernen.

Java 8 Funktionsinterface

Ein Interface mit genau einer abstrakten Methode wird als Funktionsinterface bezeichnet. Die Annotation @FunctionalInterface wird hinzugefügt, damit wir ein Interface als Funktionsinterface markieren können. Es ist nicht zwingend erforderlich, sie zu verwenden, aber es ist bewährte Praxis, sie bei Funktionsinterfaces zu verwenden, um das versehentliche Hinzufügen zusätzlicher Methoden zu vermeiden. Wenn das Interface mit der Annotation @FunctionalInterface annotiert ist und wir versuchen, mehr als eine abstrakte Methode zu haben, wird ein Kompilierfehler ausgelöst. Der Hauptvorteil von Java 8 Funktionsinterfaces besteht darin, dass wir Lambda-Ausdrücke verwenden können, um sie zu instanziieren, und die Verwendung einer umfangreichen anonymen Klassenimplementierung vermeiden können. Die Java 8 Collections-API wurde neu geschrieben, und die neue Stream-API wurde eingeführt, die viele Funktionsinterfaces verwendet. Java 8 hat viele Funktionsinterfaces im Paket java.util.function definiert. Einige der nützlichen Java 8 Funktionsinterfaces sind Consumer, Supplier, Function und Predicate. Weitere Details dazu finden Sie in Java 8 Stream-Beispiel. java.lang.Runnable ist ein großartiges Beispiel für ein Funktionsinterface mit einer einzigen abstrakten Methode run(). Der folgende Codeausschnitt bietet einige Anleitungen für Funktionsinterfaces:

interface Foo { boolean equals(Object obj); }
// Nicht funktional, weil equals bereits ein implizites Element (Object-Klasse) ist

interface Comparator {
 boolean equals(Object obj);
 int compare(T o1, T o2);
}
// Funktional, weil Comparator nur eine abstrakte, nicht-Objekt-Methode hat

interface Foo {
  int m();
  Object clone();
}
// Nicht funktional, weil die Methode Object.clone nicht öffentlich ist

interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// Funktional: Zwei Methoden, aber sie haben die gleiche Signatur

interface X { Iterable m(Iterable arg); }
interface Y { Iterable m(Iterable arg); }
interface Z extends X, Y {}
// Funktional: Y.m ist eine Untersignatur & eine Rückgabetyp-Ersatz

interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// Nicht funktional: Keine Methode hat eine Untersignatur aller abstrakten Methoden

interface X { int m(Iterable arg, Class c); }
interface Y { int m(Iterable arg, Class c); }
interface Z extends X, Y {}
// Nicht funktional: Keine Methode hat eine Untersignatur aller abstrakten Methoden

interface X { long m(); }
interface Y { int m(); }
interface Z extends X, Y {}
// Kompilerfehler: Keine Methode ist rückgabetyp-ersetzbar

interface Foo { void m(T arg); }
interface Bar { void m(T arg); }
interface FooBar extends Foo, Bar {}
// Kompilerfehler: Unterschiedliche Signaturen, gleiche Löschung

Lambda-Ausdruck

Lambda-Ausdrücke sind der Weg, um funktionale Programmierung in der objektorientierten Java-Welt zu visualisieren. Objekte sind die Grundlage der Java-Programmiersprache, und wir können niemals eine Funktion ohne ein Objekt haben, deshalb bietet die Java-Sprache Unterstützung für die Verwendung von Lambda-Ausdrücken nur mit funktionalen Schnittstellen. Da es nur eine abstrakte Funktion in den funktionalen Schnittstellen gibt, gibt es keine Verwirrung bei der Anwendung des Lambda-Ausdrucks auf die Methode. Die Syntax der Lambda-Ausdrücke ist (Argument) -> (Körper). Jetzt schauen wir uns an, wie wir den obigen anonymen Runnable mit einem Lambda-Ausdruck schreiben können.

Runnable r1 = () -> System.out.println("My Runnable");

Lassen Sie uns versuchen zu verstehen, was im obigen Lambda-Ausdruck passiert.

  • Runnable ist ein funktionales Interface, deshalb können wir Lambda-Ausdrücke verwenden, um eine Instanz davon zu erstellen.
  • Da die run() Methode keine Argumente akzeptiert, hat unser Lambda-Ausdruck ebenfalls keine Argumente.
  • Ähnlich wie bei if-else Blöcken können wir geschweifte Klammern ({}) vermeiden, da wir nur eine Anweisung im Methodenkörper haben. Für mehrere Anweisungen müssten wir geschweifte Klammern wie bei anderen Methoden verwenden.

Warum brauchen wir Lambda-Ausdrücke

  1. Reduzierung der Codezeilen Ein klarer Vorteil der Verwendung von Lambda-Ausdrücken besteht darin, dass die Menge des Codes reduziert wird. Wir haben bereits gesehen, wie einfach wir eine Instanz eines funktionalen Interfaces mithilfe eines Lambda-Ausdrucks erstellen können, anstatt eine anonyme Klasse zu verwenden.

  2. Unterstützung für sequentielle und parallele Ausführung Ein weiterer Vorteil der Verwendung von Lambda-Ausdrücken besteht darin, dass wir von der Stream-API-Unterstützung für sequentielle und parallele Operationen profitieren können. Um dies zu erklären, nehmen wir ein einfaches Beispiel, bei dem wir eine Methode schreiben müssen, um zu überprüfen, ob eine übergebene Zahl eine Primzahl ist oder nicht. Traditionell würden wir den Code wie folgt schreiben. Der Code ist nicht vollständig optimiert, aber gut für Beispielzwecke, also bitte haben Sie Verständnis dafür.

    //Traditioneller Ansatz
    private static boolean isPrime(int number) {		
    	if(number < 2) return false;
    	for(int i=2; i<number; i++){
    		if(number % i == 0) return false;
    	}
    	return true;
    }
    

    Das Problem mit dem obigen Code ist, dass er sequenziell ist. Wenn die Zahl sehr groß ist, dauert es eine erhebliche Zeit. Ein weiteres Problem mit dem Code ist, dass es so viele Ausstiegspunkte gibt und er nicht lesbar ist. Schauen wir uns an, wie wir dieselbe Methode mithilfe von Lambda-Ausdrücken und der Stream-API schreiben können.

    //Deklarativer Ansatz
    private static boolean isPrime(int number) {		
    	return number > 1
    			&& IntStream.range(2, number).noneMatch(
    					index -> number % index == 0);
    }
    

    IntStream ist eine Sequenz von primitiven Int-Werten, die sequentielle und parallele Sammelvorgänge unterstützt. Dies ist die Int-Primitive-Spezialisierung von Stream. Für mehr Lesbarkeit können wir die Methode auch wie folgt schreiben.

    private static boolean isPrime(int number) {
    	IntPredicate isDivisible = index -> number % index == 0;
    	
    	return number > 1
    			&& IntStream.range(2, number).noneMatch(
    					isDivisible);
    }
    

    Wenn Sie mit IntStream nicht vertraut sind, gibt die range()-Methode eine sequentiell geordnete IntStream von startInclusive (inklusive) bis endExclusive (ausschließlich) zurück, mit einem inkrementellen Schritt von 1. Die noneMatch()-Methode gibt zurück, ob keine Elemente dieses Streams der bereitgestellten Prädikaten entsprechen. Es kann das Prädikat für alle Elemente nicht auswerten, wenn dies nicht erforderlich ist, um das Ergebnis zu bestimmen.

  3. Übergabe von Verhaltensweisen an Methoden Schauen wir uns an, wie wir Lambda-Ausdrücke verwenden können, um das Verhalten einer Methode anhand eines einfachen Beispiels zu übergeben. Angenommen, wir müssen eine Methode schreiben, um die Zahlen in einer Liste zu summieren, wenn sie einem bestimmten Kriterium entsprechen. Wir können Predicate verwenden und eine Methode wie unten gezeigt schreiben.

    public static int sumWithCondition(List<Integer> zahlen, Predicate<Integer> prädikat) {
    	    return zahlen.parallelStream()
    	    		.filter(prädikat)
    	    		.mapToInt(i -> i)
    	    		.sum();
    	}
    

    Beispielverwendung:

    // Summe aller Zahlen
    sumWithCondition(zahlen, n -> true)
    // Summe aller geraden Zahlen
    sumWithCondition(zahlen, i -> i%2==0)
    // Summe aller Zahlen größer als 5
    sumWithCondition(zahlen, i -> i>5)
    
  4. Höhere Effizienz mit Faulheit Ein weiterer Vorteil der Verwendung von Lambda-Ausdrücken ist die verzögerte Auswertung. Angenommen, wir müssen eine Methode schreiben, um die größte ungerade Zahl im Bereich von 3 bis 11 zu finden und ihr Quadrat zurückzugeben. Normalerweise würden wir den Code für diese Methode wie folgt schreiben:

    private static int findSquareOfMaxOdd(List<Integer> numbers) {
    		int max = 0;
    		for (int i : numbers) {
    			if (i % 2 != 0 && i > 3 && i < 11 && i > max) {
    				max = i;
    			}
    		}
    		return max * max;
    	}
    

    Das obige Programm wird immer in sequenzieller Reihenfolge ausgeführt, aber wir können die Stream-API verwenden, um dies zu erreichen und den Vorteil der Faulheits-Suche zu nutzen. Schauen wir uns an, wie wir diesen Code in funktionaler Programmierung mit der Stream-API und Lambda-Ausdrücken neu schreiben können.

    public static int findSquareOfMaxOdd(List<Integer> numbers) {
    		return numbers.stream()
    				.filter(NumberTest::isOdd) 		// Prädikat ist eine funktionale Schnittstelle und
    				.filter(NumberTest::isGreaterThan3)	// wir verwenden Lambdas, um sie zu initialisieren
    				.filter(NumberTest::isLessThan11)	// anstatt anonymer innerer Klassen
    				.max(Comparator.naturalOrder())
    				.map(i -> i * i)
    				.get();
    	}
    
    	public static boolean isOdd(int i) {
    		return i % 2 != 0;
    	}
    	
    	public static boolean isGreaterThan3(int i){
    		return i > 3;
    	}
    	
    	public static boolean isLessThan11(int i){
    		return i < 11;
    	}
    

    Wenn Sie überrascht sind vom Doppelkolon (::) Operator, wurde er in Java 8 eingeführt und wird für Methodenreferenzen verwendet. Der Java-Compiler kümmert sich um die Zuordnung der Argumente zur aufgerufenen Methode. Es ist eine Kurzform von Lambda-Ausdrücken i -> isGreaterThan3(i) oder i -> NumberTest.isGreaterThan3(i).

Beispiele für Lambda-Ausdrücke

Im Folgenden finden Sie einige Code-Snippets für Lambda-Ausdrücke mit kurzen Kommentaren, die sie erklären.

() -> {}                     // No parameters; void result

() -> 42                     // No parameters, expression body
() -> null                   // No parameters, expression body
() -> { return 42; }         // No parameters, block body with return
() -> { System.gc(); }       // No parameters, void block body

 // Komplexer Blockkörper mit mehreren Rückgaben 
() -> {
  if (true) return 10;
  else {
    int result = 15;
    for (int i = 1; i < 10; i++)
      result *= i;
    return result;
  }
}                          

(int x) -> x+1             // Single declared-type argument
(int x) -> { return x+1; } // same as above
(x) -> x+1                 // Single inferred-type argument, same as below
x -> x+1                   // Parenthesis optional for single inferred-type case

(String s) -> s.length()   // Single declared-type argument
(Thread t) -> { t.start(); } // Single declared-type argument
s -> s.length()              // Single inferred-type argument
t -> { t.start(); }          // Single inferred-type argument

(int x, int y) -> x+y      // Multiple declared-type parameters
(x,y) -> x+y               // Multiple inferred-type parameters
(x, final y) -> x+y        // Illegal: can't modify inferred-type parameters
(x, int y) -> x+y          // Illegal: can't mix inferred and declared types

Methoden- und Konstruktorreferenzen

A method reference is used to refer to a method without invoking it; a constructor reference is similarly used to refer to a constructor without creating a new instance of the named class or array type. Examples of method and constructor references:

System::getProperty
System.out::println
"abc"::length
ArrayList::new
int[]::new

Das war alles für das Java 8 Functional Interfaces und Lambda Expression Tutorial. Ich würde dringend empfehlen, sich damit zu befassen, denn diese Syntax ist neu für Java und es wird einige Zeit dauern, sie zu erfassen. Sie sollten auch Java 8 Features überprüfen, um alle Verbesserungen und Änderungen in der Java 8-Version kennenzulernen.

Source:
https://www.digitalocean.com/community/tutorials/java-8-functional-interfaces