Advanced Java Services | Lambda |
Für die Erklärung des Begriffs vergleichen wir, wie man ein Objekt vom Interfacetyp Runnable bisher erstellt hat und wie das mit Hilfe eines Lambdaausdrucks nunmehr vereinfacht wird.
Java 1.7
Runnable oldRun = new Runnable() { @Override public void run() { System.out.println("oldRun"); } };
Java 1.8
Runnable newRun = () -> System.out.println("lambda!");
Sehr kurz und sehr elegant. Endlich geht das auch in Java!
Den Ausdruck () -> System.out.println("lambda!")
erkennt der Compiler als
Zuordnungsvorschrift für eine Funktion und diese stellt somit eine anonyme Methode dar. Der Compiler prüft nun,
ob diese Funktionsvorschrift zu der im Interface vereinbarten Methode paßt, in diesem Fall also zu der Methode run().
So erzeugt etwa die folgende in diesem Zusammenhang falsche Vorschrift
Runnable newRun = (int i) -> System.out.println(i);
den Fehler Lambda expression's signature does not match the signature of the functional interface method run()
Stimmt die Signatur und treten sonst keine Symtaxfehler auf, so kann der Compiler ein Objekt vom Typ des Interfaces anlegen und realisiert damit den Typ des Interfaces und die Methode run, die man nun über das Objekt rufen kann. Voraussetzung ist allerdings, daß das Interface nur eine (abstrakte) Methode enthält, denn sonst müßte man mehr als eine Funktionsvorschrift angeben und dafür gibt es keine Syntax. Interfaces mit nur einer einzigen Methode erhalten daher in Java 8 den neuen Namen Functional Interfaces.
Interfaces mit nur einer abstrakten Methode bekommen in 1.8 also den Namen Functional Interfaces. Am besten verwendet man diese Interfaces mit der (optionalen) Annotation @FunctionalInterface. Dann meldet sich der nämlich der Compiler, wenn man eine zweite abstrakte Methode einfügen will. Wir geben und das folgende Interface vor.
@FunctionalInterface // optional, sollte aber verwendet werden ! public interface FuncInterface { void foo(); }
Wir realisieren das Interface FuncInterface wie eingangs noch einmal durch eine anonyme Klasse.
/** * bis 1.7 */ private static void preLambdaStyle() { FuncInterface fi = new FuncInterface() { @Override public void bar() { System.out.println("lambda!"); } }; fi.foo(); }
Nun ersetzen wir die anonyme Klasse durch einen Lambdaausdruck.
/** * ab 1.8 */ private static void lambdaStyle() { // Implementierung des Interfaces mit einem Lambdaausdruck FuncInterface1 foo = () -> System.out.println("lambda!") ; foo.bar(); }
Die leere Klammer symbolisiert die leere Parameterliste und muß gesetzt werden. Nach dem Pfeil kommt der Methodenrumpf. Hat dieser wie hier und im einleitenden Beispiel nur eine einzige Anweisung reicht es, nach dem Pfeil dieses Statement einzufügen. Die genaue Syntax folgt weiter unten. Man kann diesen Lambdaausdruck problemlos einer Methode als Parameter übergeben, falls die Methode den passenden Interfacetyp als Parameter anbietet. Die Methode kann z.Bsp. folgendermaßen aussehen:
parameterTypeIsFunctionalInterface(FuncInterface foo) { foo.bar(); }
Und der Aufruf so:
parameterTypeIsFunctionalInterface( () -> System.out.println("only one statement") );
In den ersten beiden Beispielen wird der Lambdaausdruck () -> System.out.println("lambda!")
einmal einer Referenz vom Typ Runnable und anschließend eine Referenz vom Typ FuncInterface zugewiesen. Die Methoden in den
beiden Interfaces haben zwar verschiedene Namen, aber dieselbe Signatur und denselben Returntyp. Man kann daraus schließen, daß ein
Lambdaausdruck keinen eindeutigen Typ hat. Erst durch die Zuweisung erhält der Compiler die Möglichkeit zu prüfen, ob der Lambdaausdruck
eine Realisierung der abstrakten Methode ist. Erst durch den Kontext erhält der Lambdaausdruck einen Typ. Ein- und derselbe Lambdaausdruck
kann also durchaus für mehrere Datentypen passen. Der Lambdaausdruck alleine hat also zunächst noch keinen Typ, die Intellisense von Eclipse
reagiert daher auch nicht auf den reinen Lambdaausdruck.
Erste durch eine explizite Typisierung mit einem Cast bekommen wir einen eindeutigen Typ.
Ein Beispiel zum allgemeinen Fall. Im folgenden seien R, T und U beliebige Datentypen, wobei R auch für void stehen kann. Für die Parameterliste sind beliebig viele Parameter erlaubt, auch keiner.
Methode versus Lambdaausdruck | ||
---|---|---|
Methode | Lambdaausdruck 1 allgemeine Syntax |
Lambdaausdruck 2 vereinfachte Syntax |
public R foo(T t, U u) { //Statements //Returnstatement } |
(T t, U u) -> { //Statements //Returnstatement } |
(t, u) -> { //Statements //Returnstatement } |
In Lambdaausdruck 1 sehen wir die allgemeine Lambdasyntax. Lambdaausdruck 2 ist eine zulässige Verinfachung. Am auffälligsten ist, daß dem Lambdaausdruck Information über den Returntyp fehlt. Der Compiler muß den Returntyp aus dem Returnstatement erschließen. Im zweiten Lambdaausdruck fehlt auch die Information über die Parametertypen. Der Compiler erschließt die Typen, indem er Typen aus der Parameterliste der Methode des Functional Interfaces übernimmt. Diesen Vorgang nennt man Typinferenz.
So sind die beiden folgenden Lambdaausdrücke korrekt.
Comparator<String> ignoreCaseComparator = (String s1, String s2) -> s1.toLowerCase().compareTo(s2.toLowerCase()) ;
Comparator<String> ignoreCaseComparator = (s1, s2) -> s1.toLowerCase().compareTo(s2.toLowerCase()) ;
Man erkennt daraus, daß man nicht nur die Typen in der Parameterliste weglassen kann, sondern auch die geschweiften Klammern, das Schlüsselwort return und das Semikolon. Allerdings gilt letzteres nur, wenn der Rumpf nur aus genau einem Statement besteht. Falls im Methodenrumpf nur eine einzige Anweisung steht kann man noch weiter vereinfachen.
Vereinfachte Syntax für 'EinStatementMethoden' | |
---|---|
Methode | Lambdaausdruck |
public R foo(T t, U u) { //ein Statement (mit oder ohne return) } |
(t, u) -> einStatement |
public R bar(T t) { //ein Statement (mit oder ohne return) } |
t -> einStatement |
public R foobar() { //ein Statement (mit oder ohne return) } |
() -> einStatement |
public void foo() { System.out.println("only one statement"); }
führt zu
() -> System.out.println("only one statement")
public int foo() { return new Random().nextInt(); }
führt zu
() -> new Random().nextInt()
Durch Abgleich mit der Methode des Functional Interfaces kann der Compiler erschließen, daß der Ausdruck new Random().nextInt(); zurückgegeben werden muß. Achtung: Wenn man hier return einfügt, muß man auch geschweifte Klammern setzen!
public void foo(String st) { System.out.println(st); }
führt zu
s -> System.out.println(s)
Da der Compiler den Typ aus der Definition des Interfaces erschließen kann, kann man den Typ und die Klammer weglassen:
Ein Lambdaausdruck wird anders compliliert als einen anonyme Klasse. Das sieht man u.a. daran, daß zu einem Lambdaausdruck keine anonyme Klasse existiert. Ein this in einem Lambdaausdruck kann sich also nicht auf eine anonyme Klasse beziehen. Hierzu folgendes Beispiel
Die Klasse heiße
public class Lambda { public static void main(String[] args) throws IOException { Lambda lam = new Lambda(); lam.lambdathis(); } private void lambdathis() { IntConsumer intCo1 = new IntConsumer() { @Override public void accept(int i) { System.out.println(this.getClass().getName()); } }; intCo1.accept(0); // Lambda$1 IntConsumer intCo2 = i -> { System.out.println(this.getClass().getName()); } ; intCo2.accept(0); // Lambda } }
Das this im Lambdaausdruck ist also das this der Klasse, in der die übergordnete Methode liegt. Daraus ergibt sich, daß ein Lambdaausdruck in einer statischen Methode KEIN this kennt.
public class Lambda { public static void main(String[] args) throws IOException { lambdathis(); } private static void lambdathis() { IntConsumer intCo1 = new IntConsumer() { @Override public void accept(int i) { System.out.println(this.getClass().getName()); } }; intCo1.accept(0); // Lambda$1 IntConsumer intCo2 = i -> { System.out.println(this.getClass().getName()); } ; //Cannot use this in a static context } }
Hier muß beachtet werden, daß es in Java checked und unchecked Exceptions gibt. Der "unchecked" Fall macht keine Probleme.
Bei einer unchecked Exception haben wir freie Wahl, man kann sie im Lambdaausdruck oder auch außerhalb behandeln. In der Regel behandelt man Exceptions lieber auf einer höheren Ebene, bevorzugt also die erste Variante des folgenden Beispiels.
private static void uncheckedExceptions() { Function<String, Integer> func1 = s -> Integer.parseInt(s) ; // try außerhalb try { func1.apply("xyz"); // java.lang.NumberFormatException: For input string: "xyz" } catch(NumberFormatException ex) { System.out.println("aussen " + ex); } // try innen Function<String, Integer> func2 = s -> { try { return Integer.parseInt(s); } catch(NumberFormatException ex) { System.out.println("innen " + ex); return null; } }; func2.apply("abc"); System.out.println("end"); }
Checked Exceptions müssen sofort behandelt werden, das erzwingt der Compiler. Will man sie später behandeln, so greift man zu einem Trick: Man behandelt sie pro forma an Ort und Stelle, wirft aber im catch-Zweig eine unchecked Exception, die die Message nach außen weiterträgt. das sieht dann folgendermaßen aus:
private static void checkedExceptions() { Function<Path, List<String>> readAll = path -> { try { return Files.readAllLines(path); } catch(IOException ex) { // weiterwerfen throw new RuntimeException(ex.getMessage(), ex); } } ; try { Path path = Paths.get("foo.txt"); List<String> list = readAll.apply(path); } catch(Exception ex) { System.out.println(ex); // java.lang.RuntimeException: foo.txt System.out.println(ex.getCause()); // java.nio.file.NoSuchFileException: foo.txt } }
Eine andere Möglichkeit wäre, ein eigenes Functional Interface zu schreiben, das mit einer throws-Klausel ausgestattet ist. Dann kann man aber nicht mehr mit den Functional Interfaces aus java.util.function arbeiten und damit nicht mehr mit Streams, die von diesen Interfaces ständig gebrauch machen.
Hier gilt die gleiche Regel für anonyme Klassen wie für Lambdaausdrücke. Greift eine anonyme Klasse oder ein Lambdaausdruck auf eine lokale Variable zu, so muß diese final oder effectiv final sein
Hierzu der folgende Screenshot
Der Begriff effectiv final ist neu in Java 8 und die Oracle Dokumentation sagt hierzu:
"A variable or parameter whose value is never changed after it is initialized is effectively final."
Siehe auch die Diskussion auf
stackoverflow:
So nebenbei sieht man, daß es in einem Lambdaausdruck keine Instanzvariablen geben kann.
Es kann vorkommen, daß im Codeteil eines Lambdaausdruck die Aufgabe einfach auf eine bestehende Methode gleicher Signatur delegiert wird. Hierzu drei Beispiele.
Der Lambdaausdruck d -> Math.nextDown(d)
berechnet zu einem double die nächstniedrigere Doublezahl und erfüllt genau die Signatur des Functional Interfaces DoubleFunction.
Daher kann man folgende Zuweisung machen
DoubleFunction df = d -> Math.nextDown(d);
Hier nun zwei Arten der Verwendung der Instanz df :
(1) System.out.println(df.apply(1.2));
Wenn es eine Methode foo(DoubleFunction) gibt (statisch oder nichtstatisch, Returntyp egal), dann kann man schreiben
(2) foo(df)
oder direkt
(3) foo( d -> Math.nextDown(d) )
In (1) kann man aber auch gleich schreiben System.out.println(Math.nextDown(d))
,
da die existierende Methode nextDown() genau dasselbe leistet wie der Lambdaausdruck.
Für (2) oder (3) gibt es ab Java 1.8 etwas ähnliches, da es nun eine Syntax gibt, mit der man eine existierende Methode als Lambdaausdruck schreiben kann und das sieht folgendermaßen aus:
(3) foo( Math::nextDown )
Math::nextDown nennt man Methodenreferenz zur statischen Methode double Math.nextDown(double). Eine Methodenreferenz existiert aber genauso zu nichtstatischen Methoden, wie das folgende Beispiel zeigt
Das Beispiel verwendet die foreach()-Methode aus List.
String[] stringArr = { "eins", "zweins", "dreins", "vierns"}; List<String> stringList = Arrays.asList(stringArr); stringList.forEach(s -> System.out.println(s)); // (*)
In (*) macht der Lambdaausdruck dasselbe wie die nichtstatische Methode println(). Mit einer Methodenreferenz zu println() kann man nun schreiben
stringList.forEach(System.out::println); // (**)
Interessant ist, daß es auch Methodenreferenzen zu Konstruktoren gibt. Das nächste Beispiel zeigt das.
Das nächste Beispiel zeigt auch schon die Verwendung von Streams.
String[] stringArr = { "123", "132", "213", "231", "312", "321"}; Arrays.stream(stringArr).mapToInt( s -> new Integer(s)).filter(i -> i < 300 ).forEach( s -> System.out.println(s));
Mithilfe von Methodenreferenzen ergibt sich folgendes:
Arrays.stream(stringArr).mapToInt( Integer::new ).filter(i -> i < 300 ).forEach( System.out::println);
Da der Stream auf String typisiert ist, weiß der Compiler, welchen Integer-Konstruktor er nehmen muß.
Eine kleine tabellarische Übersicht verdeutlicht nochmal die Zusammenhänge.
Methodenreferenzen | ||
---|---|---|
Lambdaausdruck | Methodenreferenz | Typ |
s -> System.out.println(s) | System.out::println | Consumer<String> |
d -> Math.nextDown(d) | Math::nextDown | IntFunction |
ch -> Character.isDigit(ch) | Character::isDigit | Predicate<Character> |
(String s) -> new Integer(s) | Integer::new | Function<String, Integer> |
() -> Instant.now | Instant::now | Supplier<Instant> |
(String s) -> new String(s) | String::new | UnaryOperator<String> |
(String s1, String s2) -> s1.concat(s2) | String::concat | BinaryOperator<String> |
Von C bzw. C++ aus gedacht, hat Java damit eine schwache Form von Funktionspointern eingeführt. Schwach deshalb, weil man Methodenreferenzen einem Functional Interface zuweisen können muß und es gibt nicht für alle Methoden passende Functional Interfaces. Man kann sie jedoch problemlos selber schreiben.
Wir wollen die Funktion String replace(CharSequence target, CharSequence replacement) als Methodenreferenz verwenden. Dazu brauchen wir ein Functional Interface das eine Erweiterung einer BiFunction ist.
@FunctionalInterface public interface TresFunction<T, U, V, R> { R apply(T t, U u, V v); }
Und damit geht z. Bsp. folgendes:
TresFunction<String, CharSequence, CharSequence, String%gt; tf1 = String::replace;
oder
TresFunction<Duration, Long, TemporalUnit, Duration> tf2 = Duration::plus;
Die Functional Interfaces aus dem Package java.util.function kann man in die folgenden Gruppen einteilen:
Es gibt 4 Consumer
Consumer [ void accept(T t) ] DoubleConsumer [ void accept(double d) ] IntConsumer [ void accept(int i) ] LongConsumer [ void accept(lomg l) ]
Es gibt 4 BiConsumer
BiConsumer [ void accept(T t, U u) ] ObjDoubleConsumer [ void accept(T t, double d) ] ObjIntConsumer [ void accept(T t, int i) ] ObjLongConsumer [ void accept(T t, long l) ]
Es gibt 13 Functions
Function [ R apply(T t) ] DoubleFunction [ R apply(double d) ] IntFunction [ R apply(int i) ] LongFunction [ R apply(long l) ] ToDoubleFunction [ double applyAsDouble(T t) ] ToIntFunction [ int applyAsInt(T t) ] ToLongFunction [ long applyAsLong(T t) ] DoubleToIntFunction [ int applyAsInt(double d) ] DoubleToLongFunction [ long applyAsLong(double d) ] IntToDoubleFunction [ double applyAsDouble(int i) ] IntToLongFunction [ long applyAsLong(int i) ] LongToDoubleFunction [ double applyAsDouble(long l) ] LongToIntFunction [ int applyAsInt(long l) ]
Es gibt 4 UnaryOperator
UnaryOperator [ T apply(T t) ] DoubleUnaryOperator [ double applyAsDouble(double d) ] IntUnaryOperator [ int applyAsInt(int i) ] LongUnaryOperator [ long applyAsLong(long l) ]
Es gibt 4 BiFunctions
BiFunction [ R apply(T t, U u) ] ToDoubleBiFunction [ double applyAsDouble(T t, U u) ] ToIntBiFunction [ int applyAsInt(T t, U u) ] ToLongBiFunction [ long applyAsLong(T t, U u) ]
Es gibt 4 UnaryOperator
UnaryOperator [ T apply(T t, T t) ] DoubleUnaryOperator [ double applyAsDouble(T t, T t) ] IntUnaryOperator [ int applyAsInt(T t, T t) ] LongUnaryOperator [ long applyAsLong(T t, T t) ]
Es gibt 4 Predicate, Predicate geben immer ein boolean zurück
Predicate [ boolean test(T t) ] DoublePredicate [ boolean test(double d) ] IntPredicate [ boolean test(int i) ] LongPredicate [ boolean test(long l) ]
Es gibt 1 BiPredicate.
BiPredicate
Es gibt 5 Supplier, Supplier sind Producer, sie liefern immer etwas.
Supplier [ T get() ] BooleanSupplier [ boolean getAsBoolean() ] DoubleSupplier [ double getAsDouble() ] IntSupplier [ int getAsInt() ] LongSupplier [ long getAsLong() ]
CallableComparator DirectoryStream.Filter FileFilter FilenameFilter Filter KeyEventDispatcher KeyEventPostProcessor PathMatcher PreferenceChangeListener Runnable TemporalAdjuster TemporalQuery Thread.UncaughtExceptionHandler
Zu Functional Interfaces gewordene Interfaces | |||
---|---|---|---|
Interface | Methode | Typ | |
Name | Returntyp | Name | |
Callable<R> | R | call() | Supplier |
Comparator<T> | int | compare(T o1, T o2) | BiFunction |
DirectoryStream.Filter<T> | boolean | accept(T entry) | Predicate |
FileFilter | boolean | accept(File pathname) | Predicate |
FilenameFilter | boolean | accept(File dir, String name) | BiPredicate |
KeyEventDispatcher | boolean | dispatchKeyEvent(KeyEvent e) | Predicate |
KeyEventPostProcessor | boolean | postProcessKeyEvent(KeyEvent e) | Predicate |
PathMatcher | boolean | matches(Path path) | Predicate |
PreferenceChangeListener | void | preferenceChange(PreferenceChangeEvent evt) | Consumer |
Runnable | void | run() | |
Thread.UncaughtExceptionHandler | void | uncaughtException(Thread t, Throwable e) | BiConsumer |