Lambdas

Das Problem

  • Innere Klassen erlauben die Übergabe von Methoden an andere Methoden
  • Übergabe erfolgt über den Umweg einer Klasse und eines Objektes
  • Syntax ist umständlich und verwirrend
  • Wieso kann man nicht einfach direkt eine Methode übergeben?
public interface Funktion {
    public int apply(int a, int b);
}
public class Berechnung {

    public static int berechne(int input1, int input2, Funktion funktion) {
        int ergebnis = funktion.apply(input1, input2);

        return ergebnis;
    }
}
int ergebnis = Berechnung.berechne(5, 7, new Funktion() {

    @Override
    public int apply(int a, int b) {
        return a + b;
    }
});

System.out.println(ergebnis);

Wenn man sich den Aufruf der Methode berechne mit der anonymen inneren Klasse ansieht, fällt sofort auf, dass trotz der bereits verkürzten Syntax noch extrem viel umständlicher Code geschrieben werden muss. Vor allem, wenn man sich überlegt, dass die Deklaration der Methode apply und das entsprechende Überschreiben der Methode in der anonymen inneren Klasse immer den gleichen Methodenkopf wiederholt.

Wünschenswert wäre eine Syntax, die es erlaubt diese ständigen Wiederholungen zu vermeiden und so den Quelltext übersichtlicher zu gestalten.

Lösung mit Lambda

Seit Java 8 erlauben Lambdas eine verkürzte Definition

int ergebnis = Berechnung.berechne(5, 7, (x, y) -> x + y);
System.out.println(ergebnis);
Funktion add = (x, y) -> x + y;
Funktion sub = (x, y) -> x - y;

int e1 = add.apply(5, 7);
int e2 = sub.apply(10, 8);

Um noch einmal die deutliche Verkürzung der Definition durch Lambdas zu zeigen, hier das zweite Beispiel in klassischer (pre Java 8) Notation.

Funktion add = new Funktion() {
    @Override
    public int apply(int a, int b) {
        return a + b;
    }
};

Funktion sub = new Funktion() {
    @Override
    public int apply(int a, int b) {
        return a - b;
    }
};

int e1 = add.apply(5, 5);
int e2 = sub.apply(10, 8);

Functional Interface

  • Grundlage ist Interface mit einer einzigen abstrakten Methode
    • SAM-Interface (single abstract method interface)
    • funktionales Interface (functional interface)
  • Annotation @FunctionalInterface verhindert Hinzufügen weiterer Methoden
  • Compiler setzt Lambda in eine Implementierung des Interfaces um
@FunctionalInterface
public interface Aktion {
    public void run(String param);
}

Da Lambdas in Java nachträglich in die Sprache aufgenommen wurden und mit den vorhandenen Strukturen harmonieren müssen, konnte man nicht einfach hingehen und, wie z. B. in JavaScript, Funktionen als eigenständige Objekte definieren. Man muss daher Funktionen auf vorhandene Konstruktionen in Java abbilden. Da man bereits vor Java 8 schon Methoden über den Umweg eines Interfaces, einer Implementierung und eines entsprechenden Objektes übergeben konnte, hat man für Lambdas denselben Weg gewählt. Lambdas bieten also eigentlich nur eine einfache Syntax mit der man anonyme innere Klassen definieren kann. Allerdings wird eine Optimierung durchgeführt, die sich auf dem neuen ByteCode invokedynamic abstützt und es erlaubt, keine Klasse für das Lambda generieren zu müssen.

Grundlage eines Lambda-Ausdrucks ist ein Interface mit einer einzigen abstrakten Methode, das manchmal als SAM-Interface oder funktionales Interface bezeichnet wird. Um sicherzustellen, dass ein Interface genau eine abstrakte Methode hat, kann man durch die Annotation @FunctionalInterface den Compiler anweisen, dies zu überprüfen. Fügt man versehentlich eine weitere Methode hinzu (oder entfernt die einzig vorhandene), gibt es wegen der Annotation beim Kompilieren eine Fehlermeldung. Die Annotation sorgt also dafür, dass man nicht versehentlich in ein funktionales Interface eine weitere Methode einführt und damit seine Eigenschaft als funktionales Interface zerstört.

Syntax eines Lambdas

  • Lambda implementiert die Methode des funktionalen Interfaces
  • Welches Interface implementiert wird, ergibt sich aus dem Kontext
    • Typ der Variable
    • Typ des Parameters
  • Aufbau des Lambdas
    1. Parameter der Methode in Klammern (mit oder ohne Typ), durch Komma getrennt. Bei nur einem Parameter kann die Klammer entfallen
    2. Ein Pfeil ->
    3. Rumpf der Methode. Bei nur einem Statement, kein Block { } nötig
  • return ist nicht erforderlich, wenn nur ein Ausdruck im Lambda vorkommt

Wenn im Lambda nur ein Ausdruck vorkommt, ist dessen Ergebnis auch gleichzeitig der Rückgabewert des Lambdas.

Beispiele

  • Ohne Typen: (a, b) -> a + b
  • Mit Typen: (int a, int b) -> a + b
  • Mit Block: (a, b) -> { int s = a + b; return s; }
  • Ohne Klammer: a -> a * a
  • Mit Klammer: (a) -> a * a
  • Ohne Argumente: () -> { System.out.println("Hallo"); }

Der Java-Compiler muss wissen, zu welchem Interface ein Lambda gehört. Diese Information leitet er aus dem Kontext ab, in dem das Lambda vorkommt.

Wird es einer Variable zugewiesen, ergibt sich der Typ des Interfaces aus dem Typ der Variable.

Aktion a = s -> System.out.println(s);

Wird das Lambda an eine Methode übergeben, ergibt sich das funktionale Interface aus dem Typ des Parameters der Methode.

public static void executor(Aktion a) {
    a.run("Hallo");
}

public static void main(String[] args) {
    executor(s -> System.out.println(s));
}

Die Ableitung des Typs aus dem Kontext nennt man bei Lambdas target typing. Durch das Überladen von Methoden kann es passieren, dass mehrere Typen für das Lambda in Frage kommen. In diesem Fall bezieht der Compiler noch zusätzlich den Rückgabetyp der Methode und den des Lambdas mit in die Entscheidung ein, um herauszufinden, welchen Typ das Lambda haben soll. Das folgende Beispiel zeigt dies.

public interface Runner {
    public void run();
}
public interface Caller {
    public int call();
}
public class TargetTyping {
    public void doIt(Runner r) {
        r.run();
    }

    public void doIt(Caller c) {
        c.call();
    }

    public static void main(String[] args) {
        TargetTyping t = new TargetTyping();

        // implementiert Runner
        t.doIt(() -> { return; });

        // implementiert Caller
        t.doIt(() -> { return 1; });
    }
}

Syntaktisch besteht das Lambda aus zwei Teilen, die durch einen Pfeil -> getrennt werden.

Links vom Pfeil stehen die Parameter in Klammern und durch Komma getrennt. Für die Parameter kann der Typ angegeben werden. Dies ist aber unnötig, da sich der Typ aus der Methode des zu implementierenden Interfaces ergibt (Typ-Inferenz). Insofern wird man die Parameter im Normalfall weglassen. Gibt man für einen Parameter den Typ an, muss man es für alle Parameter tun.

Rechts vom Pfeil steht die Implementierung der Methode. Wenn diese nur aus einem einzigen Statement besteht, muss man keinen Block verwenden, sondern schreibt das Statement einzeln hin. Bei mehreren Statements steht der Rumpf des Lambdas in einem Block in geschweiften Klammern ({...}).

// Nur ein Statement, Block unnötig
Aktion a1 = s -> System.out.println(s);

// Mehrere Statements, Block
Aktion a2 = s -> {
    System.out.println(s);
    System.out.println("Nochmal" + s);
};

Kommt in dem Lambda nur ein einziger Ausdruck vor, so wird dieser zurückgegeben. Andernfalls muss man den Rückgabewert durch ein explizites return erzeugen.

Vorgefertigte Interfaces

Im Paket java.util.function gibt es bereits eine Reihe von vorgefertigten funktionalen Interfaces

  • BiConsumer<T,U> – Nimmt zwei Parameter vom Typ T und U und gibt kein Ergebnis zurück
  • BiFunction<T,U,R> – Nimmt zwei Parameter vom Typ T und U und gibt ein Ergebnis vom Typ R zurück
  • BinaryOperator<T> – Nimmt zwei Parameter vom Typ T und gibt ein Ergebnis vom Typ T zurück
  • Consumer<T> – Nimmt einen Parameter vom Typ T und gibt kein Ergebnis zurück
  • Function<T,R> – Nimmt einen Parameter vom Typ T und gibt ein Ergebnis vom Typ R zurück
  • Predicate<T> – Nimmt einen Parameter vom Typ T und gibt einen Wahrheitswert zurück
  • Supplier<T> – Nimmt keinen Parameter und gibt ein Ergebnis vom Typ T zurück

Der Vorteil der bereits vorhandenen Interfaces ist, dass man sich häufig das Schreiben eigener funktionaler Interfaces ersparen kann, da für die meisten Anwendungsbereiche bereits entsprechende Interfaces vorgegeben sind. Da der Name der Methode im Lambda nicht auftaucht, ist die Verwendung der Interfaces aus java.util.function besonders einfach.

Für die primitiven Datentypen gibt es spezielle Interfaces, um das Autoboxing zu vermeiden:

  • BooleanSupplier
  • DoubleBinaryOperator, DoubleConsumer, DoublePredicate, DoubleSupplier, DoubleToIntFunction, DoubleToLongFunction, DoubleUnaryOperator
  • IntBinaryOperator, IntConsumer, IntPredicate, IntSupplier, IntToDoubleFunction, IntToLongFunction, IntUnaryOperator
  • LongBinaryOperator, LongConsumer, LongPredicate, LongSupplier, LongToDoubleFunction, LongToIntFunction, LongUnaryOperator

Beispiel: Vorgefertigte Interfaces

import java.util.function.IntBinaryOperator;

public class Rechner {

    public static int berechne(int input1, int input2,
            IntBinaryOperator funktion) {
        int ergebnis = funktion.applyAsInt(input1, input2);
        return ergebnis;
    }

    public static void main(String[] args) {
        IntBinaryOperator sub = (a, b) -> a - b;
        IntBinaryOperator add = (a, b) -> a + b;

        System.out.println(berechne(5, 3, sub));
        System.out.println(berechne(1, 7, add));
    }
}
import java.util.function.IntBinaryOperator;

public class Rechner {

    public static int berechne(int input1, int input2,
            IntBinaryOperator funktion) {
        int ergebnis = funktion.applyAsInt(input1, input2);
        return ergebnis;
    }

    public static void main(String[] args) {
        System.out.println(berechne(5, 3, (a, b) -> a - b));
        System.out.println(berechne(1, 7, (a, b) -> a + b));
    }
}

Das Beispiel wurde bereits einmal im Kapitel zu Interfaces gezeigt, dort war die Implementierung aber deutlich umfangreicher und benötigte ein selbst geschriebenes Interfaces namens Funktion.

public interface Funktion {
    public abstract int apply(int o1, int o2);
}
public class Addition implements Funktion {

    public int apply(int o1, int o2) {
        return o1 + o2;
    }
}
public class Subtraktion implements Funktion {

    public int apply(int o1, int o2) {
        return o1 - o2;
    }
}
public class Berechnung {

    public static int berechne(int input1, int input2, Funktion funktion) {
        int ergebnis = funktion.apply(input1, input2);
        return ergebnis;
    }

    public static void main(String[] args) {
        Funktion sub = new Subtraktion();
        Funktion add = new Addition();

        System.out.println(berechne(5, 3, sub));
        System.out.println(berechne(1, 7, add));
    }
}

Capture

  • Lambda hat bei der Deklaration Zugriff auf die umgebenden Variablen
  • Werte sind später beim Aufruf des Lambdas vorhanden
  • Variablen dürfen nach Erzeugung des Lambdas nicht mehr verändert werden (am besten final machen) (muss effectively final sein)
  • Lambdas mit Zugriff auf externe Variablen nennt man capturing lambdas
int minusEins = -1;

Comparator<String> cmp = (a, b) -> a.compareTo(b) * minusEins;

Das Lambda nimmt bei der Erzeugung die umgebenden Variablen mit, die aus dem Lambda heraus benutzt werden. Man kann es sich so vorstellen, dass es die transitive Hülle (transitive closure) der benutzen Daten bestimmt und behält. Aus diesem Grund nennt man im Englischen die Lambdas Closures.

Die Variable, die im Lambda verwendet wird, muss zwar nicht final deklariert werden, der Compiler überprüft aber, ob sie nach der Definition des Lambdas noch einmal verändert wird. Passiert dies, wirft der Compiler eine Fehlermeldung. Daher spricht man in der Java-Spezifikation von Variablen, die effectively final sind. Prinzipiell könnte man die Variablen einfach final deklarieren, ohne dass sich etwas ändern würde, wie es bei lokalen Klassen gefordert wird. Der Vorteil, dass Lambdas dies nicht erzwingen ist, dass man vorhandenen Code nicht anpassen muss.

int minusEins = -1;
Comparator<String> cmp = (a, b) -> a.compareTo(b) * minusEins;
minusEins = 2; // FEHLER!!
\Capture.java:10: error: local variables referenced from
a lambda expression must be final or effectively final
        Comparator<String> cmp = (a, b) -> a.compareTo(b) * minusEins;
                                                            ^
1 error

Aus den Regeln für den Zugriff auf Variablen aus dem Lambda folgt, dass Lambdas letztendlich nicht Variablen, sondern Werte mitnehmen.

Lambdas als Rückgabewerte

Methoden können Lambdas zurückgeben

public class ComparatorFactory {

    public Comparator<String> createComparator() {
        return (a, b) -> a.compareTo(b);
    }
}

Dass man Lambdas als Rückgabewert verwenden kann, ist nicht verwunderlich, wenn man sich vor Augen führt, dass es sich einfach um eine verkürzte Schreibweise für die Implementierung von Interfaces und das Anlegen eines Objektes dieser Implementierung handelt. Würde man für das obenstehende Beispiel anonyme innere Klassen verwende, dann ergäbe sich dasselbe Ergebnis, nur der Code wäre länger.

public class ComparatorFactory {

    public Comparator<String> createComparator() {
        return new Comparator<String>() {

            @Override
            public int compare(String a, String b) {
                return a.compareTo(b);
            }
        };
    }
}

Scoping bei Lambdas

  • Lambdas sind eine verkürzte Schreibweise für anonyme innere Klassen
  • Wichtiger Unterschied: this bezieht sich nicht auf das Objekt des Lambdas, sondern auf das umgebende Objekt

Das Verhalten der Lambdas bezeichnet man als lexical scoping: Ein Bezeichner im Lambda bezieht sich direkt auf den umgebenden Kontext, wie man es im Programm sieht. Die Tatsache, dass das Lambda eigentlich ein Objekt ist, wird also nicht berücksichtigt. Daher bezieht sich this in einem Lambda nicht auf das (heimlich erzeugte) Lambda-Objekt, sondern das umgebende Objekt.

Dieses Verhalten ist insofern sehr sinnvoll, als dass die Lambda-Objekte nur deshalb erzeugt werden, weil die Funktionsweise von Java dies erfordert, da Methoden nur mithilfe von Objekten transportiert werden können. Durch das Scoping der Lambdas wird dafür gesorgt, dass sich dieses Implementierungsdetail nicht übermäßig in den Vordergrund drängt.

public class Scoping {

    public String toString() {
        return "Scoping";
    }

    public Runnable createRunner() {
        return () -> System.out.println(toString());
    }

    public static void main(String[] args) {
        new Scoping().createRunner().run();
    }
}
Scoping

Durch das Scoping bei Lambdas ist hier klar, dass mit toString() die entsprechende Methode der umgebenden Klasse gemeint ist. toString() ist nur eine verkürzte Schreibweise für this.toString() und aufgrund des Scopings von Lambdas bezieht sich this auf das Objekt von Scoping und nicht das (unsichtbare) Lambda-Objekt.

public class Scoping {
    public String toString() {
        return "Scoping";
    }

    public Runnable createRunner() {
        return new Runnable() {
            public void run() { System.out.println(toString()); }
        };
    }

    public static void main(String[] args) {
        new Scoping().createRunner().run();
    }
}
de.smits_net.pr2.lambda.Scoping$1@15db9742

Verwendet man anstatt eines Lambdas eine anonyme innere Klasse, so bezieht sich this innerhalb der anonymen Klasse auf das eigene Objekt, sodass nicht die toString-Methode von Scoping, sondern die Methode vom anonymen Objekt verwendet wird. Da letzter nicht überschrieben wurde, kommt die geerbte Standardimplementierung von Object zum Zuge, die den Namen der Klasse und den Hash-Code ausgibt.

Methodenreferenz

  • Lambda gehört fest zu einem Interface
  • Was tun, wenn man schon eine Methode hat, die passt aber das Interface nicht implementiert?
  • Lösung: Methodenreferenz (method references)
    • Statische Methoden: Klasse::Methode,
      z. B. Integer::parseInt
    • Nicht-statische Methoden: Referenz::Methode,
      z. B. "Hallo"::toUpper
    • Konstruktor: Klasse::new, z. B. String::new, String[]::new
  • Kann nur in Zuweisung an eine Variable vom Typ eines Functional Interfaces benutzt werden

Die Methodenreferenz stellt eine einfache Brücke zwischen Lambdas und bereits vorhandenen Methoden dar. Wenn eine vorhandene Methode von der Struktur (Anzahl und Typ der Parameter, Rückgabetyp) auf das funktionale Interface passt, kann man anstatt eines Lambdas einfach die Methodenreferenz verwenden.

public class Funktionssammlung {

    public static int add(int a, int b) {
        return a + b;
    }

    public static int sub(int a, int b) {
        return a - b;
    }
}
public class Main {
    public static void main(String[] args) {
        System.out.println(Funktion.rechne(5, 5, Funktionssammlung::add));
        System.out.println(Funktion.rechne(10, 8, Funktionssammlung::sub));
    }
}

Das Interface Funktion wurde bereits benutzt und sieht wie folgt aus:

public interface Funktion {
    public int apply(int a, int b);

    public static int rechne(int a, int b, Funktion fkt) {
        return fkt.apply(a, b);
    }
}

Copyright © 2025 Thomas Smits