Advanced   Java   Services Lambda Back Next Up Home


Lambdas

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.





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") );

Ein Lambdaausdruck braucht einen Kontext

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.

java-8-lambda-01.jpg

Erste durch eine explizite Typisierung mit einem Cast bekommen wir einen eindeutigen Typ.

java-8-lambda-02.jpg


Lambdasyntax und Typinferenz

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
MethodeLambdaausdruck 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'
MethodeLambdaausdruck
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


Beispiele zur vereinfachten Lambdasyntax

Fall 1 : Methode ohne Parameter, Returntyp void, ein Statement
public void foo()
{
   System.out.println("only one statement");
}

führt zu

() -> System.out.println("only one statement")

Fall 2 : Methode ohne Parameter aber mit Returnstatement
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!


Fall 3 : Methode mit Parameter, Returntyp void
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:


this und Lambda

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 
   }
}

Exceptions und Lambda

Hier muß beachtet werden, daß es in Java checked und unchecked Exceptions gibt. Der "unchecked" Fall macht keine Probleme.


Unchecked Exceptions

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

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.


Zugriff auf äußerer Variablen

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

java-8-lambda-03.jpg

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.


Methoden-Referenzen

Es kann vorkommen, daß im Codeteil eines Lambdaausdruck die Aufgabe einfach auf eine bestehende Methode gleicher Signatur delegiert wird. Hierzu drei Beispiele.


Beispiel 1: Methodenreferenz für DoubleFunction

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





Beispiel 2: System.out::println in forEach

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.


Beispiel 3: Methodenreferenzen in Streams

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ß.


Tabellarische Übersicht

Eine kleine tabellarische Übersicht verdeutlicht nochmal die Zusammenhänge.

Methodenreferenzen
LambdaausdruckMethodenreferenzTyp
s -> System.out.println(s)
System.out::println
Consumer<String>
d -> Math.nextDown(d)
Math::nextDown
IntFunction
DoubleUnaryOperator
ch -> Character.isDigit(ch)
Character::isDigit
Predicate<Character>

Function<Character, Boolean>
(String s) -> new Integer(s)
Integer::new
Function<String, Integer>
() -> Instant.now
Instant::now
Supplier<Instant>
(String s) -> new String(s)
(Kopierkonstruktor)
String::new
UnaryOperator<String>

Function<String, String>
(String s1, String s2) -> s1.concat(s2)
String::concat
BinaryOperator<String>
BiFunction<String, String, String>


Methoden-Referenzen als Funktionspointer

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;

Einteilung der Functional Interfaces

Die Functional Interfaces aus dem Package java.util.function kann man in die folgenden Gruppen einteilen:


Consumer<T>: "kriegt ein T, gibt nix"
Methode:  void accept(T t)

Es gibt 4 Consumer

Consumer
DoubleConsumer
IntConsumer
LongConsumer

BiConsumer<T, U>: "kriegt ein T und ein U, gibt nix"
Methode:  void accept(T t, U u)

Es gibt 4 BiConsumer

BiConsumer
ObjDoubleConsumer
ObjIntConsumer
ObjLongConsumer

Function<T , R>: "kriegt ein T, gibt ein R"
Methode:  R apply(T t)

Es gibt 13 Functions

Function
IntFunction
DoubleFunction
DoubleToIntFunction
DoubleToLongFunction
IntToDoubleFunction
IntToLongFunction
LongFunction
LongToDoubleFunction
LongToIntFunction
ToDoubleFunction
ToIntFunction
ToLongFunction

BiFunction<T , U, R>: "kriegt ein T und ein U, gibt ein R"
Methode:  R apply(T t, U u)

Es gibt 4 BiFunctions

BiFunction
ToDoubleBiFunction
ToIntBiFunction
ToLongBiFunction

UnaryOperator<T> extends Function<T, T>: "kriegt ein T, gibt ein T"
Methode:  T apply(T t)

Spezialfall einer Function, Es gibt 4 UnaryOperator

UnaryOperator
DoubleUnaryOperator
IntUnaryOperator
LongUnaryOperator

BinaryOperator<T> extends Function<T, T, T>: "kriegt zwei T, gibt ein T"
Methode:  T apply(T t, T t)

Spezialfall einer BiFunction, Es gibt 4 UnaryOperator

UnaryOperator
DoubleUnaryOperator
IntUnaryOperator
LongUnaryOperator

Predicate<T>: "kriegt ein T, gibt ein boolean"
Methode:  boolean test(T t)

Es gibt 4 Predicate.

Predicate
DoublePredicate
IntPredicate
LongPredicate

BiPredicate<T, U>: "kriegt ein T und ein U, gibt ein boolean"
Methode:  boolean test(T t, U u)

Es gibt 1 BiPredicate.

BiPredicate

Supplier: T get() "kriegt nix, gibt ein T"

Es gibt 5 Supplier

BooleanSupplier
DoubleSupplier
IntSupplier
LongSupplier
Supplier

Zu Functional Interfaces gewordene Interfaces

Callable
Comparator
DirectoryStream.Filter
FileFilter
FilenameFilter
Filter
KeyEventDispatcher
KeyEventPostProcessor
PathMatcher
PreferenceChangeListener
Runnable
TemporalAdjuster
TemporalQuery
Thread.UncaughtExceptionHandler
>
Zu Functional Interfaces gewordene Interfaces
InterfaceMethodeTyp
NameReturntypName
Callable<R>Rcall()Supplier
Comparator<T>intcompare(T o1, T o2)BiFunction
DirectoryStream.Filter<T>booleanaccept(T entry)Predicate
FileFilterbooleanaccept(File pathname)Predicate
FilenameFilterbooleanaccept(File dir, String name)BiPredicate
KeyEventDispatcherbooleandispatchKeyEvent(KeyEvent e)Predicate
KeyEventPostProcessorbooleanpostProcessKeyEvent(KeyEvent e)Predicate
PathMatcherbooleanmatches(Path path)Predicate
PreferenceChangeListenervoidpreferenceChange(PreferenceChangeEvent evt)Consumer
Runnablevoidrun()
Thread.UncaughtExceptionHandlervoiduncaughtException(Thread t, Throwable e)BiConsumer

Valid XHTML 1.0 Strict top Back Next Up Home