Java 8: Lambda Expressions

Category: 

Lambda Ausdrücke (auch Closures genannt) sind ein neues und wichtiges Feature in Java 8. Durch sie kann man den Code übersichtlicher halten und unnötigen Boilerplate-Code vermeiden. Außerdem wurden in Java 8 die Collections Bibliothek erweitert, sodass man nun effektiver über Listen iterieren, filtern und extrahieren kann.
Als erstes klären wir aber erst einmal was genau denn Lambda Ausdrücke sind.

Einführung

In vielen Fällen, insbesondere bei der GUI Programmierung, benötigt man sehr viele anonyme Klassen, welche eigentlich nichts weiter beinhalten als eine einzige Methode (z.B. actionPerformed des ActionListeners).
JButton helloButton = new JButton("Hello");
helloButton.addActionListener( new ActionListener() {
  @Override
  public void actionPerformed(ActionEvent e) {
    System.out.println("Hello World!");
  }
});
Eigentlich wollen wir nur eine einzige Zeile ausgeben, benötigen dafür jedoch sehr viel Code. Mit Hilfe von Lambda ausdrücken können wir das zu folgendem Code vereinfachen:
JButton helloButton = new JButton("Hello");
helloButton.addActionListener( e -> System.out.println("Hello World!"));
So haben wir nun einfach 6 Zeilen auf eine einzige geschrumpft. Insbesondere ist der Code nun viel übersichtlicher geworden! Doch wie genau macht Java das nun?

Functional Interfaces

Interfaces mit nur einer einzigen Methoden werden in Java 8 als functional interfaces bezeichnet. Anstelle manuell eine anonyme Klasse von einem dieser Interfaces zu erzeugen, schreibt man direkt den Methodenrumpf in der Lambda Syntax:
Argument List Arrow Token Body
(int x, int y) -> x + y
Der Body kann entweder ein einzelner Ausdruck sein oder ein Block ({ ... }). In der Ausdrucksform wird der Ausdruck ausgewertet und zurückgegeben, wodurch man kein return Statement benötigt. Falls die Typen der Parameter durch ihren Kontext erschlossen werden können, so kann man diese auch weglassen. Betrachten wir dazu nochmal das Beispiel mit dem ActionListener:
helloButton.addActionListener( e -> System.out.println("Hello World!"));
Die addActionListener-Methode benötigt als Parameter einen ActionListener, dieser hat nur eine Methode, welche einen Parameter vom Typ ActionEvent hat. Daher ist es nicht notwendig diesen Typen explizit anzugeben.
Wenn die Methode des entsprechenden functional interfaces nur einen Parameter hat und dessen Typ durch den Compiler festgestellt werden kann, darf man wie im Beispiel die Klammern weglassen. Also direkt  e -> ... anstatt (ActionEvent e) -> ... .

Interne Darstellung

Man könnte erwarten, dass der Java Compiler aus einem Lambda-Ausdruck einfach eine anonyme Klasse macht, jedoch geht Java einen etwas anderen Weg.
Betrachten wir dazu noch einmal das obige Beispiel:
public class TestFrame extends JFrame {
  public TestFrame() {
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    JButton btnHello = new JButton("Hello");
    btnHello.addActionListener(e -> System.out.println("Hello World!"));
    getContentPane().add(btnHello);
    this.pack();
  }
}
Intern erzeugt Java nun innerhalb der TestFrame-Klasse eine neue Methode:
private static void lambda$0(ActionEvent e) {
  System.out.println("Hello World!");
}
Die Signatur der Methode entspricht der Signatur des Interfaces, jedoch ist diese Methode private static. Der Parameter von btnHello.addActionListener(...) wird nun in einen dynamischen Aufruf umgewandelt. Dies ist ein spezieller Bytecode Befehl, welcher die JVM zur Laufzeit dazu veranlasst ein Objekt zu erzeugen, welches dem Interface genügt, aber dessen Methode unsere lambda$0 Methode ist.

Selbstverständlich erweitert der Compiler nicht den Quellcode, sondern er erzeugt direkt modifizierten Bytecode. Die genauen Internas würden aber den Rahmen dieses Tutorials sprengen.

Weitere Beispiele

Hier sind ein paar häufige Anwendungsfälle für Lambda-Ausdrücke

Runnable

Bekanntermaßen müssen updates der Benutzeroberfläche innerhalb des GUI-Threads (Event Dispatch Thread) geschehen. Häufig nutzt man dazu wieder eine anonyme Klasse:
SwingUtilities.invokeLater(new Runnable() [
  public void run() {
    //do something on GUI-Thread
  }
});
Dies ist offensichtlich ein perfekter Kandidat für Lambda-Ausdrücke:
SwingUtilities.invokeLater( () -> { ... } );
Natürlich kann man so auch das Threading insgesamt lösen:
Thread thread = new Thread(() -> {
  //do complex work
});
thread.start();

Comparator

Listen und Arrays werden mit Comparatoren sortiert. Ein Comparator erhält zwei zu vergleichende Objekte als Parameter und gibt -1, 0 oder 1 zurück, je nachdem ob der erste Parameter kleiner, gleich oder größer dem zweiten ist. Betrachten wir nun die folgende Personen-Klasse. Wir möchten später Listen von Personen führen und diese nach dem verschiedenen Eigenschaften einer Person sortieren.
public class Person {
  public String name;
  public int age;
  public String email;

  public Person(String name, int age, String email) {
    this.name = name;
    this.age = age;
    this.email = email;
  }
}

Als erstes erzeugen wir nun eine neue Liste und füllen diese mit Beispieldaten. Anschließend lassen wir die Liste nach Namen sortieren und geben den Inhalt der Liste mit der printPerson-Methode aus.
public static void main(String[] args) throws IOException {
  List<Person> list = new ArrayList<Person>();
  // fill list
  list.add(new Person("Bob", 20, "bob@example.com"));
  list.add(new Person("Alice", 22, "liz@example.com"));
  list.add(new Person("Carol", 30, "karol@example.com"));

  list.sort(new Comparator<Person>() {
    @Override
    public int compare(Person o1, Person o2) {
      return o1.name.compareTo(o2.name);
    }
  });

  for (Person p : list) {
    printPerson(p);
  }
}
public static void printPerson(Person p) {
  System.out.printf("Name: %s, Age: %d, eMail: %s\n", p.name, p.age, p.email);
}

Hier können wir direkt zwei Punkte verbessern. Als erstes natürlich der Comparator:
list.sort((o1, o2) -> o1.name.compareTo(o2.name));
Außerdem können wir die Schleife zur Ausgabe vereinfachen:
list.forEach(Test::printPerson);

Da printPerson eine statische Methode der Klasse Test ist, müssen wir Test::printPerson verwenden. Wenn man Instanzmethoden verwenden möchte, so verwendet man folgende Notation: obj::Methode. Also z.B. this::printPerson oder auch System.out::println.