Lambda-Ausdrücke und funktionale Interfaces in Java


Funktionale Programmierung ist ein sehr beliebtes Programmierparadigma, daher ist es in vielen modernen Sprachen zu finden. Seit Version 8 bietet auch Java mit Lambda-Ausdrücken und funktionalen Interfaces Möglichkeiten zur funktionalen Programmierung an. In diesem Artikel schauen wir uns das genauer an.

Was ist funktionale Programmierung?

In der Objektorientierten Programmierung haben Methoden bzw. Funktionen Daten als Eingabeparameter und Rückgabewerte. 
Die funktionale Programmierung ermöglicht uns neben Daten zusätzlich Funktionen zu verknüpfen und sie als Eingabeparameter und Rückgabe zu definieren. Zum Beispiel können wir einem Bubblesort Sortieralgorithmus eine Funktion übergeben, mit der der Sortieralgorithmus über die Reihenfolge der zu sortierenden Daten entscheidet. Der selbe Sortieralgorithmus kann so z.B. abhängig vom übergebenen Funktions-Parameter eine Liste von Autos nach Kaufpreis absteigend oder nach Erstzulassungs-Datum aufsteigend sortieren.

Weitere Infos zur funktionalen Programmierung im Allgemeinen findet ihr hier:
https://de.wikipedia.org/wiki/Funktionale_Programmierung

Lambda-Ausdrücke

In Java Version 8 wurde der Lambda-Ausdruck eingeführt, um die funktionale Programmierung mit Java zu ermöglichen. Funktionen können also in Form eines Lambda-Ausdrucks an Methoden als Parameter übergeben werden. In Java erkennt man den Lambda am Pfeiloperator: ->

Kompakte Lambdas

Im folgenden Code-Beispiel sortieren wir eine Liste von Wörter bzw. Strings anhand der Häufigkeit des Buchstaben "a" in jedem String:

    List<String> languages = new ArrayList<>(
        List.of("Kotlin", "Java", "Python", "Haskell", "Scala"));
    languages.sort((a, b) -> StringUtils.countOccurrencesOf(b, "a") 
        - StringUtils.countOccurrencesOf(a, "a"));
    
    System.out.println(languages);
    // Console output: [Java, Scala, Haskell, Kotlin, Python]

  • Das Java List Interface bietet eine default Methode sort an, welche anhand eines von Java bereitgestellten Sortieralgorithmus Instanzen der Klasse List sortieren kann. Diese default Methode weiß welchen Sortieralgorithmus sie verwenden muss - keine Angst, es ist nicht Bubblesort 😉. Sie weiß aber nicht woran sie erkennt, welches von 2 Listenelementen das größere ist bzw. welches der beiden im sortieren Ergebnis zuerst kommt.
  • Zum Vergleich zweier Listenelemente verwendet die Methode sort den als Parameter übergebenen Lambda-Ausdruck: 
    (a, b) -> StringUtils.countOccurrencesOf(b, "a") 
        - StringUtils.countOccurrencesOf(a, "a")
  • StringUtils ist eine Klasse aus dem Spring Framework, die diverse Operationen für Strings zur Verfügung stellt. Unter anderem die Methode countOccurrencesOf zum Zählen, wie häufig der Sub-String "a" ersten Parameter vom Typ String vorkommt.
  • Der Lambda-Ausdruck hat hier zwei Eingabe-Parameter a und b, welche vom Typ String sind - das erkennt der Compiler für uns anhand des Kontextes List<String>.
    Für jeden Eingabeparameter wird gezählt wie oft der Sub-String "a" vorkommt. Die beiden Zählergebnisse sind vom primitiven Typ
    int und werden dann voneinander subtrahiert.
    Je nachdem ob das Subtraktionsergebnis positiv, 0 oder negativ ist, wissen wir, welcher der Eingabeparameter
    in der sortierten Liste zuerst kommt.
    Die Eingabeparameter stehen also bei Lambda-Ausdrücken in Java links vom Pfeiloperator. Gibt es, wie hier, mehr als einen Parameter, so müssen sie in Klammern stehen.
    Rechts vom Pfeiloperator steht der eigentliche Lambda-Ausdruck, der in diesem Beispiel die beiden Strings miteinander vergleicht.

Komplexe Lambdas

Lambda-Ausdrücke können, wie der zuvor gezeigt, kompakt geschrieben sein. Ihr könnt sie aber auch ausführlich mit beliebig komplexen Ausdrücken schreiben. Hier noch Mal die gleiche Sortierfunktion in einer ausführlichen Schreibweise mit zwei zusätzlichen Zwischenschritten:

    languages.sort((String a, String b) -> {
        int countOfA = StringUtils.countOccurrencesOf(a, "a");
        int countOfB = StringUtils.countOccurrencesOf(b, "a"); 
        return countOfB - countOfA;
    });
  • Für die Eingabeparameter wurde hier noch der Typ String festgelegt.
  • Die Sortierfunktion besteht aus drei Anweisungen, die wie üblich in Java mit Semikolon getrennt sind. Da es jetzt nicht nur eine einzelne Anweisung (wie im ersten Beispiel) ist, muss der Lambda-Audruck rechts vom Pfeiloperator in geschweiften Klammern stehen und sein Ergebnis mit return zurückgeben.

Anonyme Klassen

Vor Java 8 musste man anonyme Klassen anstelle von Lambda-Ausdrücken verwenden. Das geht in neueren Java Versionen auch noch, produziert aber deutlich mehr geschwätzigen Standard- bzw. Boilerplate-Code:

    languages.sort(new Comparator<String>() {
        @Override
        public int compare(String a, String b) {
            return StringUtils.countOccurrencesOf(b, "a") 
                - StringUtils.countOccurrencesOf(a, "a");
        }
    });
  • Die hier gezeigte anonyme Klasse ist eine Instanz des Comparator Interfaces und überschreibt die Methode compare. Die Logik in der compare Methode entspricht 1:1 den zuvor gezeigten Lambda-Ausdrücken.
  • Vergleicht man das mit den Lambda-Ausdrücken stellt man fest, dass bei anonymen Klassen relativ viel Boilerplate-Code durch die Instanziierung der anonymen Klasse (new Comparator...) und Deklaration der Methode (public int compare...) entsteht. Bei Lambda-Ausdrücken sparen wir uns das und verbessern so die Lesbarkeit unseres Codes.
  • Wenn ihr euch nun die Methode sort im List Interface genauer anschaut, stellt ihr auch fest, dass diese als Eingabe-Parameter eine Instanz von Comparator<? super E> erwartet. Comparator ist ein funktionales Interface. Wie das mit Lambda-Ausdrücken zusammenhängt, erkläre ich im nächsten Abschnitt...

Funktionale Interfaces

In Java ist ein funktionales Interface ist ein Interface, das genau eine abstrakte Methode hat. Das zuvor gezeigte Comparator Interface sieht so aus:

    @FunctionalInterface
    public interface Comparator<T> {
    
        int compare(T o1, T o2);
    
        // several default methods & static methods, 
        // but no more non-static, public, abstact methods.
    ...
  • Die Annotation @FunctionalInterface ist optional. Wenn sie gesetzt ist, stellt der Compiler sicher, dass das annotierte Interface nur eine public, abstract Methode hat.
  • Die Methode compare ist implizit public und abstract - man muss diese Schlüsselworte nicht explizit setzen.
  • default und static Methoden sind nicht abstrakt, daher kann ein funktionales Interface beliebig viele von ihnen haben.

Lambda-Ausdrücke sind die Implementierung eines funktionalen Interfaces!

Die Umwandlung eines Lambda-Ausdrucks in eine anonyme Klasse ist immer möglich. Eclipse bietet diese Umwandlung sogar per Klick an:
Cursor im Lambda-Ausdruck => Strg + 1 => Convert to anonymous class creation

Die Umwandlung einer anonymen Klasse in einen Lambda, funktioniert nur dann, wenn die anonyme Klasse die Implementierung eines funktionalen Interfaces ist.
Lambdas sind also immer Implementierung eines funktionalen Interfaces.

Funktionale Interfaces im JDK

Im Package java.util.function befinden sich die wichtigsten funktionalen Interfaces, welche ein Großteil der eingesetzten Lambda-Ausdrücke implementiert. Die folgenden solltet ihr unbedingt kennen:
  • Consumer - void accept(T t);
    Ein Eingabe-Parameter wird verarbeitet, es gibt aber keine Rückgabe.
  • Supplier - T get();
    Stellt einen Rückgabewert zur Verfügung, hat aber keinen Input.
    Supplier und Consumer habe ich in diesem Blog-Artikel als Daten-Input für Streams und zur Verarbeitung von Daten aus einem Stream verwendet: webflux.html 
  • Function - R apply(T t);
    Wandelt einen Eingabe-Parameter t in einen Rückgabewert um. Function wird zum Beispiel zum Umwandeln von Daten in einem Stream mit der Methode map verwendet.
  • Predicate - boolean test(T t);
    Prüft anhand des Eingabe-Parameters, ob die Rückgabe true oder false ist. Predicate wird zum Beispiel zum Filtern von Daten in einem Stream mit der Methode filter verwendet.
  • Runnable - void run();
    Führt einen Task aus, der weder Parameter noch Rückgabewert hat - bekannt als das Interface von Threads.
Neben diesen gibt es noch weitere funktionale Interfaces, die vom JDK bereitgestellt werden. Diese sind zum Beispiel besser geeignet für den Umgang mit primitiven Datentypen: DoubleConsumer, IntPredicate, LongFunction, usw. Ihre Namen bestehen immer aus dem primitiven Datentyp und dem jeweiligen funktionalen Interface. Wenn der Rückgabetyp der Funktion long sein soll, wäre es das ToLongFunction Interface.

Häufig benötigen wir auch einen zweiten Eingabe-Parameter, daher gibt es auch die funktionalen Interfaces, die einen zweiten Eingabe-Parameter unterstützen: BiConsumer, BiFunction, BiPredicate. Das zuvor gezeigte funktionale Interface Comparator entspricht daher dem Interface ToIntBiFunction mit der abstrakten Methode int applyAsInt(T t, U u);ToIntBiFunction  ist allerdings etwas generischer, da die beiden Parameter unterschiedliche Typen haben können.

Eigene funktionale Interfaces schreiben

...könnt ihr tun, funktioniert genau so wie beim gezeigten Comparator Interface. Ob es wirklich nötig und sinnvoll ist, ist eine komplexe Frage. Wenn ihr z.B. eigene generische Algorithmen geschrieben habt, die aber nur mit bestimmten Eingabe- oder Ausgabetypen funktionieren, könnte ein eigenes funktionales Interface die Verständlichkeit verbessern.

Falls euch 2 Eingabe-Parameter nicht ausreichen, müsst ihr auch selbst funktionale Interfaces erstellen. In diesem Fall solltet ihr euch aber fragen, ob der dritte oder die weiteren Eingabe-Parameter wirklich notwendig sind? In der Regel ist das ein Zeichen für schlechten Code. Im Buch Clean Code warnt der Autor Robert C. Martin auch vor der Verwendung von Funktionen mit drei Eingabeparametern (Triads), da sie signifikant schwerer zu verstehen sind als Funktionen mit zwei Eingabeparameters (Dyadic Functions).

Fazit

Lambda-Ausdrücke werden mittlerweile in Java Programmen sehr häufig eingesetzt, da man mit ihnen deutlich kompakteren Code schreiben kann. Insbesondere in Kombination mit Streams sind sie ein tolles Mittel, um lange Schleifen zu ersetzen. Funktionale Interfaces sind eine notwendige Voraussetzung für den Einsatz von Lambdas. Daher kann ich euch wirklich nur empfehlen die vom JDK angebotenen funktionalen Interfaces zu kennen, da sie euch tolle neue Dinge wie z.B. die reaktive Programmierung mit Spring WebFlux ermöglichen (webflux.html).

Kommentare

Beliebte Posts aus diesem Blog

OpenID Connect mit Spring Boot 3

CronJobs mit Spring

Kernkonzepte von Spring: Beans und Dependency Injection