Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.
 
Inhaltsverzeichnis
Vorwort
1 Java ist auch eine Sprache
2 Imperative Sprachkonzepte
3 Klassen und Objekte
4 Der Umgang mit Zeichenketten
5 Eigene Klassen schreiben
6 Objektorientierte Beziehungsfragen
7 Ausnahmen müssen sein
8 Äußere.innere Klassen
9 Besondere Typen der Java SE
10 Generics<T>
11 Lambda-Ausdrücke und funktionale Programmierung
12 Architektur, Design und angewandte Objektorientierung
13 Die Klassenbibliothek
14 Einführung in die nebenläufige Programmierung
15 Einführung in Datenstrukturen und Algorithmen
16 Einführung in grafische Oberflächen
17 Einführung in Dateien und Datenströme
18 Einführung ins Datenbankmanagement mit JDBC
19 Einführung in <XML>
20 Testen mit JUnit
21 Bits und Bytes und Mathematisches
22 Die Werkzeuge des JDK
A Java SE Paketübersicht
Stichwortverzeichnis

Download:
- Beispielprogramme, ca. 20,0 MB
- Übungsaufgaben, ca. 1,8 MB
- Musterlösungen, ca. 0,8 MB

Buch bestellen
Ihre Meinung?

Spacer
<< zurück
Java ist auch eine Insel von Christian Ullenbloom
Das umfassende Handbuch
Buch: Java ist auch eine Insel

Java ist auch eine Insel
Rheinwerk Computing
1306 Seiten, gebunden, 11. Auflage
49,90 Euro, ISBN 978-3-8362-2873-2
Pfeil 11 Lambda-Ausdrücke und funktionale Programmierung
Pfeil 11.1 Code = Daten
Pfeil 11.2 Funktionale Schnittstellen und Lambda-Ausdrücke im Detail
Pfeil 11.2.1 Funktionale Schnittstellen
Pfeil 11.2.2 Typ eines Lambda-Ausdrucks ergibt sich durch Zieltyp
Pfeil 11.2.3 Annotation @FunctionalInterface
Pfeil 11.2.4 Syntax für Lambda-Ausdrücke
Pfeil 11.2.5 Die Umgebung der Lambda-Ausdrücke und Variablenzugriffe
Pfeil 11.2.6 Ausnahmen in Lambda-Ausdrücken
Pfeil 11.2.7 Klassen mit einer abstrakten Methode als funktionale Schnittstelle? *
Pfeil 11.3 Methoden-Referenz
Pfeil 11.3.1 Varianten von Methoden-Referenzen
Pfeil 11.4 Konstruktor-Referenz
Pfeil 11.4.1 Standard- und parametrisierte Konstruktoren
Pfeil 11.4.2 Nützliche vordefinierte Schnittstellen für Konstruktor-Referenzen
Pfeil 11.5 Implementierung von Lambda-Ausdrücken *
Pfeil 11.6 Funktionale Programmierung mit Java
Pfeil 11.6.1 Programmierparadigmen: imperativ oder deklarativ
Pfeil 11.6.2 Funktionale Programmierung und funktionale Programmiersprachen
Pfeil 11.6.3 Funktionale Programmierung in Java am Beispiel vom Comparator
Pfeil 11.6.4 Lambda-Ausdrücke als Funktionen sehen
Pfeil 11.7 Funktionale Schnittstelle aus dem java.util.function-Paket
Pfeil 11.7.1 Blöcke mit Code und die funktionale Schnittstelle java.util.function.Consumer
Pfeil 11.7.2 Supplier
Pfeil 11.7.3 Prädikate und java.util.function.Predicate
Pfeil 11.7.4 Funktionen und die allgemeine funktionale Schnittstelle java.util.function.Function
Pfeil 11.7.5 Ein bisschen Bi …
Pfeil 11.7.6 Funktionale Schnittstellen mit Primitiven
Pfeil 11.8 Optional ist keine Nullnummer
Pfeil 11.8.1 Optional-Typ
Pfeil 11.8.2 Primitive optionale Typen
Pfeil 11.8.3 Erstmal funktional mit Optional
Pfeil 11.9 Was ist jetzt so funktional?
Pfeil 11.10 Zum Weiterlesen
 
Zum Seitenanfang

11.2Funktionale Schnittstellen und Lambda-Ausdrücke im Detail Zur vorigen ÜberschriftZur nächsten Überschrift

In unserem Beispiel haben wir den Lambda-Ausdruck als Argument von Array.sort(…) eingesetzt:

Arrays.sort( words,
(String s1, String s2) -> { return s1.trim().compareTo(s2.trim()); } );

Wir hätten aber auch den Lambda-Ausdruck explizit einer lokalen Variablen zuweisen können, was deutlich macht, dass der hier eingesetzte Lambda-Ausdruck vom Typ Comparator ist:

Comparator<String> c = (String s1, String s2) -> {
return s1.trim().compareTo( s2.trim() ); }
Arrays.sort( words, c );
 
Zum Seitenanfang

11.2.1Funktionale Schnittstellen Zur vorigen ÜberschriftZur nächsten Überschrift

Nicht zu jeder Schnittstelle gibt es eine Abkürzung über einen Lambda-Ausdruck, und es gibt eine zentrale Bedingung, wann ein Lambda-Ausdruck verwendet werden kann.

Definition

Schnittstellen, die nur eine Operation (abstrakte Methode) besitzen, heißen funktionale Schnittstellen. Ein Funktions-Deskriptor beschreibt diese Methode. Eine abstrakte Klasse mit genau einer abstrakten Methode zählt nicht als funktionale Schnittstelle.

Lambda-Ausdrücke und funktionale Schnittstellen haben eine ganz besondere Beziehung, denn ein Lambda-Ausdruck ist ein Exemplar einer solchen funktionalen Schnittstelle. Natürlich müssen Typen und Ausnahmen passen. Dass funktionale Schnittstellen genau eine abstrakte Methode vorschreiben, ist eine naheliegende Einschränkung, denn gäbe es mehrere, müsste ein Lambda-Ausdruck ja auch mehrere Implementierungen anbieten oder irgendwie eine Methode bevorzugen und andere ausblenden.

Wenn wir also ein Objekt vom Typ einer funktionalen Schnittstelle aufbauen möchten, können wir folglich zwei Wege einschlagen: Es lässt sich die traditionelle Konstruktion über die Bildung von Klassen wählen, die funktionale Schnittstellen implementieren, und dann mit new ein Exemplar bilden, oder es lässt sich mit kompakten Lambda-Ausdrücken arbeiten. Moderne IDEs zeigen uns an, wenn kompakte Lambda-Ausdrücke zum Beispiel statt innerer anonymer Klassen genutzt werden können, und bieten uns mögliche Refactorings an. Lambda-Ausdrücke machen den Code also kompakter und nach kurzer Eingewöhnung auch lesbarer.

[+]Hinweis

Funktionale Schnittstellen müssen auf genau eine zu implementierende Methode hinauslaufen, auch wenn aus Oberschnittstellen mehrere Operationen vorgeschrieben werden, die sich aber durch den Einsatz von Generics auf eine Operation verdichten:

interface I<S, T extends CharSequence> {
void len( S text );
void len( T text );
}
interface FI extends I<String, String> { }

FI ist unsere funktionale Schnittstelle mit einer eindeutigen Operation len(String).

Viele funktionale Schnittstellen in der Java-Standardbibliothek

Java bringt schon viele Schnittstellen mit, die in Java 8 als funktionale Schnittstellen gekennzeichnet sind. Darüber hinaus führt Java 8 mit dem Paket java.util.function mehr als 40 neue funktionale Schnittstellen ein. Eine kleine Auswahl:

  • interface Runnable { void run(); }

  • interface Supplier<T> { T get(); } (Java 8)

  • interface Consumer<T> { void accept(T t); } (Java 8)

  • interface Comparator<T> { int compare(T o1, T o2); }

  • interface ActionListener { void actionPerformed(ActionEvent e); }

Ob die Schnittstelle noch andere Default-Methoden hat – also Schnittstellenmethoden mit vorgegebener Implementierung –, ist egal, wichtig ist nur, dass sie genau eine zu implementierende Operation deklariert.

 
Zum Seitenanfang

11.2.2Typ eines Lambda-Ausdrucks ergibt sich durch Zieltyp Zur vorigen ÜberschriftZur nächsten Überschrift

In Java hat jeder Ausdruck einen Typ. 1 und 1*2 haben einen Typ (nämlich int), genauso wie "A" + "B" (Typ String) oder String.CASE_INSENSITIVE_ORDER (Typ Comparator<String>). Lambda-Ausdrücke haben auch immer einen Typ, denn ein Lambda-Ausdruck ist immer Exemplar einer funktionalen Schnittstelle. Damit steht auch der Typ fest. Allerdings ist es im Vergleich zu Ausdrücken wie 1*2 bei Lambda-Ausdrücken etwas anders gelagert, denn der Typ von Lambda-Ausdrücken ergibt sich ausschließlich aus dem Kontext. Erinnern wir uns an den Aufruf von sort(…):

Arrays.sort( words, (String s1, String s2) -> { return … ) } );

Dort steht nichts vom Typ Comparator, sondern der Compiler erkennt aus dem Typ des zweiten Parameters von sort(…), ob der Lambda-Ausdruck auf Comparator passt oder nicht.

Der Typ eines Lambda-Ausdrucks ist also abhängig davon, welche funktionale Schnittstelle er im jeweiligen Kontext gerade realisiert. Der Compiler kann ohne Kenntnis des Zieltyps (engl. target type) keinen Lambda-Ausdruck aufbauen.

Typ-Inferenz vom Compiler

Abbildung 11.1Typ-Inferenz vom Compiler

[zB]Beispiel

Callable und Supplier sind funktionale Schnittstellen mit Methoden, die keine Parameterlisten deklarieren und eine Referenz zurückgeben; der Code für den Lambda-Ausdruck sieht gleich aus:

java.util.concurrent.Callable<String> c = () -> { return "Rückgabe"; };
java.util.function.Supplier<String> s = () -> { return "Rückgabe"; };

Wer bestimmt den Zieltyp?

Gerade weil an dem Lambda-Ausdruck der Typ nicht abzulesen ist, kann er nur dort verwendet werden, wo ausreichend Typinformationen vorhanden sind. Das sind unter anderem die folgenden Stellen:

  • Variablendeklarationen: Etwa wie bei Supplier<String> s = () -> { return "" };

  • Argumente an Methoden oder Konstruktoren: Der Parametertyp gibt alle Typinformationen. Ein Beispiel lieferte Arrays.sort(…).

  • Methodenrückgaben: Das könnte aussehen wie Comparator<String> trimComparator() { return (s1, s2) -> { return … }; }.

  • Bedingungsoperator: Der ?:-Operator liefert je nach Bedingung einen unterschiedlichen Lambda-Ausdruck. Beispiel: Supplier<Double> randomNegOrPos = Math.random() > 0.5 ? () -> { return -Math.random(); } : () -> { return Math.random(); };.

Parametertypen

In der Praxis ist der häufigste Fall, dass die Parametertypen von Methoden den Zieltyp vorgeben. Der Einsatz von Lambda-Ausdrücken ändert ein wenig die Sichtweise auf überladene Methoden. Unser Beispiel mit () -> { return "Rückgabe"; } macht das deutlich, denn es »passt« auf den Zieltyp Callable<String> genauso wie auf Supplier<String>. Nehmen wir an, wir würden zwei überladene Methoden run(…) deklarieren:

  • <V> void run( Callable<V> callable ) { }

  • <V> void run( Supplier<V> callable ) { }

Spielen wir den Aufruf der Methoden einmal durch:

Callable<String> c = () -> { return "Rückgabe"; };
Supplier<String> s = () -> { return "Rückgabe"; };
run( c );
run( s );
// run( () -> { return "Rückgabe"; } ); // Compilerfehler
run( (Callable<String>) () -> { return "Rückgabe"; } );

Rufen wir run(c) bzw. run(s) auf, ist das kein Problem, denn c und s sind klar typisiert. Aber run(…) mit dem Lambda-Ausdruck aufzurufen funktioniert nicht, denn der Zieltyp (entweder Callable oder Supplier) ist mehrdeutig; der (Eclipse-)Compiler meldet: »The method run(Callable<Object>) is ambiguous for the type T«. Hier sorgt eine explizite Typanpassung für Abhilfe.

[+]Tipp zum API-Design

Aus Sicht eines API-Designers sind überladene Methoden natürlich schön, aus Sicht des Nutzers sind Typanpassungen aber nicht schön. Um explizite Typanpassungen zu vermeiden, sollte auf überladene Methoden verzichtet werden, wenn diese den Parametertyp einer funktionalen Schnittstelle aufweisen. Stattdessen lassen sich die Methoden unterschiedlich benennen (was bei Konstruktoren natürlich nicht funktioniert).

Wird in unserem Fall die Methode runCallable(…) und runSupplier(…) genannt, ist keine Typanpassung mehr nötig, und der Compiler kann den Typ herleiten.

Rückgabetypen

Typinferenz spielt bei Lambda-Ausdrücken eine große Rolle – das gilt insbesondere für die Rückgabetypen, die überhaupt nicht in der Deklaration auftauchen und für die es gar keine Syntax gibt; der Compiler »inferred« sie. In unserem Beispiel

Comparator<String> c =
(String s1, String s2) -> { return s1.trim().compareTo( s2.trim() ); };

ist String als Parametertyp der Comparator-Methode ausdrücklich gegeben, der Rückgabetyp int, den der Ausdruck s1.trim().compareTo( s2.trim()) liefert, taucht dagegen nicht auf.

Mitunter muss dem Compiler etwas geholfen werden: Nehmen wir die funktionale Schnittstelle Supplier<T>, die eine Methode T get() deklariert, für ein Beispiel. Die Zuweisung

Supplier<Long> two = () -> { return 2; } // Compilerfehler

ist nicht korrekt und führt zum Compilerfehler »incompatible types: bad return type in lambda expression«. 2 ist ein Literal vom Typ int, und der Compiler kann es nicht an Long anpassen. Wir müssen schreiben

Supplier<Long> two = () -> { return 2L };

oder

Supplier<Long> two = () -> { return (long) 2 };

Bei Lambda-Ausdrücken gelten keine wirklich neuen Regeln im Vergleich zu Methodenrückgaben, denn auch eine Methodendeklaration wie

Long two() { return 2; } // Compilerfehler

wird vom Compiler bemängelt. Doch weil Wrapper-Typen durch die Generics bei funktionalen Schnittstellen viel häufiger sind, treten diese Besonderheiten öfter auf als bei Methodendeklarationen.

Sind Lambda-Ausdrücke Objekte?

Ein Lambda-Ausdruck ist ein Exemplar einer funktionalen Schnittstelle und tritt als Objekt auf. Bei Objekten besteht normalerweise zu java.lang.Object immer eine natürliche Ist-eine-Art-von-Beziehung. Fehlt aber der Kontext, ist selbst die Ist-eine-Art-von-Beziehung zu java.lang.Object gestört und Folgendes nicht korrekt:

Object o = () -> {}; // Compilerfehler

Der Compilerfehler ist: »incompatible types: the target type must be a functional interface«. Nur eine explizite Typanpassung kann den Fehler korrigieren und dem Compiler den Zieltyp vorgeben:

Object r = (Runnable) () -> {};

Lambda-Ausdrücke haben also keinen eigenen Typ an sich, und für das Typsystem von Java ändert sich im Prinzip nichts. Möglicherweise ändert sich das in späteren Java-Versionen.

[+]Hinweis

Dass Lambda-Ausdrücke Objekte sind, ist eine Eigenschaft, die nicht überstrapaziert werden sollte. So sind die üblichen Object-Methoden equals(Object), hashCode(), getClass(), toString() und die zur Thread-Kontrolle ohne besondere Bedeutung. Es sollte auch nie ein Szenario geben, in dem Lambda-Ausdrücke mit == verglichen werden müssen, denn das Ergebnis ist laut Spezifikation undefiniert. Echte Objekte haben eine Identität, einen Identity-Hashcode, lassen sich vergleichen und mit instanceof testen, können mit einem synchronisierten Block abgesichert werden; all dies gilt für Lambda-Ausdrücke nicht. Im Grunde charakterisiert der Begriff »Lambda-Ausdruck« schon sehr gut, was wir nie vergessen sollten: Es handelt sich um einen Ausdruck, also etwas, das ausgewertet wird und ein Ergebnis produziert.

 
Zum Seitenanfang

11.2.3Annotation @FunctionalInterface Zur vorigen ÜberschriftZur nächsten Überschrift

Jede Schnittstelle mit genau einer abstrakten Methode eignet sich als funktionale Schnittstelle und damit für einen Lambda-Ausdruck. Jedoch soll nicht jede Schnittstelle in der API, die im Moment nur eine abstrakte Methode deklariert, auch für Lambda-Ausdrücke verwendet werden. So kann zum Beispiel eine Weiterentwicklung der Schnittstelle mit mehreren (abstrakten) Methoden geplant sein, aber zurzeit ist nur eine abstrakte Methode vorhanden. Der Compiler kann nicht wissen, ob sich eine Schnittstelle vielleicht weiterentwickelt. Um kenntlich zu machen, dass ein interface als funktionale Schnittstelle gedacht ist, existiert der Annotationstyp FunctionalInterface im java.lang-Paket. Diese markiert, dass es bei genau einer abstrakten Methode und damit bei einer funktionalen Schnittstelle bleiben soll.

[zB]Beispiel

Eine eigene funktionale Schnittstelle sollte immer als FunctionalInterface markiert werden:

@FunctionalInterface
public interface MyFunctionalInterface {
void foo();
}

Der Compiler prüft, ob die Schnittstelle mit einer solchen Annotation tatsächlich nur exakt eine abstrakte Methode enthält, und löst einen Fehler aus, wenn dem nicht so ist. Aus Kompatibilitätsgründen erzwingt der Compiler diese Annotation bei funktionalen Schnittstellen allerdings nicht; das ermöglicht es, dass innere Klassen, die herkömmliche Schnittstellen mit einer Methode implementieren, einfach in Lambda-Ausdrücke umgeschrieben werden können. Die Annotation ist also keine Voraussetzung für die Nutzung der Schnittstelle in einem Lambda-Ausdruck und dient bisher nur der Dokumentation. In der Java SE sind aber alle zentralen funktionalen Schnittstellen so ausgezeichnet.

 
Zum Seitenanfang

11.2.4Syntax für Lambda-Ausdrücke Zur vorigen ÜberschriftZur nächsten Überschrift

Lambda-Ausdrücke haben wie Methoden mögliche Parameter- und Rückgabewerte. Die Java-Grammatik für die Schreibweise von Lambda-Ausdrücken sieht ein paar nützliche syntaktische Abkürzungen vor.

Ausführliche Schreibweise

Lambda-Ausdrücke lassen sich auf unterschiedliche Art und Weise schreiben, da es für diverse Konstruktionen Abkürzungen gibt. Eine Form, die jedoch immer gilt ist:

'(' LambdaParameter ')' '->' '{' Anweisungen '}'

Der Lambda-Parameter besteht (voll ausgeschrieben) wie ein Methodenparameter aus a) dem Typ, b) dem Namen und c) optionalen Modifizierern.

Der Parametername öffnet einen neuen Gültigkeitsbereich für eine Variable, wobei der Parametername keine anderen Namen von lokalen Variablen überlagern darf. Hier verhält sich die Lambda-Parametervariable wie eine neue Variable aus einem inneren Block und nicht wie eine Variable aus einer inneren Klasse, wo die Sichtbarkeit anders ist.

[zB]Beispiel

Folgendes ergibt einen Compilerfehler im Lambda-Ausdruck, weil var schon deklariert ist; die Parametervariable vom Lambda-Ausdruck muss also »frisch« sein:

String var = "";
var.chars().forEach( var -> { System.out.println( var ); } ); // Compilerfehler

Abkürzung 1: Typinferenz

Der Java-Compiler kann viele Typen aus dem Kontext ablesen, was Typ-Inferenz genannt wird. Wir kennen so etwas vom Diamant-Operator, wenn wir etwa schreiben:

List<String> list = new ArrayList<>()

Sind für den Compiler genug Typ-Informationen verfügbar, dann erlaubt der Compiler bei Lambda-Ausdrücken eine Abkürzung. Bei

Comparator<String> c =
(String s1, String s2) -> { return s1.trim().compareTo( s2.trim() ); };

ist Typ-Inferenz einfach (Comparator<String> sagt alles aus), daher funktioniert die folgende Abkürzung:

Comparator<String> c = (s1, s2) -> { return s1.trim().compareTo( s2.trim() ); };

Die Parameterliste enthält also entweder explizit deklarierte Parametertypen oder implizite Inferred-Typen. Eine Mischung ist nicht erlaubt, der Compiler blockt so etwas wie (String s1, s2) oder (s1, String s2) mit einem Fehler ab.

Wenn der Compiler die Typen ablesen kann, sind die Parametertypen optional. Aber Typ-Inferenz ist nicht immer möglich, weshalb die Abkürzung nicht immer möglich ist. Außerdem hilft die explizite Schreibweise auch der Lesbarkeit: Kurze Ausdrücke sind nicht unbedingt die verständlichsten.

[+]Hinweis

Der Compiler liest aus den Typen ab, ob alle Eigenschaften vorhanden sind. Die Typen sind dabei entweder explizit oder implizit gegeben.

Comparator<String> sc = (a, b) -> { return Integer.compare( a.length(), b.length() ); };
Comparator<BitSet> bc = (a, b) -> { return Integer.compare( a.length(), b.length() ); };

Die Klassen String und BitSet besitzen beide die Methode length(), daher ist der Lambda-Ausdruck korrekt. Der gleiche Lambda-Code lässt sich für zwei völlig verschiedene Klassen einsetzen, die überhaupt keine Gemeinsamkeiten haben, nur dass sie zufällig beide eine Methode namens length() besitzen.

Abkürzung 2: Lambda-Rumpf ist entweder einzelner Ausdruck oder Block

Besteht der Rumpf eines Lambda-Ausdrucks nur aus einem einzelnen Ausdruck, kann eine verkürzte Schreibweise die Blockklammern und das Semikolon einsparen. Statt

( LambdaParameter ) -> { return Ausdruck; }

heißt es dann

( LambdaParameter ) -> Ausdruck

Lambda-Ausdrücke mit einer return-Anweisung im Rumpf kommen häufig vor, da dies den typischen Funktionen entspricht. Somit ist es eine willkommene Verkürzung, wenn die abgekürzte Syntax für Lambda-Ausdrücke lediglich den Ausdruck fordert, der dann die Rückgabe bildet.

Hier sind drei Beispiele:

Lange Schreibweise

Abkürzung

(s1, s2) ->

 { return s1.trim().compareTo( s2.trim() ); }

(s1, s2) ->

 s1.trim().compareTo( s2.trim() )

(a, b) -> { return a + b; }

(a, b) -> a + b

() -> { System.out.println(); }

() -> System.out.println()

Tabelle 11.2Ausführliche und abgekürzte Schreibweise

Ausdrücke können in Java auch zu void ausgewertet werden, sodass ohne Probleme ein Aufruf wie System.out.println() in der kompakten Schreibweise ohne Block gesetzt werden kann. Das heißt, wenn Lambda-Ausdrücke mit der kurzen Ausdruckssyntax eingesetzt werden, können diese Ausdrücke etwas zurückgeben, müssen aber nicht.

[+]Hinweis

Die Schreibweise mit den geschweiften Klammern und den Rückgabe-Ausdrücken kann nicht gemischt werden. Entweder gibt es einen Block geschweifter Klammern und return oder keine Klammern und kein return-Schlüsselwort. Fehler ergeben also diese falschen Mischungen:

Comparator<String> c;
c = (s1, s2) -> { s1.trim().compareTo( s2.trim() ) }; // Compilerfehler (1)
c = (s1, s2) -> return s1.trim().compareTo( s2.trim() ); // Compilerfehler (2)

Würden wir in (1) ein explizites return nutzen wäre alles in Ordnung, würde bei (2) das return wegfallen, wäre die Zeile auch compilierbar.

Ob Lambda-Ausdrücke eine Rückgabe haben, drücken zwei Begriffe aus:

  • void-kompatibel: Der Lambda-Rumpf gibt kein Ergebnis zurück, entweder weil der Block kein return enthält oder ein return ohne Rückgabe oder weil ein void-Ausdruck in der verkürzten Schreibweise eingesetzt wird. Der Lambda-Ausdruck () -> System.out.println() ist also void-kompatibel, genauso wie () -> {}.

  • Wert-kompatibel: Der Rumpf beendet den Lambda-Ausdruck mit einer return-Anweisung, die einen Wert zurückgibt, oder besteht aus der kompakten Schreibenweise mit einer Rückgabe ungleich void.

Eine Mischung aus void- und Wert-kompatibel ist nicht erlaubt und führt wie bei Methoden zu einem Compilerfehler.[ 197 ]

Abkürzung 3: Einzelner Identifizierer statt Parameterliste und Klammern

Besteht die Parameterliste

  1. nur aus einem einzelnen Identifizierer

  2. und ist der Typ durch Typ-Inferenz klar,

können die runden Klammern wegfallen.

Lange Schreibweise

Typen inferred

Vollständig abgekürzt

(String s) -> s.length()

(s) -> s.length()

s -> s.length()

(int i) -> Math.abs( i )

(i) -> Math.abs( i )

i -> Math.abs( i )

Tabelle 11.3Unterschiedlicher Grad von Abkürzungen

Kommen alle Abkürzungen zusammen, lässt sich etwa die Hälfte an Code einsparen. Aus (int i) -> { return Math.abs( i ); } wird einfach i -> Math.abs( i ).

Syntax-Hinweis

Nur bei genau einem Lambda-Parameter können die Klammern weggelassen werden, da es sonst Mehrdeutigkeiten gibt, für die es wieder komplexe Regeln zur Auflösung geben müsste. Heißt es etwa foo( k, v -> { … } ), ist unklar, ob foo zwei Parameter deklariert. Ist das zweite Argument ein Lambda-Ausdruck, oder handelt es sich um nur genau einen Parameter, wobei dann ein Lambda-Ausdruck übergeben wird, der selbst zwei Parameter deklariert? Um Problemen wie diesen aus dem Weg zu gehen, können Entwickler auf den ersten Blick sehen, dass foo( k, v -> { … } ) eindeutig für Parameter steht, und foo( (k, v) -> { … } ) nur einen Parameter besitzt.

Unbenutzte Parameter in Lambda-Ausdrücken

Es kommt vor, dass ein Lambda-Ausdruck eine funktionale Schnittstelle implementiert, aber nicht jeder Parameter von Interesse ist. Als Beispiel schauen wir uns an:

interface Consumer<A> { void apply( A a ); }

Ein Konsument, der das Argument in Hochkommata ausgibt, sieht so aus:

Consumer<String> printQuoted = s -> System.out.printf( "'%s'", s );
printQuoted.accept( "Chris" ); // 'Chris'

Was ist nun, wenn ein Konsument auf das Argument gar nicht zugreifen möchte, weil zum Beispiel die aktuelle Zeit ausgegeben wird?

Consumer<String> printNow =
s -> System.out.println( System.currentTimeMillis() );

Die Variable s in der Lambda-Parameterliste ist ungenutzt und wird vom Compiler auch als »unused« bemängelt. Daher erlaubt eine spezielle Schreibweise, den Variablennamen wegzulassen und nur den Typ anzugeben:

Consumer<String> printNow =
String /* bzw. (String)*/ -> System.out.println( System.currentTimeMillis() );

Der Typ selbst darf nicht entfallen, denn () -> … passt nicht auf die funktionale Schnittstelle Consumer, die immer einen Parameter deklariert.

[+]Hinweis

Nur bei einem Lambda-Parameter ist diese Form der Abkürzung erlaubt. Folgendes führt zu einem Compilerfehler:

BiFunction<Double, Double, Double> bifunc = (Double a, Double /* */) -> Math.signum( a );
 
Zum Seitenanfang

11.2.5Die Umgebung der Lambda-Ausdrücke und Variablenzugriffe Zur vorigen ÜberschriftZur nächsten Überschrift

Ein Lambda-Ausdruck »sieht« seine Umgebung genauso wie der Code, der vor oder nach dem Lambda-Ausdruck steht. Insbesondere hat ein Lambda-Ausdruck vollständigen Zugriff auf alle Eigenschaften der Klasse, genauso wie auch der einschließende äußere Block sie hat. Es gibt keinen besonderen Namensraum, sondern nur neue und vielleicht überdeckte Variablen durch die Parameter. Das ist einer der grundlegenden Unterschiede zwischen Lambda-Ausdrücken und inneren Klassen. Somit ist auch die Bedeutung von this und super bei Lambda-Ausdrücken und inneren Klassen unterschiedlich.

Zugriff auf finale, lokale Variablen/Parametervariablen

Lambda-Ausdrücke können problemlos auf Objektvariablen und Klassenvariablen lesend und schreibend zugreifen. Auch auf lokale Variablen und Parameter hat ein Lambda-Ausdruck Zugriff. Doch greift ein Lambda-Ausdruck auf lokale Variablen bzw. Parametervariablen zu, müssen diese final sein. Dass eine Variable final ist, muss nicht extra mit einem Modifizierer geschrieben werden, aber sie muss effektiv final (engl. effectively final) sein. Effektiv final ist eine Variable, wenn sie nach der Initialisierung nicht mehr beschrieben wird.

Ein Beispiel: Der Benutzer soll über eine Eingabe die Möglichkeit bekommen, zu bestimmen, ob String-Vergleiche mit unserem trimmenden Comparator unabhängig von der Groß-/Kleinschreibung stattfinden sollen:

public class CompareIgnoreCase {
public static void main( String[] args ) {
/*final*/ boolean compareIgnoreCase = new Scanner( System.in ).nextBoolean();
Comparator<String> c = (s1, s2) -> compareIgnoreCase ?
s1.trim().compareToIgnoreCase( s2.trim() ) :
s1.trim().compareTo( s2.trim() );
String[] words = { "M", "\nSkyfall", " Q", "\t\tAdele\t" };
Arrays.sort( words, c );
System.out.println( Arrays.toString( words ) );
}
}

Ob compareIgnoreCase von uns final gesetzt wird oder nicht, ist egal, denn die Variable wird hier effektiv final verwendet. Natürlich kann es nicht schaden, final als Modifizierer immer davorzusetzen, um dem Leser des Codes diese Tatsache bewusst zu machen.

Neu eingeschobene Lambda-Ausdrücke, die auf lokale Variablen bzw. Parametervariablen zugreifen, können also im Nachhinein zu Compilerfehlern führen. Folgendes Segment ist ohne Lambda-Ausdruck korrekt:

/*1*/ boolean compareIgnoreCase = new Scanner( System.in ).nextBoolean();
/*2*/ …
/*3*/ compareIgnoreCase = true;

Schiebt sich zwischen Zeile 1 und 3 nachträglich ein Lambda-Ausdruck, der auf compareIgnoreCase zugreift, gibt es anschließend einen Compilerfehler. Allerdings liegt der Fehler nicht in Zeile 3, sondern beim Lambda-Ausdruck. Denn die Variable compareIgnoreCase ist nach der Änderung nicht mehr effektiv final, was sie aber sein müsste, um in dem Lambda-Ausdruck verwendet zu werden.

[+]Tipp

Lambda-Ausdrücke verhalten sich genauso wie innere anonyme Klassen, die auch nur auf finale Variablen zugreifen können. Mit Behältern wie einem Feld oder den speziellen AtomicXXX-Klassen aus dem java.util.concurrent.atomic-Paket lässt sich das Problem im Prinzip lösen. Denn greift ein Lambda-Ausdruck etwa auf das Feld boolean[] compareIgnoreCase = new boolean[1]; zu, so ist die Variable compareIgnoreCase selbst final, aber compareIgnoreCase [0] = true; ist erlaubt, denn es ist ein Schreibzugriff auf das Feld, nicht auf die Variable compareIgnoreCase. Je nach Code besteht jedoch die Gefahr, dass Lambda-Ausdrücke parallel ausgeführt werden. Wird etwa ein Lambda-Ausdruck mit Veränderung auf diesem Feldinhalt parallel ausgeführt, so ist der Zugriff nicht synchronisiert, und das Ergebnis kann »kaputt« sein, denn paralleler Zugriff auf Variablen muss immer koordiniert vorgenommen werden.

Namensräume

Deklariert eine innere anonyme Klasse Variablen innerhalb der Methode, so sind diese immer »neu«, das heißt, die neuen Variablen überlagern vorhandene lokale Variablen aus dem äußeren Kontext. Die Variable compareIgnoreCase kann im Rumpf von compare(…) zum Beispiel problemlos neu deklariert werden:

boolean compareIgnoreCase = true;
Comparator<String> c = new Comparator<String>() {
@Override public int compare( String s1, String s2 ) {
boolean compareIgnoreCase = false; // völlig ok
return …
}
};

In einem Lambda-Ausdruck ist das nicht möglich, und Folgendes führt zu einer Fehlermeldung des Compilers: »variable compareIgnoreCase ist already defined«.

boolean compareIgnoreCase = true;
Comparator<String> c = (s1, s2) -> {
boolean compareIgnoreCase = false; //  Compilerfehler
return …
}

this-Referenz

Ein Lambda-Ausdruck unterscheidet sich von einer inneren (anonymen) Klasse auch in dem, worauf die this-Referenz verweist:

  • Beim Lambda-Ausdruck zeigt this immer auf das Objekt, in dem der Lambda-Ausdruck eingebettet ist.

  • Bei einer inneren Klasse referenziert this die innere Klasse, und die ist ein komplett neuer Typ.

Folgendes Beispiel macht das deutlich:

Listing 11.2InnerVsLambdaThis.java

class InnerVsLambdaThis {
InnerVsLambdaThis() {
Runnable lambdaRun = () -> System.out.println( this.getClass().getName() );
Runnable innerRun = new Runnable() {
@Override public void run() { System.out.println( this.getClass().getName()); }
};

lambdaRun.run(); // InnerVsLambdaThis
innerRun.run(); // InnerVsLambdaThis$1
}
public static void main( String[] args ) {
new InnerVsLambdaThis();
}
}

Als Erstes nutzen wir this in einen Lambda-Ausdruck im Konstruktor der Klasse InnerVsLambdaThis. Damit bezieht sich this auf jedes gebaute InnerVsLambdaThis-Objekt. Bei der inneren Klasse referenziert this ein anderes Exemplar, und zwar vom Typ Runnable. Da es bei anonymen Kassen keinen Namen hat, trägt es lediglich die Kennung InnerVsLambdaThis$1.

Rekursive Lambda-Ausdrücke

Lambda-Ausdrücke können auf sich selbst verweisen. Da aber ein this zur Selbstreferenz nicht funktioniert, ist ein kleiner Umweg nötig. Erst muss eine Objekt- oder eine Klassenvariable deklariert werden, dann muss dieser Variablen ein Lambda-Ausdruck zugewiesen werden, und dann kann der Lambda-Ausdruck auf diese Variable zugreifen und einen rekursiven Aufruf starten. Für den Klassiker der Fakultät sieht das so aus:

public class RecursiveFactLambda {
public static IntFunction<Integer> fact = n -> (n == 0) ? 1 : n * fact.apply(n-1);
public static void main( String[] args ) {
System.out.println( fact.apply( 5 ) ); // 120
}
}

IntFunction ist eine funktionale Schnittstelle aus dem Paket java.util.function mit einer Operation T apply(int i). T ist ein generischer Rückgabetyp, den wir hier mit Integer belegt haben.

fact hätte genauso gut als normale Methode deklariert werden können. Großartige Vorteile bietet die Schreibweise mit Lambda-Ausdrücken hier nicht. Zumal jetzt auch der Begriff anonyme Methode nicht mehr so richtig passt, da der Lambda-Ausdruck ja doch einen Namen hat, nämlich fact. Und weil der Lambda-Ausdruck einer Variablen zugewiesen wurde, kann er in dieser Form natürlich auch nicht mehr als Implementierung an eine Methode oder einen Konstruktor übergeben werden, sondern nur als Methoden-/Konstruktor-Referenz, dazu später mehr.

 
Zum Seitenanfang

11.2.6Ausnahmen in Lambda-Ausdrücken Zur vorigen ÜberschriftZur nächsten Überschrift

Lambda-Ausdrücke sind Implementierungen von funktionalen Schnittstellen, und bisher haben wir noch nicht die Frage betrachtet, was passiert, wenn der Codeblock vom Lambda-Ausdruck eine Ausnahme auslöst, und wer diese auffangen muss.

Ausnahmen im Codeblock eines Lambda-Ausdrucks

In java.util.function gibt es eine funktionale Schnittstelle Predicate, deren Deklaration im Kern wie folgt ist:

public interface Predicate<T> { boolean test( T t ); }

Ein Predicate führt einen Test durch und liefert wahr oder falsch als Ergebnis. Ein Lambda-Ausdruck kann diese Schnittstelle nun implementieren. Nehmen wir an, wir wollten testen, ob eine Datei die Länge 0 hat, um etwa Dateileichen zu finden. In einer ersten Idee greifen wir auf die existierende Files-Klasse zurück, die size(…)anbietet:

Predicate<Path> isEmptyFile = path -> Files.size( path ) == 0; // Compilerfehler

Problem dabei ist, dass Files.size(…) eine IOException auslöst, die behandelt werden muss, und zwar nicht vom Block, in dem der Lambda-Ausdruck als Ganzes steht, sondern vom Code im Lambda-Ausdruck selbst. Das schreibt der Compiler so vor. Folgendes ist also keine Lösung:

try {
Predicate<Path> isEmptyFile = path -> Files.size( path ) == 0; //
} catch ( IOException e ) { … }

sondern nur:

Predicate<Path> isEmptyFile = path -> {
try {
return Files.size( path ) == 0;
} catch ( IOException e ) { return false; }
};

Die Eigenschaft, die Java fehlt, nennt sich Exception-Transparenz, und hier ist deutlich der Unterschied zwischen geprüften und ungeprüften Ausnahmen zu sehen. Bei der Exception-Transparenz wäre keine Ausnahmebehandlung im Lambda-Ausdruck nötig und an einer übergeordneten Stelle möglich. Doch da diese Möglichkeit in Java fehlt, bleibt uns nur übrig, geprüfte Ausnahmen im Lambda-Ausdrücken direkt zu behandeln.

Funktionale Schnittstellen mit throws-Klausel

Ungeprüfte Ausnahmen können immer auftreten und führen (nicht abgefangen) wie üblich zum Abbruch des Threads. Eine throws-Klausel an den Methoden/Konstruktoren ist dafür nicht nötig. Doch können funktionale Schnittstellen eine throws-Klausel mit geprüften Ausnahmen deklarieren, und die Implementierung einer funktionalen Schnittstelle kann logischerweise geprüfte Ausnahmen auslösen.

Eine Deklaration wie Callable aus dem Paket java.util.concurrent macht das deutlich. (Callable trägt kein @FunctionalInterface):

public interface Callable<V> {
V call() throws Exception;
}

Das könnte durch folgenden Lambda-Ausdruck realisiert werden:

Callable<Integer> randomDice = () -> (int)(Math.random() * 6) + 1;

Der Aufruf von call() auf einem randomDice muss mit einer Ausnahmebehandlung einhergehen, da call() eine Exception auslöst, etwa so:

try {
System.out.println( randomDice.call() );
System.out.println( randomDice.call() );
}
catch ( Exception e ) { … }

Dass der Aufrufer die Ausnahme behandeln muss, ist klar. Die Deklaration des Lambda-Ausdrucks enthält keinen Hinweis auf die Ausnahme, das ist ein Unterschied zum vorangegangenen Abschnitt.

Design-Tipp

Ausnahmen in Methoden funktionaler Schnittstellen schränken den Nutzen stark ein, und daher löst keine der funktionalen Schnittstellen aus etwa java.util.function eine geprüfte Ausnahme aus. Der Grund ist einfach, denn jeder Methodenaufrufer müsste sonst entweder die Ausnahme weiterleiten oder behandeln.[ 198 ]

Um die Einschränkungen und Probleme mit einer throws-Klausel noch etwas deutlicher zu machen, stellen wir uns vor, dass die funktionale Schnittstelle Predicate ein throws Exception (vom Sinn der Typs Exception an sich einmal abgesehen) enthält:

interface Predicate<T> { boolean test( T t ) throws Exception; } // Was wäre wenn?

Die Konsequenz wäre, dass jeder Aufrufer von test(…) nun seinerseits die Exception in die Hände bekäme und sie auffangen oder weiterleiten müsste. Leitet der test(…)-Aufrufer mit throws Exception die Ausnahme weiter nach oben, bekommen wir plötzlich an allen Stellen ein throws Exception in die Methodensignatur, was auf keinen Fall gewünscht ist. So enthält jetzt etwa ArrayList eine Deklaration von removeIf(Predicate filter); hier müsste sich dann removeIf(…) – was letztendlich filter.test(…) aufruft – mit der Testausnahme rumärgern, und removeIf(Predicate filter) throws Exception ist keine gute Sache.

Von geprüft nach ungeprüft

Geprüfte Ausnahmen sind in Lamba-Ausdrücken nicht schön. Eine Lösung ist, Code, der geprüfte Ausnahmen auslöst, zu verpacken und die geprüfte Ausnahme in einer ungeprüften zu manteln. Das kann etwa so aussehen:

Listing 11.3PredicateWithException.java

public class PredicateWithException {
@FunctionalInterface
public interface ExceptionalPredicate<T, E extends Exception> {
boolean test( T t ) throws E;
}
public static <T> Predicate<T> asUncheckedPredicate( ExceptionalPredicate<T, Exception> predicate ) {
return t -> {
try {
return predicate.test( t );
}
catch ( Exception e ) {
throw new RuntimeException( e.getMessage(), e );
}
};
}
public static void main( String[] args ) {
Predicate<Path> isEmptyFile = asUncheckedPredicate( path -> Files.size( path ) == 0 );
System.out.println( isEmptyFile.test( Paths.get( "c:/" ) ) );
}
}

Die Schnittstelle ExceptionalPredicate ist ein Prädikat mit optionaler Ausnahme. In der eigenen Hilfsmethode asUncheckedPredicate(ExceptionalPredicate) nehmen wir so ein ExceptionalPredicate an und packen es in ein Predicate, was die Methode zurückgibt. Geprüfte Ausnahmen werden in eine ungeprüfte Ausnahme vom Typ RuntimeException gesetzt. Somit muss Predicate keine geprüfte Ausnahme weiterleiten, was es ja laut Deklaration auch nicht kann.

Die Java-Bibliothek selbst bringt keine Ummantelungen dieser Art mit. Es gibt nur eine interne Methode, die etwas Vergleichbares tut:

Listing 11.4java.nio.file.Files.java, asUncheckedRunnable(…)

/**
* Convert a Closeable to a Runnable by converting checked IOException
* to UncheckedIOException
*/
private static Runnable asUncheckedRunnable( Closeable c ) {
return () -> {
try {
c.close();
}
catch ( IOException e ) {
throw new UncheckedIOException( e );
}
};
}

Hier kommt die Klasse UncheckedIOException zum Einsatz. Diese ist eine ungeprüfte Ausnahme, die als Wrapper-Klasse für Ein-/Ausgabefehler genutzt wird. Wir finden die UncheckedIOException etwa bei lines() von BufferedReader bzw. Files, die einen Stream<String> mit Zeilen liefert – geprüfte Ausnahmen sind hier nur im Weg.

 
Zum Seitenanfang

11.2.7Klassen mit einer abstrakten Methode als funktionale Schnittstelle? * Zur vorigen ÜberschriftZur nächsten Überschrift

Als die Entwickler der Sprache Java die Lambda-Ausdrücke diskutierten, stand auch die Frage im Raum, ob abstrakte Klassen, die nur über eine abstrakte Methode verfügen, ebenfalls für Lambda-Ausdrücke genutzt werden können.[ 199 ] Sie entschieden sich dagegen, unter anderem deswegen, weil bei der Implementierung von Schnittstellen die JVM weitreichende Optimierungen vornehmen kann. Und bei Klassen wird das schwierig. Das liegt auch daran, dass ein Konstruktor umfangreiche Initialisierungen mit Seiteneffekten vornimmt (die Konstruktoren aller Oberklassen nicht zu vergessen) sowie Ausnahmen auslösen könnte. Gewünscht ist aber nur die Ausführung einer Implementierung der funktionalen Schnittstelle und kein anderer Code.

Es gibt nun im JDK einige abstrakte Klassen, die genau eine abstrakte Methode vorschreiben, etwa java.util.TimerTask. Solche Klassen können nicht über einen Lambda-Ausdruck realisiert werden; hier müssen Entwickler weiterhin zu Klassenimplementierungen greifen, und die kürzeste Lösung ist eine innere anonyme Klasse. Eigene Hilfsklassen können natürlich den Code etwas abkürzen, aber eben nur mithilfe einer eigenen Implementierung.

Wer abstrakte Methoden mit Lambda-Ausdrücken implementieren möchte, kann mit Hilfsklassen arbeiten. Denn wenn eine Hilfsklasse funktionale Schnittstellen einsetzt, so können Lambda-Ausdrücke wieder ins Spiel kommen, indem die Implementierung der abstrakten Methode an den Lambda-Ausdruck weiterleitet. Nehmen wir das Beispiel für TimerTask und gehen zwei unterschiedliche Strategien der Implementierung durch. Mit Delegation sieht das so aus:

Listing 11.5TimerTaskLambda.java

import java.util.*;
class TimerTaskLambda {
public static TimerTask createTimerTask( Runnable runnable ) {
return new TimerTask() {
@Override public void run() { runnable.run(); }
};
}
public static void main( String[] args ) {
new Timer().schedule( createTimerTask( () -> System.out.println("Hi") ), 500 );
}
}

Mit Vererbung erhalten wir:

public class LambdaTimerTask extends TimerTask {
private final Runnable runnable;

public LambdaTimerTask( Runnable runnable ) {
this.runnable = runnable;
}

@Override public void run() { runnable.run(); }
}

Der Aufruf erfolgt dann statt über createTimerTask(…) mit dem Konstruktor:

new Timer().schedule( new LambdaTimerTask( () -> System.out.println("Hi") ), 500 );

 


Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.

>> Zum Feedback-Formular
<< zurück
 Zum Katalog
Zum Katalog: Java ist auch eine Insel Java ist auch eine Insel
Jetzt bestellen

 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Katalog: Java SE 8 Standard-Bibliothek
Java SE 8 Standard-Bibliothek


Zum Katalog: Professionell entwickeln mit Java EE 7
Professionell entwickeln mit Java EE 7


Zum Katalog: Schrödinger programmiert Java
Schrödinger programmiert Java


Zum Katalog: Einführung in Java
Einführung in Java


Zum Katalog: Programmieren lernen mit Java
Programmieren lernen mit Java


Zum Katalog: Apps entwickeln für Android 5
Apps entwickeln für Android 5


Zum Katalog: Apps entwickeln mit Android Studio
Apps entwickeln mit Android Studio


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo

 
 


Copyright © Rheinwerk Verlag GmbH 2016
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 
Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de