Java-Lambdas Einführung: Unterschied zwischen den Versionen

Aus MimiPedia
Keine Bearbeitungszusammenfassung
Keine Bearbeitungszusammenfassung
 
(2 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 4: Zeile 4:
funktionalen Programmiersprachen wie LISP, Haskell, Clojure oder Scala aufsetzen und das sich fundamental vom
funktionalen Programmiersprachen wie LISP, Haskell, Clojure oder Scala aufsetzen und das sich fundamental vom
Modell der Turing-Maschine unterscheidet, das den imperativen Sprachen wie Java oder C zugrunde liegt.
Modell der Turing-Maschine unterscheidet, das den imperativen Sprachen wie Java oder C zugrunde liegt.
Der Name leitet sich vom griechischen Buchstaben {{Lambda}} ab den Church für seine Notation verwendete.
Der Name leitet sich vom griechischen Buchstaben {{lambda}} ab den Church für seine Notation verwendete.
Weil das kürzer ist -- und cooler aussieht -- steht im Folgenden {{lambda}} für "Lambda".
Weil das kürzer ist -- und cooler aussieht -- steht im Folgenden {{lambda}} für "Lambda".


Zeile 20: Zeile 20:


== Ein Beispiel mit konventionellen Java-Mitteln ==
== Ein Beispiel mit konventionellen Java-Mitteln ==
Wir haben hier eine Methode, die die Zahlen einer Liste durchgeht und zu jedem Wert der Liste den Wert 1
Wir haben hier eine Methode, die die Zahlen einer Liste durchgeht und die Summe der Zahlen als Ergebnis liefert.
hinzuzählt. Die bearbeiteten Zahlen werden in einer neuen Liste als Ergebnis zurückgegeben:
{{java|code=
{{java|code=
public List<Integer> verarbeite(List<Integer> liste) {
public Integer verarbeite(List<Integer> liste) {
     List<Integer> result = new ArrayList<>();
     Integer result = 0;
     for (Integer x : liste) {
     for (Integer x : liste) {
         result.add(x + 1);
         result += x;
     }
     }
     return result;
     return result;
}
}
}}
}}
Jedesmal, wenn eine andere Funktion (als "plus eins") zur Berechnung verwendet werden soll, muß die Methode kopiert und angepaßt werden – nicht schön.
Den Rahmen dieser Methode könnte man benutzen um andere Berechnungen anzustellen. Etwa zu zählen wieviele gerade Zahlen die
Man kann die Methode aber auch mit konventionellen Mitteln so erweitern, daß auf die Zahlen eine beliebige Funktion angewandt werden kann.
Liste enthält. Aber jedemal, wenn eine neue Funktion zur Berechnung verwendet werden soll, muß die Methode kopiert und angepaßt
Dafür definiert man zunächst ein Interface
werden – das ist nicht schön.
Zunähst einmal muß die Methode so erweitert werden, daß auf die Zahlen eine beliebige Funktion angewandt werden kann. Das geschieht
in Java immer auf die gleiche Art und Weise, nämlich mit Hilfe einer Klasse oder eines Interface. Wir betrachten hier nur die
Interface-Variante, weil sie felxibler ist. Wir definieren also ein Interface das eine Funktion zur Berechnung enthält:
{{java|code=
{{java|code=
interface Funktion {
interface Function {
     Integer calc(Integer x);
     Integer apply(Integer x);
}
}
}}
}}
Nun erweitert man die Methode so, daß sie eine Instanz dieses Interfaces als Argument übernimmt und
Nun erweitert man die Methode so, daß sie eine Instanz dieses Interfaces als Argument übernimmt und
anstelle des fest verdrahteten Ausdrucks {{java|x + 1}} für die Verechnung verwndet:
anstelle des fest verdrahteten Ausdrucks {{java|x}} für die Verechnung verwendet:
{{java|code=
{{java|code=
public List<Integer> verarbeite(List<Integer> liste, Funktion fkt) {
public Integer verarbeite(List<Integer> liste, Function fkt) {
     List<Integer> result = new ArrayList<>();
     Integer result = 0;
     for (Integer x : liste) {
     for (Integer x : liste) {
         result.add(fkt.calc(x));
         result += fkt.apply(x);
     }
     }
     return result;
     return result;
}
}
}}
}}
Um die Methode {{java|verarbeite()}} verwenden zu können benötigen wir ein Objekt, das das Interface {{java|Funktion}} implementiert.
Um die Methode {{java|verarbeite}} verwenden zu können benötigen wir nun ein Objekt, das das Interface {{java|Function}} implementiert.
Nur mit einem solchen Objekt kann die Methode verwendet werden. Möchten wir zum Beispiel folgende Klasse verwenden:
Nur mit einem solchen Objekt kann die Methode verwendet werden. Um die Summe der Quadrate zu berechnen definieren wir daher eine Klasse
die das Interface in der geeigneten Weise implementiert:
{{java|code=
{{java|code=
class Calc {
class Square implements Function {
    public Integer apply(Integer x) {
        return x * x;
    }
}
}}
Die Verwendung sieht dann vielleicht so aus:
{{java|code=
Integer quadrate = verarbeite(meineListe, new Square());
}}
Das funktioniert natürlich nur dann, wenn wir die Klasse mit der Berechnung frei implementieren und vor allem den Namen der Methode
so benennen können wie es das Interface erfordert. Bei einer existierenden Klasse -- vor allem wenn sie final ist, geht das nicht.
Möchten wir zum Beispiel folgende Klasse verwenden:
{{java|code=
final class Calc {
     public Integer doppel(Integer x) {
     public Integer doppel(Integer x) {
         return 2 * x;
         return 2 * x;
Zeile 59: Zeile 76:
}
}
}}
}}
benötigen wir zusätzlich einen Wrapper oder Adapter der die Methode {{java|doppel}} auf die Methode {{java|calc}} des Interface {{java|Funktion}}
benötigen wir zusätzlich einen Wrapper oder Adapter der die Methode {{java|doppel}} auf die Methode {{java|apply}} des Interface {{java|Function}} abbildet. Anstelle einer eigenen Klasse kann man beim Aufruf von {{java|bearbeite}} ein Objekt einer anonymen Klasse instantiieren:
abbildet. Anstelle einer eigenen Klasse kann man beim Aufruf von {{java|bearbeite}} ein Objekt einer anonymen Klasse instantiieren:
{{java|code=
{{java|code=
verarbeite(meineListe, new Funktion() {
verarbeite(meineListe, new Funktion() {
     public Integer calc(Integer x) {
     public Integer apply(Integer x) {
         return new Calc().doppel(x);
         return new Calc().doppel(x);
     }
     }
});
});
}}
}}
Und das ist so unübersichtlich wie aufwendig.
Und das ist so unübersichtlich wie aufwendig. Für statische Methoden führt überhaupt kein Weg am Wrapper vorbei,
denn statische Methoden sind für ein Interface im konventionellen Java unerreichbar.
 
== Was da stört ==
Die parameterisierte Methode {{java|verarbeite}} ist -- so wie sie ist -- eine gute Lösung.
Da brauchen wir nichts zu ändern, entscheidend ist dabei, daß ein Interface mit einer einzigen Methode verwendet wird.
 
Wirklich unelegant ist der Aufruf der Methode. Keine der Lösungen ist befriedigend und hat Nachteile.
Klassen die direkt verwendet werden sollen müssen ein gegebenes Interface implemenieren. Eine Bedingung
die sich oft gar nicht erfüllen läßt. Ist die Klasse final, läßt sie sich nicht einmal ableiten...
 
Verwendet man statt dessen eine Wrapperklasse, muß man eine weitere Klasse definieren die dem Code keine
Funktionalität hinzufügt, sonder nur Code zur Verwaltung enthält. Das Ausweichen über anonyme Klassen scheint
elegant, erzeugt aber ebenfalls zusätzlichen Code und erzeugt tatsächlich zusätzliche Klassen.


== Und jetzt mit Lambdas ==
== Und jetzt mit Lambdas ==
Um uns dem Problem zu nähern, beginnen wir diesmal bei der Funktion die auf die Zahlen angewandt werden soll.
Wir erinnern uns: Ein {{lambda}}-Ausdruck repräsentiert eine Funktion.
Das ist zunächst die Inkrementierungs-Funktion.
Was unsere Java-Lösungen oben so schwerfällig erscheinen läßt ist der Versuch eine ''einzelne'' Funktion
in ein Java-Objekt zu verpacken. Das wird noch dadurch erschwert, daß Java stark typisiert ist und sehr kleinlich
bei der Frage welche Eigenschaften die Klasse eines Objekts haben muß damit es verwendet werden kann.
Im Grunde genommen möchten wir doch oben nur die Funktion {{java|f(x) -> x}} ersetzen durch die Funktion {{java|f(x) -> x * x}}.


Wir definieren dafür eine Variable und weisen ihr einen {{lambda}}-Ausdruck zu, der die Inkrementierungs-Funktion implementiert.
Springen wir also direkt hinein und betrachten einen {{lambda}}-Ausdruck der die Quadrat-Funktion darstellt.
im Anschluß wird der Ausdruck ausführlich beschrieben. Vollständig ausgeschrieben sieht das so aus:
Er wird einer Variable vom Typ unseres oben definierten Interface zugewiesen -- warum das geht betrachten wir später,
{{java|code=Function<Integer, Integer> foo = (Integer x) -> {return x+1;};}}
wir konzentrieren uns zunächst auf den {{lambda}}-Ausdruck rechts :
Wir wenden uns zunächst der ''rechten'' Seite der Zuweisung zu, das ist der eigentliche Lamnda-Ausdruck.
{{java|code=
Den Typ der Variable {{java|foo}} nehmen wir im nächsten Abschnitt unter die Lupe.
Function square = (Integer x) -> { return x * x; }
}}
Ein {{lambda}}-Ausdruck entspricht der Definition einer mathematischen Funktion.
Ein {{lambda}}-Ausdruck entspricht der Definition einer mathematischen Funktion.
Links des {{java|->}}-Operators steht die Parameter-Liste (die Parameter
Links des {{java|->}}-Operators (der ist neu in Java 8) steht die Liste der durch Komma getrennten Paramter mit ihren Typen.
werden durch Komma getrennt, im Beispiel haben wir nur einen Parameter). Rechts des Operators steht der
Im Beispiel haben wir einen Parameter vom Typ {{java|Integer}}. Die Parameter-Liste wird in runde Klammern gesetzt.
auszuwertende Ausdruck, ein – nahezu – beliebiger Ausdruck, der einen Wert des Ergebnistyps (hier: {{java|Integer}}) liefert.
 
Rechts des Operators steht der auszuwertende Code-Block, genau wie bei einer Methode. Wenn der {{lambda}}-Ausdruck einen anderen
Ergebnis-Typ hat als {{code|void}}, muß er mit einem {{java|return}}-Statement einen Wert des entsprechenden Ergebnistyps (hier:
{{java|Integer}}) liefert. Der Ergebnistyp wird hier nicht explizit angegeben, er ergibt sich aus dem Ausdruck im
{{java|return}}-Statement. Java ist extrem smart was das bestimmen nicht angegebener Typen anbelangt und wir können froh sein,
daß uns unsere IDE so viele Informationen darüber mitteilt.


Wir haben den {{lambda}}-Ausdruck oben in seiner vollen Ausführlichkeit formuliert.
Um den Ausdruck kompakter und dadurch übersichtlicher zu machen, lassen sich einige Teile weglassen:
Um den Ausdruck kompakter und dadurch übersichtlicher zu machen, lassen sich einige Teile weglassen:


Besteht der Ausdruck lediglich aus einem {{java|return}}-Statement, kann man die Klammern samt {{java|return}} weglassen:
Besteht der Ausdruck lediglich aus einem {{java|return}}-Statement, kann man die Klammern samt {{java|return}} weglassen:
{{java|code=Function<Integer, Integer> foo = (Integer x) -> x + 1;}}
{{java|code=Function square = (Integer x) -> x * x;}}
Wenn aus dem Kontext klar ist, welche Typen die Parameter der Parameter-Liste haben, kann man auch die Typen weglassen:
Wenn aus dem Kontext klar ist, welche Typen die Parameter der Parameter-Liste haben, kann man auch die Typen weglassen:
{{java|code=Function<Integer, Integer> foo = (x) -> x + 1;}}
{{java|code=Function square = (x) -> x *x;}}
Wenn die Parameter-Liste nur einen einzigen Parameter enthält, können wir die Klammern um den Parameter weglassen:
Wenn die Parameter-Liste nur einen einzigen Parameter enthält, können wir die Klammern um den Parameter weglassen:
{{java|code=Function<Integer, Integer> foo = x -> x + 1;}}
{{java|code=Function square = x -> x *x;}}
Wir können die Methode nun so umschreiben, daß sie unseren {{lambda}}-Ausdruck akzeptiert:
Da {{java|square}} vom Typ des Interfaces {{java|Function}} ist, können wir jeden der gezeigten Ausdrücke
verwenden um unsere Methode aufzurufen. Am kompaktesten ist die letzte:
{{java|code=verarbeite(liste, x -> x * x);}}
=== {{lambda}}-Ausdrücke als Interfaces ===
Wir haben oben -- als wäre das selbstverständlich -- den {{lambda}}-Ausdruck mit einem Interface gleichgesetzt.
Das funktioniert dann, wenn das Interface genau eine (nicht-statische) Funktion definiert deren Typ-Signatur
dem {{lambda}}-Ausdruck entspricht. Das heißt, die Liste der Eingabe-Typen und der Ausgabe-Typ müssen übereinstimmen:
{{java|code=
{{java|code=
public List<Integer> verarbeite(List<Integer> liste, Function<Integer, Integer> fkt) {
interface Function {
     List<Integer> result = new ArrayList<>();
     Integer apply(Integer x);
    for (Integer x : liste) {
        result.add(fkt.apply(x));
    }
    return result;
}
}
}} 
und
{{java|code=
Function square = (Integer x) -> { return x * x; }
}}
}}
Auf den Ausdruck {{java|fkt.apply(x)}} werden wir später zurückkommen, einstweilen genügt uns zu wissen, daß damit der
Man könnte sich vorstellen, daß ein {{lambda}}-Ausdruck eine ultrakompakte Beschreibung einer anonymen Klasse ist
{{lambda}}-Ausdruck {{java|fkt}} mit dem Argument {{java|x}} ausgewertet wird. Wir können die Methode {{java|verarbeite()}} nun so aufrufen:
die das Interface implementiert. Diese Vorstellung geht allerdings nicht weit genug.
{{java|code=verarbeite(liste, x -> x + 1);}}
 
und der Aufruf mit dem Wrapper um die Klasse {{java|Calc}} sieht nun so aus:
Tatsächlich ist der {{lambda}}-Ausdruck gleichwertig ist mit ''jeder'' Methode ''jeder'' Klasse deren Parameter-Liste die gleichen Typen hat und den gleichen Ergebnis-Typ und das unabhängig davon wie die Metode heißt. Und genau hier liegt die Superkraft des {{lambda}}-Ausdrucks.
{{java|code=verarbeite(liste, x -> new Calc().doppel(x));}}
Denn auf diese Weise läßt sich jede Methode als {{lambda}}-Ausdruck verwenden. Wie das geht, wird im nächsten Abschnitt erklärt.
Wenn das mal kein Fortschritt ist –- und es wird noch besser!


=== Funktionale Interfaces ===
Zudem "implementiert" der {{lambda}}-Ausdruck ''jedes'' Interface das eine einzige Methode besitzt deren typ-Signatur der des
Betrachten wir nun den ''Typ'' des {{lambda}}-Ausdrucks. {{java|Function}} ist ein generisches Interface des JDK, das eine
{{lambda}}-Ausdrucks emtspricht also zum Beispiel auch:
einzige Methode definiert:
{{java|code=
{{java|code=
public interface Function<T, R> {
interface Blafasel {
     R apply(T t);
     Integer schnufusel(Integer x);
}
}
}}
}}
Man könnte sagen, daß der {{lambda}}-Ausdruck einer gedachten Klasse {{java|MyFunction}} entspricht die so aussieht:
{{java|code=
class MyFunction {
    Integer apply(Integer t){
        return t + 1;
    }
}
}}
Der Unterschied des {{lambda}}-Ausdrucks zur Methode besteht darin, daß der {{lambda}}-Ausdruck gleichwertig ist mit jeder Methode jeder Klasse deren Parameter-Liste die gleichen Typen hat (also hier einen {{java|Integer}}) und den gleichen
Ergebnis-Typ (hier ebenfalls {{java|Integer}}). Konkret heißt das, daß das {{lambda}} die Methode {{java|calc}} aus unserem Ausgangs-Interface implementiert:
{{java|code=
interface Funktion {
    Integer calc(Integer x);
}
}}
Diese Äquivalenz geht sogar noch einen Schritt weiter: Das {{lambda}} {{java|foo}} implementiert jedes Java-Interface das
genau eine nicht-statische Methode hat die einen {{java|Integer}} als Parameter übernimmt und einen {{java|Integer}} als Ergebnis
liefert. Das {{lambda}} {{java|foo}} implementiert die einzige Methode des Interface {{java|Funktion}} und kann überall dort verwendet
werden, wo ein Objekt verlangt wird das das Interface {{java|Funktion}} implementiert – wir hätten unsere Methode also
gar nicht auf das JDK-Interface {{java|Function}} umzuschreiben brauchen (naja, hätten wir gleich mit {{lambda}}-Ausdrücken
angefangen, hätten wir von Anfang an das JDK-Interface verwendet).


Interfaces, die der genannten Regel entsprechen und genau eine nicht-statische Methode haben heißen in Java
Interfaces, die der genannten Regel entsprechen und genau eine nicht-statische Methode haben heißen in Java
"functional Interfaces" und werden meist mit der Annotation {{java|@FunctionalInterface}} versehen. Die Annotation ist nicht
"functional Interfaces" und werden meist mit der Annotation {{java|@FunctionalInterface}} versehen. Die Annotation ist nicht
erforderlich, aber hilfreich. Der JDK bietet im Package {{java|java.util.function}} eine ganze Reihe nützlicher Interfaces an,
erforderlich aber hilfreich, da sie verhindert daß das Interface durch Hinzufügen einer weiteren Funktion für {{lambda}}-Ausdrücke
unbrauchbar machen würde. Der JDK bietet im Package {{java|java.util.function}} eine ganze Reihe nützlicher Interfaces an,
aber es steht dem Entwickler frei, beliebige Interfaces zu definieren. Bisweilen ist es auch hilfreich nicht die JDK-Interfaces
aber es steht dem Entwickler frei, beliebige Interfaces zu definieren. Bisweilen ist es auch hilfreich nicht die JDK-Interfaces
zu verwenden wenn das Wording nicht zu hundert Prozent paßt.
zu verwenden wenn das Wording nicht zu hundert Prozent paßt.


=== Methoden als Lambda-Ausdruck ===
=== Methoden als Lambda-Ausdruck ===
Kommen wir nun zu einigen alternativen Varianten, {{lambda}}-Ausdrücke zu schreiben die genau eine existierende
Mit der vorgestellten {{java|->}}-Notation läßt sich im Prinzip alles schreiben was an {{lambda}}-Ausdrücken möglich ist.
Methode aufrufen.
Natürlich gilt die Clean Code Regel "kürzer ist besser als kurz" auch hier. Also folgen wir dem Konzept, Code in Klassen
und Methoden aufzuteilen, auch hier. Um die dabei entstehenden Methoden aufzurufen giebt es mit dem {{java|::}}-Operator
(auch neu in Java 8) drei weitere Möglichkeiten, {{lambda}}-Ausdrücke zu definieren
===== statische Methoden =====
===== statische Methoden =====
Beginnen wir mit statischen Methode. Die JDK-Klasse {{java|Math}} besitzt eine Methode {{java|abs()}} die den Absolutwert einer
Beginnen wir mit statischen Methoden. Die JDK-Klasse {{java|Math}} besitzt eine Methode {{java|abs()}} die den Absolutwert einer
Integerzahl berechnet:
Integerzahl berechnet:
{{java|code=
{{java|code=
Zeile 163: Zeile 188:
verarbeite(liste, x -> Math.abs(x));
verarbeite(liste, x -> Math.abs(x));
}}
}}
Wir können die Methode aber auch mit dem {{java|::}}-Operator referenzieren. Dabei wird die Klasse links des Operators
Wir können die Methode aber auch mit dem {{java|::}}-Operator referenzieren.
angegeben und Name der Methode rechts:
Dabei wird die Klasse links des Operators angegeben und Name der Methode rechts:
{{java|code=
{{java|code=
verarbeite(liste, Math::abs);
verarbeite(liste, Math::abs);
}}
}}
 
Beide Varianten sind funktional äquivalent.
==== nicht-statische Methoden ====
==== nicht-statische Methoden ====
Das geht auch mit nicht-statischen Methoden. Allerdings benötigen wir dafür ein Objekt auf das sich die Methode
Das geht auch mit nicht-statischen Methoden. Allerdings benötigen wir dafür ein Objekt auf das sich die Methode
Zeile 186: Zeile 211:
Tatsächlich darf auf der linken Seite des {{java|::}}-Operators ein beliebiger Ausdruck stehen der ein Objekt liefert, also zum
Tatsächlich darf auf der linken Seite des {{java|::}}-Operators ein beliebiger Ausdruck stehen der ein Objekt liefert, also zum
Beispiel eine Methode – statisch oder auch nicht – die ein Objekt liefert.
Beispiel eine Methode – statisch oder auch nicht – die ein Objekt liefert.
Hier muß man allerdings eine Feinheit beachten. Vergleichen wir die beiden Varianten:
{{java|code=
verarbeite(liste, new Calc()::doppel);
}}
und
{{java|code=
verarbeite(liste, x -> new Calc().doppel(x));
}}
Im ersten Falle wird ein {{java|Calc}}-Objekt erzeugt, und innerhalb des Aufrufs der {{java|verarbeite}}-Methode
die Methode {{java|doppel}} auf dieses Objekt aufgerufen. Kurz: es wird ''genau'' ein {{java|Calc}}-Objekt verwendet.
Im zwiten Falle hingegen wird bei jedem Aufruf des {{lambda}}-Ausdrucks ein neues {{java|Calc}}-Objekt erzeugt auf das die
Methode {{java|doppel}} aufgerufen wird.
Wenn die aufgerufene Methode ohne Seiteneffekt ist, erzeugen wir nur ein paar zusätzliche Objekte.
Andernfalls jedoch kann das Ergebnis der beiden Varianten vollkommen unterschiedlich ausfallen.


==== Statische Referenz nicht-statischer Methoden ====
==== Statische Referenz nicht-statischer Methoden ====
Zeile 224: Zeile 266:
auf diesen Parameter anwendet.
auf diesen Parameter anwendet.
Gerade diese letzte Variante wirkt ohne praktische Anwendung ziemlich akademisch, In der [[Optional|Einführungen zu Optionals]] gibt es einige praktische Anwendungsbeispiele für solche {{lambda}}-Ausdrücke.
Gerade diese letzte Variante wirkt ohne praktische Anwendung ziemlich akademisch, In der [[Optional|Einführungen zu Optionals]] gibt es einige praktische Anwendungsbeispiele für solche {{lambda}}-Ausdrücke.
=== Ein Wort zu Interfaces ===
Wir haben Anfangs ein Interface definiert und ihm den Namen {{java|Function}} gegeben.
Das erschien notwendig, da unsere {{java|verarbeite}}-Funktion ein Interface für den Parameter-Typ braucht.
Tatsächlich bietet der JDK ein passendes Interface, das allerdings mit Typ-Parameter viel flexibler ist:
{{java|code=
package java.util.function;
public interface Function<T, R> {
    R apply(T t);
}
}}
Usere {{lambda}}-Definition sähe damit so aus
{{java|code=
Function<Integer, Integer> square = x -> x * x;
}}
Das Package bietet eine ganze Sammlung von Interfaces für viele Zwecke, so daß es eigentlich selten erforderlich ist
eigene Interfaces zu definieren. Sinnvoll ist das aber immer dann, wenn {{lambda}}-Ausdrücke in speziellen Kontexten
verwendet werden sollen und das Interface genauer sagen soll was die Funktion machen soll.
Das ändert zunächst nichts an der Austauschbarkeit Typ-äquivalenter Interfaces, über die Typstrenge von Java stolpert
man dabei aber dennoch manchmal. Betrachten wir mal folgende Code-Fragmente:
{{java|code=
Interface F {
  Integer apply(Integer x);
}
...
Integer use(F function) {
...
}
...
Function<Integer, Integer> square = x -> x * x;
Integer bla = use(square);
}}
Hier wirft uns der Compiler in der letzten Zeile einen Compile-Fehler.
Zwar ist der {{lambda}}-Ausdruck {{java|x -> x * x}} äquivalent mit dem Interface {{java|F}},
die Variable {{java|square}} jedoch hat einen Typ der nicht in {{java|F}} konvertierbar ist,
denn es gelten hier die Typ-Regeln für Interfaces.
=== {{lambda}}-Ausdrücke  und Exceptions ===
Tatsächlich gehört neben den Typen der Ein- und Ausgabe-Parameter eine vorhandene
throws-Clausse an der Methoden-Definition ebenfalls zur Typ-Signatur.
Hier hilft ein Beispiel:
{{java|code=
interface Func{
    Integer apply(Integer x);
}
interface FuncEx {
    Integer apply(Integer x) throws Exception;
}
}}
Die beiden Interfaces sind im Sinne der {{lambda}}-Ausdrücke nicht ineinander konvertierbar.
Das ist eine extrem starke Einschränkung bei der Verwendung von checked Exceptions. Unchecked Exceptions
sind in der Regel kein Problem, weil man sie in der {{java|throws}}-Clause nicht anzugeben braucht und
sie daher nicht angeben sollte -- sie würden die Signatir natürlich ebenso verunreinigen.
Grundsätzlich giebt es zwei Wege damit umzugehen. Entweder man verzichtet ganz auf die Verwendung von checked Exceptions
oder man wrappt -- wo erforderlich -- die checked Exceptions in unchecked Exceptions.


=== Lambdas mit Kontext ===
=== Lambdas mit Kontext ===
Die oben vorgestellte, zweite Variante läßt sich – anders als die Variante mit der statischen Methode – nicht ohne
Wie wir oben gesehen haben, läßt sich die zweite Variant nicht unmittelbar von der {{java|::}}-Notation in die
weiteres als "normaler" {{lambda}}-Ausdruck schreiben, da zur Ausführung ein Objekt benötigt wird. Hier sehen wir
{{java|->}}-Notation überführen, da da zur Ausführung ein Objekt benötigt wird. Wir müsse dazu die Möglichkeit
eine weitere Eigenschaft von {{lambda}}-Ausdrücken: Man kann ihnen nämlich aus dem sie umgebenden Kontext
nutzen, ''außerhalb'' des {{lambda}}-Ausdrucks ein Objekt zu erzeugen und dem {{lambda}}-Ausdruck mitzugeben.
Daten mitgeben:
Hier sehen wir eine weitere Eigenschaft von {{lambda}}-Ausdrücken:
Man kann ihnen nämlich Daten aus dem sie umgebenden Kontext mitgeben:
{{java|code=
{{java|code=
Calc rechner = new Calc();
Calc rechner = new Calc();
Zeile 246: Zeile 348:
     return (Integer x) -> rechner.doppel(x);
     return (Integer x) -> rechner.doppel(x);
}
}
 
public void rechne() {
public void rechne() {
     this.verarbeite(liste, getRechner());
     this.verarbeite(liste, getRechner());
Zeile 252: Zeile 354:
}}
}}
Die Methode {{java|getRechner}} erzeugt das {{java|Calc}}-Objekt und baut es in den {{lambda}}-Ausdruck ein.
Die Methode {{java|getRechner}} erzeugt das {{java|Calc}}-Objekt und baut es in den {{lambda}}-Ausdruck ein.
Wenn die Methode {{java|rechne()}} das {{lambda}} von {{java|getRechner}} als Ergebnis erhält, ist die Methode {{java|getRechner()}}
Wenn die Methode {{java|rechne}} den {{lambda}}-Ausdruck von {{java|getRechner}} als Ergebnis erhält, ist die Methode {{java|getRechner}}
zu Ende gelaufen, aber das Objekt das die Variable {{java|rechner}} referenziert lebt im {{lambda}}-Ausdruck weiter und wird
zu Ende gelaufen, aber das Objekt das die Variable {{java|rechner}} referenziert lebt im {{lambda}}-Ausdruck weiter und wird
nun bei der Verarbeitung verwendet.
nun bei der Verarbeitung verwendet.

Aktuelle Version vom 13. Dezember 2022, 00:06 Uhr

Der Begriff "Lambda-Ausdruck" entstammt dem Lambda-Kalkül mit dem sich Alonzo Church in den 1930er Jahren anschickte eine formale Spezifikation der Mathematik zu schaffen. Dabei hat er ein Konzept entwickelt, auf dem die funktionalen Programmiersprachen wie LISP, Haskell, Clojure oder Scala aufsetzen und das sich fundamental vom Modell der Turing-Maschine unterscheidet, das den imperativen Sprachen wie Java oder C zugrunde liegt. Der Name leitet sich vom griechischen Buchstaben λ ab den Church für seine Notation verwendete. Weil das kürzer ist -- und cooler aussieht -- steht im Folgenden λ für "Lambda".

Im nachfolgenden Artikel wird gezeigt, was λ-Ausdrücke in Java sind und wie man sie anwenden kann. Es wird hier nur ein Überblick gegeben, die Verwendungsmöglichkeiten von λ-Ausdrücken sind immens vielseitig. Was also ist ein λ oder besser gesagt ein λ-Ausdruck?

Ein Lambda-Ausdruck definiert eine einzelne Funktion deren Typ bestimmt ist durch die Typen ihrer Parameter-Liste und ihres Ergebnis-Typs.

Das klingt abstrakt und sehr akademisch. Aber behalten wir diese Aussage im Hinterkopf, wenn wir im Folgenden an einem Beispiel die Verwendung von λ-Ausdrücken in Java erkunden.

Es sei nicht verschwiegen, daß in obiger Definition ein Detail weggelassen wurde. Für den Typ des λ-Ausdruck ist in Java auch die throws-Klausel der Methode von Bedeutung. Was das bedeutet wird später erläutert.

Ein Beispiel mit konventionellen Java-Mitteln

Wir haben hier eine Methode, die die Zahlen einer Liste durchgeht und die Summe der Zahlen als Ergebnis liefert.

public Integer verarbeite(List<Integer> liste) {
    Integer result = 0;
    for (Integer x : liste) {
        result += x;
    }
    return result;
}

Den Rahmen dieser Methode könnte man benutzen um andere Berechnungen anzustellen. Etwa zu zählen wieviele gerade Zahlen die Liste enthält. Aber jedemal, wenn eine neue Funktion zur Berechnung verwendet werden soll, muß die Methode kopiert und angepaßt werden – das ist nicht schön. Zunähst einmal muß die Methode so erweitert werden, daß auf die Zahlen eine beliebige Funktion angewandt werden kann. Das geschieht in Java immer auf die gleiche Art und Weise, nämlich mit Hilfe einer Klasse oder eines Interface. Wir betrachten hier nur die Interface-Variante, weil sie felxibler ist. Wir definieren also ein Interface das eine Funktion zur Berechnung enthält:

interface Function {
    Integer apply(Integer x);
}

Nun erweitert man die Methode so, daß sie eine Instanz dieses Interfaces als Argument übernimmt und anstelle des fest verdrahteten Ausdrucks x für die Verechnung verwendet:

public Integer verarbeite(List<Integer> liste, Function fkt) {
    Integer result = 0;
    for (Integer x : liste) {
        result += fkt.apply(x);
    }
    return result;
}

Um die Methode verarbeite verwenden zu können benötigen wir nun ein Objekt, das das Interface Function implementiert. Nur mit einem solchen Objekt kann die Methode verwendet werden. Um die Summe der Quadrate zu berechnen definieren wir daher eine Klasse die das Interface in der geeigneten Weise implementiert:

class Square implements Function {
    public Integer apply(Integer x) {
        return x * x;
    }
}

Die Verwendung sieht dann vielleicht so aus:

Integer quadrate = verarbeite(meineListe, new Square());

Das funktioniert natürlich nur dann, wenn wir die Klasse mit der Berechnung frei implementieren und vor allem den Namen der Methode so benennen können wie es das Interface erfordert. Bei einer existierenden Klasse -- vor allem wenn sie final ist, geht das nicht. Möchten wir zum Beispiel folgende Klasse verwenden:

final class Calc {
    public Integer doppel(Integer x) {
        return 2 * x;
    }
}

benötigen wir zusätzlich einen Wrapper oder Adapter der die Methode doppel auf die Methode apply des Interface Function abbildet. Anstelle einer eigenen Klasse kann man beim Aufruf von bearbeite ein Objekt einer anonymen Klasse instantiieren:

verarbeite(meineListe, new Funktion() {
    public Integer apply(Integer x) {
        return new Calc().doppel(x);
    }
});

Und das ist so unübersichtlich wie aufwendig. Für statische Methoden führt überhaupt kein Weg am Wrapper vorbei, denn statische Methoden sind für ein Interface im konventionellen Java unerreichbar.

Was da stört

Die parameterisierte Methode verarbeite ist -- so wie sie ist -- eine gute Lösung. Da brauchen wir nichts zu ändern, entscheidend ist dabei, daß ein Interface mit einer einzigen Methode verwendet wird.

Wirklich unelegant ist der Aufruf der Methode. Keine der Lösungen ist befriedigend und hat Nachteile. Klassen die direkt verwendet werden sollen müssen ein gegebenes Interface implemenieren. Eine Bedingung die sich oft gar nicht erfüllen läßt. Ist die Klasse final, läßt sie sich nicht einmal ableiten...

Verwendet man statt dessen eine Wrapperklasse, muß man eine weitere Klasse definieren die dem Code keine Funktionalität hinzufügt, sonder nur Code zur Verwaltung enthält. Das Ausweichen über anonyme Klassen scheint elegant, erzeugt aber ebenfalls zusätzlichen Code und erzeugt tatsächlich zusätzliche Klassen.

Und jetzt mit Lambdas

Wir erinnern uns: Ein λ-Ausdruck repräsentiert eine Funktion. Was unsere Java-Lösungen oben so schwerfällig erscheinen läßt ist der Versuch eine einzelne Funktion in ein Java-Objekt zu verpacken. Das wird noch dadurch erschwert, daß Java stark typisiert ist und sehr kleinlich bei der Frage welche Eigenschaften die Klasse eines Objekts haben muß damit es verwendet werden kann.

Im Grunde genommen möchten wir doch oben nur die Funktion f(x) -> x ersetzen durch die Funktion f(x) -> x * x.

Springen wir also direkt hinein und betrachten einen λ-Ausdruck der die Quadrat-Funktion darstellt. Er wird einer Variable vom Typ unseres oben definierten Interface zugewiesen -- warum das geht betrachten wir später, wir konzentrieren uns zunächst auf den λ-Ausdruck rechts :

Function square = (Integer x) -> { return x * x; }

Ein λ-Ausdruck entspricht der Definition einer mathematischen Funktion. Links des ->-Operators (der ist neu in Java 8) steht die Liste der durch Komma getrennten Paramter mit ihren Typen. Im Beispiel haben wir einen Parameter vom Typ Integer. Die Parameter-Liste wird in runde Klammern gesetzt.

Rechts des Operators steht der auszuwertende Code-Block, genau wie bei einer Methode. Wenn der λ-Ausdruck einen anderen Ergebnis-Typ hat als Vorlage:Code, muß er mit einem return-Statement einen Wert des entsprechenden Ergebnistyps (hier: Integer) liefert. Der Ergebnistyp wird hier nicht explizit angegeben, er ergibt sich aus dem Ausdruck im return-Statement. Java ist extrem smart was das bestimmen nicht angegebener Typen anbelangt und wir können froh sein, daß uns unsere IDE so viele Informationen darüber mitteilt.

Wir haben den λ-Ausdruck oben in seiner vollen Ausführlichkeit formuliert. Um den Ausdruck kompakter und dadurch übersichtlicher zu machen, lassen sich einige Teile weglassen:

Besteht der Ausdruck lediglich aus einem return-Statement, kann man die Klammern samt return weglassen:

Function square = (Integer x) -> x * x;

Wenn aus dem Kontext klar ist, welche Typen die Parameter der Parameter-Liste haben, kann man auch die Typen weglassen:

Function square = (x) -> x *x;

Wenn die Parameter-Liste nur einen einzigen Parameter enthält, können wir die Klammern um den Parameter weglassen:

Function square = x -> x *x;

Da square vom Typ des Interfaces Function ist, können wir jeden der gezeigten Ausdrücke verwenden um unsere Methode aufzurufen. Am kompaktesten ist die letzte:

verarbeite(liste, x -> x * x);

λ-Ausdrücke als Interfaces

Wir haben oben -- als wäre das selbstverständlich -- den λ-Ausdruck mit einem Interface gleichgesetzt. Das funktioniert dann, wenn das Interface genau eine (nicht-statische) Funktion definiert deren Typ-Signatur dem λ-Ausdruck entspricht. Das heißt, die Liste der Eingabe-Typen und der Ausgabe-Typ müssen übereinstimmen:

interface Function {
    Integer apply(Integer x);
}

und

Function square = (Integer x) -> { return x * x; }

Man könnte sich vorstellen, daß ein λ-Ausdruck eine ultrakompakte Beschreibung einer anonymen Klasse ist die das Interface implementiert. Diese Vorstellung geht allerdings nicht weit genug.

Tatsächlich ist der λ-Ausdruck gleichwertig ist mit jeder Methode jeder Klasse deren Parameter-Liste die gleichen Typen hat und den gleichen Ergebnis-Typ und das unabhängig davon wie die Metode heißt. Und genau hier liegt die Superkraft des λ-Ausdrucks. Denn auf diese Weise läßt sich jede Methode als λ-Ausdruck verwenden. Wie das geht, wird im nächsten Abschnitt erklärt.

Zudem "implementiert" der λ-Ausdruck jedes Interface das eine einzige Methode besitzt deren typ-Signatur der des λ-Ausdrucks emtspricht also zum Beispiel auch:

interface Blafasel {
    Integer schnufusel(Integer x);
}

Interfaces, die der genannten Regel entsprechen und genau eine nicht-statische Methode haben heißen in Java "functional Interfaces" und werden meist mit der Annotation @FunctionalInterface versehen. Die Annotation ist nicht erforderlich aber hilfreich, da sie verhindert daß das Interface durch Hinzufügen einer weiteren Funktion für λ-Ausdrücke unbrauchbar machen würde. Der JDK bietet im Package java.util.function eine ganze Reihe nützlicher Interfaces an, aber es steht dem Entwickler frei, beliebige Interfaces zu definieren. Bisweilen ist es auch hilfreich nicht die JDK-Interfaces zu verwenden wenn das Wording nicht zu hundert Prozent paßt.

Methoden als Lambda-Ausdruck

Mit der vorgestellten ->-Notation läßt sich im Prinzip alles schreiben was an λ-Ausdrücken möglich ist. Natürlich gilt die Clean Code Regel "kürzer ist besser als kurz" auch hier. Also folgen wir dem Konzept, Code in Klassen und Methoden aufzuteilen, auch hier. Um die dabei entstehenden Methoden aufzurufen giebt es mit dem ::-Operator (auch neu in Java 8) drei weitere Möglichkeiten, λ-Ausdrücke zu definieren

statische Methoden

Beginnen wir mit statischen Methoden. Die JDK-Klasse Math besitzt eine Methode abs() die den Absolutwert einer Integerzahl berechnet:

public static int abs(int a) {
    return (a < 0) ? -a : a;
}

An dieser Stelle sei darauf hingewiesen, daß wir hier durch das Autoboxing von Java die Typen Integer und int jederzeit gegen einander austauschen können. Wir können diese Methode nun so verwenden wie oben und in einen λ-Ausdruck verpacken:

verarbeite(liste, x -> Math.abs(x));

Wir können die Methode aber auch mit dem ::-Operator referenzieren. Dabei wird die Klasse links des Operators angegeben und Name der Methode rechts:

verarbeite(liste, Math::abs);

Beide Varianten sind funktional äquivalent.

nicht-statische Methoden

Das geht auch mit nicht-statischen Methoden. Allerdings benötigen wir dafür ein Objekt auf das sich die Methode bezieht. Befindet sich die Methode in der gleichen Klasse, können wir this verwenden:

verarbeite(liste, this::doppel);

Natürlich kann man auch eine Variable verwenden:

Calc c = new Calc();
verarbeite(liste, c::doppel);

oder ein Objekt in place erzeugen:

verarbeite(liste, new Calc()::doppel);

Tatsächlich darf auf der linken Seite des ::-Operators ein beliebiger Ausdruck stehen der ein Objekt liefert, also zum Beispiel eine Methode – statisch oder auch nicht – die ein Objekt liefert.

Hier muß man allerdings eine Feinheit beachten. Vergleichen wir die beiden Varianten:

verarbeite(liste, new Calc()::doppel);

und

verarbeite(liste, x -> new Calc().doppel(x));

Im ersten Falle wird ein Calc-Objekt erzeugt, und innerhalb des Aufrufs der verarbeite-Methode die Methode doppel auf dieses Objekt aufgerufen. Kurz: es wird genau ein Calc-Objekt verwendet.

Im zwiten Falle hingegen wird bei jedem Aufruf des λ-Ausdrucks ein neues Calc-Objekt erzeugt auf das die Methode doppel aufgerufen wird.

Wenn die aufgerufene Methode ohne Seiteneffekt ist, erzeugen wir nur ein paar zusätzliche Objekte. Andernfalls jedoch kann das Ergebnis der beiden Varianten vollkommen unterschiedlich ausfallen.

Statische Referenz nicht-statischer Methoden

Die dritte Variante ist auf den ersten Blick ausgesprochen verwirrend, weil sie die beiden vorangegangenen Varianten zu vermischen scheint.

Tatsächlich ist es aber vermutlich die am häufigsten eingesetzte Variante und ist beim Arbeiten mit Streams und Optionals unverzichtbar.

Nehmen wir uns also etwas mehr Zeit dafür:

Wie bei der ersten Variante schreibt man den Namen der Klasse auf der linken Seite des ::-Operators und den Namen der (nicht-statischen) Methode auf der rechten Seite:

String::trim

Während die ersten beiden Varianten noch intuitiv verständlich sein dürften, muß man sich fragen, wie dieser Ausdruck zu interpretieren ist. Die Methode trim() der Klasse String ist nicht statisch. Sie wird auf ein String-Objekt angewandt und liefert als Ergebnis ebenfalls einen String. Da stellt sich zunächst die Frage, welches funktionale Interface die Methode implementiert? Probiert man das aus erhält man folgendes verblüffende Ergebnis:

Function<String, String> mapper = String::trim;

Dann ist trim() also eine Methode, die einen String als Eingabe hat und einen String als Ergebnis liefert? Ganz falsch ist diese Interpretation nicht, wenn man sich vorstellt, daß der String der getrimmt werden soll nicht in der Klammer steht, sondern vor dem Punkt:

String foo = " mit Leerzeichen ";
foo = foo.trim();

Und genauso wird der Ausdruck String::trim verwendet. Man benutzt ihn, wenn man eine Methode angeben möchte, die auf ein Objekt angewendet werden soll (in diesem Falle vom Typ String). Die Notation String::trim könnte man als "normalen" λ-Ausdruck auch so schreiben:

(String s) -> {return s.trim();}

Dieser λ-Ausdruck definiert eine Funktion, die einen String als Parameter übernimmt und dann die Methode trim() auf diesen Parameter anwendet. Gerade diese letzte Variante wirkt ohne praktische Anwendung ziemlich akademisch, In der Einführungen zu Optionals gibt es einige praktische Anwendungsbeispiele für solche λ-Ausdrücke.

Ein Wort zu Interfaces

Wir haben Anfangs ein Interface definiert und ihm den Namen Function gegeben. Das erschien notwendig, da unsere verarbeite-Funktion ein Interface für den Parameter-Typ braucht. Tatsächlich bietet der JDK ein passendes Interface, das allerdings mit Typ-Parameter viel flexibler ist:

package java.util.function;
public interface Function<T, R> {
    R apply(T t);
}

Usere λ-Definition sähe damit so aus

Function<Integer, Integer> square = x -> x * x;

Das Package bietet eine ganze Sammlung von Interfaces für viele Zwecke, so daß es eigentlich selten erforderlich ist eigene Interfaces zu definieren. Sinnvoll ist das aber immer dann, wenn λ-Ausdrücke in speziellen Kontexten verwendet werden sollen und das Interface genauer sagen soll was die Funktion machen soll.

Das ändert zunächst nichts an der Austauschbarkeit Typ-äquivalenter Interfaces, über die Typstrenge von Java stolpert man dabei aber dennoch manchmal. Betrachten wir mal folgende Code-Fragmente:

Interface F {
   Integer apply(Integer x);
}
...
Integer use(F function) {
...
}
...
Function<Integer, Integer> square = x -> x * x;
Integer bla = use(square);

Hier wirft uns der Compiler in der letzten Zeile einen Compile-Fehler. Zwar ist der λ-Ausdruck x -> x * x äquivalent mit dem Interface F, die Variable square jedoch hat einen Typ der nicht in F konvertierbar ist, denn es gelten hier die Typ-Regeln für Interfaces.

λ-Ausdrücke und Exceptions

Tatsächlich gehört neben den Typen der Ein- und Ausgabe-Parameter eine vorhandene throws-Clausse an der Methoden-Definition ebenfalls zur Typ-Signatur.

Hier hilft ein Beispiel:

interface Func{
    Integer apply(Integer x);
}

interface FuncEx {
    Integer apply(Integer x) throws Exception;
}

Die beiden Interfaces sind im Sinne der λ-Ausdrücke nicht ineinander konvertierbar. Das ist eine extrem starke Einschränkung bei der Verwendung von checked Exceptions. Unchecked Exceptions sind in der Regel kein Problem, weil man sie in der throws-Clause nicht anzugeben braucht und sie daher nicht angeben sollte -- sie würden die Signatir natürlich ebenso verunreinigen.

Grundsätzlich giebt es zwei Wege damit umzugehen. Entweder man verzichtet ganz auf die Verwendung von checked Exceptions oder man wrappt -- wo erforderlich -- die checked Exceptions in unchecked Exceptions.

Lambdas mit Kontext

Wie wir oben gesehen haben, läßt sich die zweite Variant nicht unmittelbar von der ::-Notation in die ->-Notation überführen, da da zur Ausführung ein Objekt benötigt wird. Wir müsse dazu die Möglichkeit nutzen, außerhalb des λ-Ausdrucks ein Objekt zu erzeugen und dem λ-Ausdruck mitzugeben.

Hier sehen wir eine weitere Eigenschaft von λ-Ausdrücken: Man kann ihnen nämlich Daten aus dem sie umgebenden Kontext mitgeben:

Calc rechner = new Calc();
Function<Integer, Integer> fkt = (Integer x) -> rechner.doppel(x);
this.verarbeite(liste, fkt);

Was geschieht hier? Wir erzeugen zunächst ein Objekt rechner von Typ Calc und bauen dann einen λ-Ausdruck der das Objekt rechner verwendet um die Methode doppel() mit einem Integer-Wert auszuführen der als Parameter mitgegeben wird.

Das Objekt kommt aus dem Kontext des λ-Ausdrucks (der Methode in der der Ausdruck erzeugt wird) und wird dem λ-Ausdruck mitgegeben. Man kann dieses Verhalten zum Beispiel dazu nutzen, λ-Ausdrücke mit einer Methode zu erzeugen um sie dann an anderer Stelle zu verwenden:

public Function<Integer, Integer> getRechner() {
    Calc rechner = new Calc();
    return (Integer x) -> rechner.doppel(x);
}

public void rechne() {
    this.verarbeite(liste, getRechner());
}

Die Methode getRechner erzeugt das Calc-Objekt und baut es in den λ-Ausdruck ein. Wenn die Methode rechne den λ-Ausdruck von getRechner als Ergebnis erhält, ist die Methode getRechner zu Ende gelaufen, aber das Objekt das die Variable rechner referenziert lebt im λ-Ausdruck weiter und wird nun bei der Verarbeitung verwendet.

Dabei muß man zwei Dinge beachten: Die Variable rechner, die das Objekt enthält das hier in den λ-Ausdruck eingearbeitet wird, muß "effektiv final" sein. Das heißt, ihr Inhalt darf nach der Zuweisung nicht mehr verändert werden. Folgender Code wird daher nicht kompilieren, da rechner nachträglich verändert wird:

public Function<Integer, Integer> getRechner() {
    Calc rechner = new Calc();
    Function<Integer, Integer> fkt = (Integer x) -> rechner.doppel(x);
    rechner = new Calc();
    return fkt;
}

Das Objekt das durch die Variable rechner referenziert wird kann aber sehr wohl verändert werden. Dadurch kann sich das Verhalten des λ-Ausdrucks ändern. Das ist ein generelles Problem in Java, das keine unveränderbare (immutable) Objekte kennt. Es ist dringend davon abzuraten, solches Verhalten in λ-Ausdrücke einzuarbeiten, auch wenn es auf den ersten Blick sehr elegant wirkt. Solches Seiteneffekte führen sehr gerne zu obskuren Fehlern, die nur schwer zu debuggen sind.