|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Effective Java - Java 8 - Lambda Expressions & Method References
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
In diesem
Beitrag sehen wir uns Lambda-Ausdrücke und Methoden-Referenzen an.
Beides sind neue Sprachmittel, die mit Java 8 hinzugekommen sind und einen
eher funktionalen Programmierstil in Java unterstützen.
Wie aus der „Closure-Debatte“ das „Project Lambda“ entstanden ist
Diskussionen
über Spracherweiterungen in Java für funktionale Programmierung hatte
es schon vor einigen Jahren gegeben. Seit der Freigabe von Java 5 wurde
heftig und intensiv darüber nachgedacht, wie solche Erweiterungen aussehen
könnten. Es gab drei konkrete Vorschläge in dieser als „Closure-Debatte“
bekannt gewordenen Anstrengung (siehe /
CLO
/). Neil
Gafter, vormals als Compiler-Experte bei Sun tätig, hatte sogar einen
Prototyp-Compiler für den Vorschlag gebaut, an dem er mitgewirkt hatte.
Dennoch hat sich keine Konvergenz der drei Closure-Vorschläge ergeben.
Es hatte auch keiner der drei Vorschläge die uneingeschränkte Unterstützung
von Sun Microsystems. Zu allem Überfluss wurde dann noch Sun Microsystems
vom Oracle-Konzern übernommen und die Closure-Diskussion ist ergebnislos
im Sande verlaufen. Es sah dann erst mal so aus, als würde es in Java
keine Erweiterungen für die funktionale Programmierung geben.
Im Jahr 2009 hat sich
dann die Erkenntnis durchgesetzt, dass Java ohne Closures (oder Lambdas,
wie sie fortan hießen) gegenüber anderen Programmiersprachen veraltet
aussehen könnte. Erstens gibt es Closure- bzw. Lambda-artige Sprachmittel
in einer ganzen Reihe von Sprachen, die auf der JVM ablaufen. Zweitens
braucht man auf Multi-CPU- und Multi-Core-Hardware eine einfache Unterstützung
für die Parallelisierung von Programmen. Denn, was nützen die vielen
Cores, wenn die Applikation sie nicht nutzt, weil sie in weiten Teilen
sequentiell und nur in geringem Umfang parallel arbeitet.
Nun bietet der JDK mit
seinen Concurrency Utilities im
java.util.concurrent
-Package
umfangreiche Unterstützung für die Parallelisierung. Die Handhabung
dieser Concurrency Utilities ist aber anspruchsvoll, erfordert Erfahrung
und wird allgemein als schwierig und fehleranfällig angesehen. Eigentlich
bräuchte man für die Parallelisierung bequemere, weniger fehleranfällige
und einfach zu benutzende Mittel. Doug Lea, der sich schon seit vielen
Jahren um die Spezifikation und Implementierung der Concurrency Utilities
in Java kümmert, hat dann prototypisch eine Abstraktion
ParallelArray
gebaut, um zu demonstrieren, wie eine Schnittstelle aussehen könnte für
die parallele Ausführung von Operationen auf Sequenzen von Elementen (siehe
/
PAR
/). Die Sequenz war einfach ein Array von Elementen
mit Operationen, die paralleles Sortieren, paralleles Filtern, sowie das
parallele Anwenden von beliebiger Funktionalität auf alle Elemente der
Sequenz zur Verfügung gestellt hat. Dabei hat sich herausgestellt, dass
eine solche Abstraktion ohne Closures/Lambdas nicht gut zu benutzen ist.
Deshalb gibt es seitdem
bei Oracle unter der Leitung von Brian Goetz (der vielen Lesern vielleicht
als Autor des Buchs „Java Concurrency in Practice“ bekannt ist) ein
„Project Lambda“, d.h. eine Arbeitsgruppe, die die neuen Lambda-Sprachmittel
definiert und gleichzeitig neue Abstraktionen für den JDK-Collection-Framework
spezifiziert und implementiert (siehe /
LAM
/) hat.
Ein
ParallelArray
wird es in Java 8 zwar
nicht geben; das war nur ein Prototyp, der Ideen geliefert hat. Stattdessen
wird es sogenannte
Streams
geben. Und aus dem anfänglich als
Closure bezeichneten Sprachmittel sind im Laufe der Zeit Lambda-Ausdrücke
sowie Methoden- und Konstruktor-Referenzen entstanden.
Diese Lambda-Ausdrücke
bzw. Methoden- / Konstruktor-Referenzen wollen wir uns im Folgenden genauer
ansehen (siehe auch /
TUT
/).
Wie sieht ein Lambda-Ausdruck aus?
Wir haben im letzten Beitrag
bereits Lambda-Ausdrücke gezeigt und zwar am Beispiel der Verwendung der
forEach
-Methode.
In Java 8 haben alle Collections eine
forEach
-Methode,
die sie von ihrem Super-Interface
Iterable
erben. Das
Iterable
-Interface gibt es
schon seit Java 5; es ist erweitert worden und sieht in Java 8 so aus:
public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) { for (T t : this) { action.accept(t); } }
}
Das
Iterable
-Interface
hat zusätzlich zur
iterator
-Methode,
die es schon immer hatte, eine
forEach
-Methode
bekommen. Die
forEach
-Methode iteriert
über alle Elemente in der Collection und wendet auf jedes Element eine
Funktion an, die der Methode als Argument vom Typ
Consumer
übergeben wird. Die Benutzung der
forEach
-Methode
sieht dann zum Beispiel so aus:
List<Integer> numbers = new ArrayList<>(); ... populate list ...
numbers.forEach(
i ->
System.out
.
println
(i)
);
Als Consumer haben wir einen Lambda-Ausdruck übergeben (im Code farbig hervorgehoben), der alle Integer-Werte aus der Collection nach System.out ausgibt. Ein Lambda-Ausdruck besteht aus einer Parameterliste (das ist der Teil vor dem " -> "-Symbol) und einem Rumpf (das ist der Teil nach dem " -> "-Symbol). Für Parameterliste und Rumpf gibt es mehrere syntaktische Möglichkeiten. Hier die vereinfachte Version der Syntax für Lambda-Ausdrücke:
LambdaE
xpression:
LambdaParameters:
LambdaBody:
Lambda-Parameterliste
Die Parameterliste ist
entweder eine kommagetrennte Liste in runden Klammern oder ein einzelner
Bezeichner ohne runde Klammern. Wenn man die Liste in Klammern verwendet,
dann kann man sich entscheiden, ob man für alle Parameter den Parametertyp
explizit hinschreiben will oder ob man den Typ weglässt und ihn vom Compiler
automatisch bestimmen lässt. Hier ein paar Beispiele:
Lambda-Body
Der Rumpf ist entweder
ein einzelner Ausdruck oder eine Liste von Anweisungen in geschweiften
Klammern. Hier ein paar Beispiele:
Das Prinzip für die Syntax ist recht einfach. Wenn die Parameterliste oder der Rumpf ganz simpel sind, dann darf man sogar die Klammern weglassen; wenn sie ein bisschen komplexer sind, muss man die Klammern setzen. Typdeduktion und SAM-Typen
Die Syntax für Lambda-Ausdrücke
ist knapp und kurz. Es stellt sich die Frage: wo nimmt der Compiler all
die Information her, die wir weggelassen dürfen? Wenn wir beispielsweise
in der Parameterliste die Typen weglassen, dann muss der Compiler sich
die Typen selber überlegen. Wie macht er das? Man lässt bei den Lambda-Ausdrücken
grundsätzlich den Returntyp und die Exception-Spezifikation weg. Woher
nimmt der Compiler diese Information? Was ist eigentlich überhaupt der
Typ eines Lambda-Ausdrucks? Dazu hatten wir im letzten Beitrag bereits
erläutert, dass der Compiler den Typ eines Lambda-Ausdrucks aus dem umgebenden
Kontext deduziert. Sehen wir uns das noch einmal genauer an.
Zunächst einmal hat man
sich beim Design der Lambda-Ausdrücke überlegt, dass das Typsystem von
Java nach Möglichkeit nicht gravierend geändert werden soll. Man hätte
prinzipiell hingehen können und eine neue Kategorie von Typen für Lambda-Ausdrücke
erfinden können. Dann hätte es neben primitiven Typen, Klassen, Interfaces,
Enum-Typen, Array-Typen und Annotation-Typen auch noch Funktionstypen gegeben.
Funktionstypen hätten Signaturen beschrieben, z.B.
void(String,String)IOException
für einen Lambda-Ausdruck, der zwei Strings als Parameter nimmt, nichts
zurück gibt und
IOException
s wirft.
Diesen heftigen Eingriff ins Typsystem wollte man aber vermeiden. Stattdessen
hat man nach einer Möglichkeit gesucht, wie man herkömmliche Typen für
die Lambda-Ausdrücke verwenden könnte.
Man hat sich also überlegt,
welche schon existierenden Typen in Java einem Funktionstyp am ähnlichsten
sind und hat festgestellt, dass es eine ganze Menge Interfaces gibt, die
nur eine einzige Methode haben. Beispiele sind
Runnable
,
Callable
,
AutoCloseable
,
Comparable
,
Iterable
,
usw. Diese Interfaces beschreiben Funktionalität und ihre einzige Methode
hat eine Signatur mit Parametertypen, Returntyp und Exception-Spezifikation
- also genau der Information, die auch ein Funktionstyp repräsentieren
würde. Also hat man sich eine Strategie überlegt, wie man Lambda-Ausdrücke
auf Interfaces mit einer einzigen Methode abbilden kann.
Solche Interfaces mit
einer einzigen abstrakten Methode haben deshalb in Java 8 im Zusammenhang
mit den Lambda-Ausdrücken eine besondere Bedeutung. Man bezeichnet sie
als
Functional Interface Types
(bisweilen auch
SAM Types
genannt, wobei SAM für Single Abstract Method steht). Man kann sie mit
einer speziellen Annotation, nämlich
@FunctionalInterface
,
markieren. Sie sind die einzigen Typen, die der Compiler für Lambda-Ausdrücke
verwenden kann.
Der SAM Type für einen
Lambda-Ausdruck wird vom Java-Entwickler niemals explizit spezifiziert,
sondern immer vom Compiler in Rahmen einer Typ-Deduktion aus dem Kontext
bestimmt, in dem der Lambda-Ausdruck vorkommt. Sehen wir uns dazu Beispiele
von Lambda-Ausdrücken in einem Zuweisungskontext an:
BiPredicate<String,String> sp1
=
(s,t) -> s.equalsIgnoreCase(t)
;
// 1
BiFunction<String,String,Boolean> sp2 =
(s,t)
-> s.equalsIgnoreCase(t)
;
// 2
Auf der linken Seite der
beiden Zuweisungen stehen Variablen vom Typ
BiPredicate<String,String>
bzw.
BiFunction<String,String,Boolean>
.
BiPredicate
und
BiFunction
sind Interfaces aus dem
Package
java.util.function
, das es in
Java 8 im JDK gibt. Die Interfaces sehen (vereinfacht) so aus:
public interface BiPredicate<T, U> { boolean test(T t, U u); } public interface BiFunction<T, U, R> { R apply(T t, U u);
}
Auf der rechten Seite der Zuweisungen steht in beiden Fällen der gleiche Lambda-Ausdruck. Wie passen linke und rechte Seite der Zuweisung zusammen? Der Compiler schaut sich zunächst einmal an, ob die linke Seite der Zuweisung ein SAM Type ist. Das ist in beiden Zuweisungen der Fall. Dann ermittelt der Compiler die Signatur der Methode in dem SAM Type. Das BiPredicate -Interface in Zeile //1 hat eine test -Methode mit der Signatur boolean(String,String ) . Das Bi Function -Interface in Zeile //2 hat eine apply -Methode mit der Signatur B oolean(String,String) . Nun schaut der Compiler den Lambda-Ausdruck auf der rechten Seite an und prüft, ob der Lambda-Ausdruck eine dazu passende Signatur hat. Die Parametertypen fehlen im Lambda-Ausdruck. Da auf der linken Seite String s als Parameter verlangt werden, nimmt der Compiler an, dass auf der rechten Seite s und t vom Typ String sein sollten. Dann wird geprüft, ob die String -Klasse eine Methode equalsIgnoreCase hat, die einen String als Argument akzeptiert. Diese Methode existiert in der String -Klasse; sie gibt einen boolean -Wert zurück und wirft keine checked Exceptions. Die Exception-Spezifikation passt also, der Returntyp passt im ersten Fall auch und im zweiten Fall mit Hilfe von Autoboxing. Wie man sieht, hat der Compiler im Laufe dieses Deduktionsprozesses nicht nur die fehlenden Parametertypen des Lambda-Ausdrucks bestimmt, sondern auch den Returntyp und die Exception-Spezifikation. Außerdem hat er einen SAM Type für jeden der Lambda-Ausdrücke gefunden. Deduktionskontext
Ein Lambda-Ausdruck kann
im Source-Code nur an Stellen stehen, wo es einen Deduktionskontext gibt,
den der Compiler auflösen kann. Zulässig sind Lambda-Ausdrücke deshalb
nur an folgenden Stellen:
•
auf
der rechten Seite von Zuweisungen (wie im obigen Beispiel),
•
als
Argumente in einem Methoden-Aufruf,
•
als
Returnwert in einer
return
-Anweisung,
und
•
in
einem Cast-Ausdruck.
Der Deduktionsprozess
ist in allen Fällen ähnlich. Den Zuweisungskontext haben wir uns im obigen
Beispiel bereits angesehen: bei der Zuweisung ist der Typ auf der linken
Seite der Zuweisung der Zieltyp, zu dem der Lambda-Ausdruck auf der rechten
Seite kompatibel sein muss. Beim Methodenaufruf ist der deklarierte Parametertyp
der aufgerufenen Methode der Zieltyp, zu dem der Lambda-Ausdruck kompatibel
sein muss. Bei der
return
-Anweisung
ist der deklarierte Returntyp der Methode, in der die
return
-Anweisung
steht, der Zieltyp. Beim Cast-Ausdruck ist der Zieltyp des Casts der
Zieltyp für den Lambda-Ausdruck.
Es kann aber auch vorkommen, dass ein Lambda-Ausdruck
in einem zulässigen Kontext vorkommt und die Typdeduktion dennoch scheitert.
Hier ist ein Beispiel:
Object o =
(s,t)
-> s.equalsIgnoreCase(t
)
; //
error: Object is not a functional type
Das ist ein Zuweisungskontext
und deshalb prinzipiell erlaubt, aber der Typ
Object
auf der linken Seite ist kein SAM-Typ. Also scheitert die Typdeduktion.
Hier kann man sich behelfen, indem man einen Cast einfügt.
Object o = (BiPredicate<String,String>)
(s,t)
-> s.equalsIgnoreCase(t
)
;
Jetzt steht der Lambda-Ausdruck in einem Cast-Kontext und der Zieltyp des Casts ist ein SAM-Typ, mit dem der Compiler die erforderliche Typdeduktion durchführen kann. Wir haben nun die Syntax für Lambda-Ausdrücke kennen gelernt und gesehen, dass der Typ eines Lambda-Ausdruck immer vom Compiler aus dem Kontext deduziert wird und immer ein SAM-Typ sein muss. Was darf nun im Rumpf eines Lambda-Ausdrucks stehen? Genauer gesagt, auf welche Variablen und Felder hat man im Lambda-Body Zugriff? Variable Binding
Im Lambda-Body hat man natürlich Zugriff
auf die Parameter und lokale Variablen des Lambda-Ausdrucks. Manchmal
möchte man aber auch auf Variablen des umgebenden Kontextes zugreifen.
Hier ist ein einfaches Beispiel. Wir verwenden darin den SAM Type
IntUnaryOperator
aus
dem
java.util.function
-Package. Dieser
Typ sieht so aus:
@FunctionalInterface public interface IntUnaryOperator { int applyAsInt(int operand);
}
Das Beispiel selbst verwendet diverse Abstraktionen
aus dem Stream-Framework und sieht so aus.
private static void test() { int factor = 1000; // 1 IntUnaryOperator times1000 = (int x ) -> { return x * factor ; } ; // 2 Arrays.stream(new int[]{1, 2, 3, 4, 5}).map(times1000).forEach(System.out::println); // 3
}
Nur kurz zur Erläuterung: in Zeile //3 machen
wir aus einem
int
-Array einen
Stream
,
dessen
map
-Methode wir benutzen, um alle
Elemente in dem Array mit Hilfe der Funktion
times1000
auf einen neuen
int
-Wert abzubilden und
anschließend werden die neuen Werte nach
System.out
ausgegeben. Eigentlich geht es aber um den blau eingefärbten Lambda-Ausdruck.
Wir verwenden im Lambda-Body nicht nur den
Parameter
x
des Lambda-Ausdrucks, sondern
auch die Variable
factor
aus dem umgebenden
Kontext. Das ist erlaubt. Alle Variablen, die im Lambda-Ausdruck verwendet
werden, aber nicht im Lambda-Ausdruck selbst definiert wurden, haben dieselbe
Bedeutung wie im umgebenden Kontext. Die einzige Voraussetzung ist, dass
die betreffenden lokalen Variablen "effectively final" sind, d.h. sie dürfen
nicht geändert werden - weder im Lambda-Ausdruck noch im umgebenden Kontext.
[1]
Folgendes wäre also falsch:
private static void test() { int factor = 1000; IntUnaryOperator times1000 = (int x) -> { return x * factor ; }; ... factor = 1_000_000; // error: local variable used in lambda must be final or effectively final ...
}
Dieses Binden von Namen in einem Lambda-Ausdruck
an lokale Variablen, die außerhalb des Lambda-Ausdrucks definiert sind,
ähnelt dem Binding, das auch in lokalen und anonymen Klassen erlaubt ist.
Lokale und anonyme Klassen hatten schon immer Zugriff auf
final
-Variablen
des umgebenden Kontextes. In Java 8 hat man übrigens die Regeln gelockert.
Analog zu den Lambda-Ausdrücken haben in Java 8 auch die lokalen und anonymen
Klassen Zugriff auf alle "effectively final"-Variablen des umgebenden Kontextes.
Eigentlich ist alles so wie vorher, nur muss man das
final
nicht mehr explizit hinschreiben; der Compiler ergänzt es einfach, sobald
eine Variable in einer lokalen oder anonymen Klasse (oder in einem Lambda-Ausdruck)
verwendet wird.
Lambda-Ausdrücke haben außerdem Zugriff auf Felder der Klasse, in
der sie definiert sind. Hier ist ein Beispiel:
class Test { private int factor = 1000; public void test() { IntUnaryOperator times1000 = x -> x * factor ; Arrays.stream(new int[]{1, 2, 3, 4, 5}).map(times1000).forEach(System.out::println); factor = 1_000_000; // fine }
}
Dieses Mal ist
factor
keine lokale Variable in der Methode, in der der Lambda-Ausdruck vorkommt,
sondern
factor
ist ein Feld der Klasse,
in der der Lambda-Ausdruck definiert ist. Bei Feldern wird nicht verlangt,
dass sie
final
oder "effectively final"
sein müssen. Der Lambda-Ausdruck hat ganz normalen, uneingeschränkten
Zugriff darauf. Auch dies gilt für Lambda-Ausdrücke wie bisher für
Inner Classes.
Die Ähnlichkeit der Regeln für Inner Classes und Lambda- Ausdrücke ist nicht verwunderlich. Denn Lambda-Ausdrücke ähneln anonymen Klassen, die Interfaces mit genau einer abstrakten Methoden implementieren. Verglichen mit anonymen Klassen verzichten die Lambda-Ausdrücke dabei auf jeglichen Syntax-Overhead. Dafür muss der Compiler bei ihnen deutlich mehr Arbeit leisten und, wie weiter oben beschrieben, die fehlende Information aus dem Kontext deduzieren. Methoden- und Konstruktor-Referenzen
Neben den Lambda-Ausdrücken gibt es die
Methoden- und Konstruktor-Referenzen, die von der Syntax her noch kompakter
als die Lambda-Ausdrücke sind. Wenn man in einem Lambda-Body ohnehin
nichts weiter tut, als eine bestimmte Methode aufzurufen, dann kann man
den Lambda-Ausdruck häufig durch eine Methoden-Referenz ersetzen. Das
lässt sich an unserem
forEach
-Beispiel
von oben demonstrieren. Hier ist noch einmal das Original-Beispiel:
List<Integer> numbers = new ArrayList<>(); ... populate list ...
numbers.forEach(
i ->
System.out.println(i)
);
Anstelle des Lambda-Ausdruck kann man eine
Methoden-Referenz verwenden. Dann sieht es so aus:
List<Integer> numbers = new ArrayList<>(); ... populate list ...
numbers.forEach(
System.out
::
println
);
Alles bisher über Lambda-Ausdrücke Gesagte,
gilt auch für Methoden-Referenzen: sie dürfen nur in einem Kontext
vorkommen, in dem der Compiler eine Typ-Deduktion machen und einen SAM
Type für die Methoden-Referenz bestimmen kann. Der Deduktionsprozess
ist ähnlich, lediglich mit dem Unterschied, dass der Compiler für eine
Methoden-Referenz noch mehr Informationen deduzieren muss. Beispielsweise
fehlt bei einer Methoden-Referenz nicht nur der Typ der Parameter, sondern
auch jegliche Information über die Anzahl der Parameter.
Syntaktisch betrachtet besteht eine Methoden-Referenz
aus einem
Receiver
(das ist der Teil vor dem "
::
"-Symbol)
und einem Methodennamen (das ist der Teil nach dem "
::
"-Symbol).
Der Receiver kann - wie im obigen Beispiel - ein Objekt sein; es kann aber
auch ein Typ sein. Der Methodenname ist entweder der Name einer existierenden
Methode oder "
new
"; mit "
new
"
werden Kostruktoren referenziert. Sehen wir uns einige Beispiele an.
String
Builder
::new
ist eine Konstruktor-Referenz. Der Receiver ist in diesem Falle kein
Objekt, sondern ein Typ, nämlich die Klasse
String
Builder
.
Offensichtlich wird ein Konstruktor der
String
Builder
-Klasse
referenziert. Die
String
Builder
-Klasse
hat aber eine ganze Reihe von überladenen Konstruktoren. Welcher der
Konstruktoren mit
String
Builder
::new
gemeint ist, hängt vom Kontext ab, in dem die Konstruktor-Referenz auftaucht.
Hier ist ein Beispiel für einen Kontext, in dem die Konstruktor-Referenz
String
Builder
::new
vorkommt:
ThreadLocal<StringBuilder> localTextBuffer = ThreadLocal.withInitial(
StringBuilder::new
);
Die Method
withInital
-Methode
der Klasse
ThreadLocal
sieht so aus:
public static <T> ThreadLocal<T> withInitial(Supplier<? extends T> supplier) { return new SuppliedThreadLocal<>(supplier);
}
Der verwendete SAM-Typ
Supplier
sieht so aus:
@FunctionalInterface public interface Supplier<T> { T get();
}
Der Compiler deduziert aus diesem Kontext,
dass die Konstruktor-Referenz
String
Builder
::new
vom Typ
Supplier<StringBuilder>
sein muss, d.h. eine Funktion, die keine Argumente nimmt und einen
StringBuilder
zurück gibt. Es ist also in diesem Kontext der No-Argument-Konstruktor
der
String
Builder
-Klasse
gemeint.
Hier ist ein anderer Kontext, in dem die
Konstruktor-Referenz
String
Builder
::new
vorkommt:
char[] suffix = new char[] {'.','t','x','t'}; Arrays.stream(new String[] {"readme", "releasenotes"}) .map( StringBuilder::new ) .map(s->s.append(suffix))
.forEach(System.out::println);
Hier taucht die Konstruktor-Referenz als
Argument der
map
-Methode eines
Stream<String>
vor. Die betreffende
map
-Methode sieht
so aus:
public interface Stream<T> <R> Stream<R> map(Function<? super T, ? extends R> mapper);
}
Der verwendete SAM-Typ
Function
sieht so aus:
@FunctionalInterface public interface Function<T, R> { R apply(T t);
}
In diesem Kontext deduziert der Compiler,
dass die Konstruktor-Referenz
String
Builder
::new
vom Typ
Function<String,StringBuilder>
sein muss, also eine Funktion, die einen
String
als Argument nimmt und einen
StringBuilder
zurück gibt. Es ist also in diesem Kontext der Konstruktor der
String
Builder
-Klasse
gemeint, der einen
String
als Argument
akzeptiert.
Wie man sieht, sind Methoden- und Konstruktor-Referenzen
sehr flexibel, weil mit einem einzigen syntaktischen Gebilde wie
String
Builder
::new
eine
ganze Reihe von Methoden bzw. Konstruktoren bezeichnet werden und der Compiler
den richtigen von allein herausfindet.
In den obigen Beispielen haben wir Konstruktor-Referenzen
gesehen. Der Receiver ist dabei immer ein Typ. Bei Methoden-Referenzen
ist als Receiver neben einem Typ alternativ auch ein Objekt erlaubt.
Das sieht man am Beispiel von
System.out::println
.
Wir haben diese Methoden-Referenzen mehrfach als Argument der
forEach
-Methode
benutzt. Zum Beispiel hier:
char[] suffix = new char[] {'.','t','x','t'}; Arrays.stream(new String[] {"readme", "releasenotes"}) .map(StringBuilder::new) .map(s->s.append(suffix))
.forEach(
System.out::println
);
Die betreffende
forEach
-Methode
sieht so aus:
public interface Stream<T> void forEach(Consumer<? super T> action);
}
Der verwendete SAM-Typ
Function
sieht so aus:
@FunctionalInterface public interface Consumer<T> { void accept(T t);
}
In diesem Kontext muss die Methoden-Referenz
System.out::println
vom Typ
Consumer<? super StringBuilder>
sein, also eine Methode, die einen
StringBuilder
oder einen Supertyp von
StringBuilder
als Argument nimmt und nichts zurückgibt. Nun ist das Objekt
System.out
vom Typ
PrintStream
und die Klasse
PrintStream
hat eine passende nicht-statische
println
-Methode,
die ein
Object
(also einen Supertyp von
StringBuilder
)
als Argument nimmt. Diese
println
-Methode
ist aber nicht-statisch und benötigt daher für den Aufruf ein Objekt
vom Typ
PrintStream
, auf dem sie gerufen
wird, und das
Object
, das als Argument
übergeben wird. Eigentlich hat die
println
-Methode
die Signatur
void(PrintStream,Object)
,
d.h. sie braucht zwei Objekte für den Aufruf.
Wenn man nun als Receiver für die
println
-Methode
nicht den Typ
PrintStream
angibt, sondern
ein
PrintStream
-Object wie z.B.
System.out
,
dann ist das erste Argument bereits versorgt und die Methoden-Referenz
hat die Signatur
void(
Object)
,
d.h. sie braucht nur noch ein Objekt für den Aufruf.
Es macht also einen Unterschied, wie ich
eine Methoden-Referenz hinschreibe. Die Referenz
PrintStream::println
hat die Signatur
void(PrintStream,Object)
mit zwei Argumenten; die Referenz
System.out
::println
hat die Signatur
void(
Object)
mit nur einem Argument.
Die Verwendung von Objekten als Receiver in einer Methoden-Referenz ist nur für nicht-statische Methoden möglich, denn statische Methoden kann man über den Typ aufrufen; sie brauchen kein Objekt, auf dem sie aufgerufen werden. Zusammenfassung und Ausblick
Wir haben uns in diesem Beitrag die Lambda-Ausdrücke
und Methoden-/Konstruktor-Referenzen näher angesehen. Wir haben die
Syntax-Varianten betrachtet, die automatische Typdeduktion, die besondere
Bedeutung der Functional Interface Types (aka SAM Types) und den Zugriff
auf Variablen des umgebenden Kontextes aus einem Lambda-Body heraus.
Damit hat man alle Mittel in der Hand, um Lambda-Ausdrücke und Methoden-/Konstruktor-Referenzen
benutzen zu können.
Im nächsten Beitrag sehen wir uns weitere
Sprachneuerung an, die mit Java 8 freigegeben werden: die Default-Methoden.
Interfaces dürfen in Java 8 nicht nur abstrakte Methoden haben, sondern
auch Methoden mit einer Implementierung. Damit sind Interfaces keine
reinen Abstraktionen mehr und wir sehen uns an, wie das geht und was es
bedeutet.
Literaturverweise
Die gesamte Serie über Java 8:
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
© Copyright 1995-2018 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/71.Java8.Lambdas/71.Java8.Lambdas.html> last update: 26 Oct 2018 |