AngelikaLanger.com - Effective Java - Java 8 - Lambda Expressions & Method References - Angelika Langer Training/Consulting
- ️Angelika Langer
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
HOME
| COURSES
| TALKS
| ARTICLES
| GENERICS
| LAMBDAS
| IOSTREAMS
| ABOUT
| CONTACT
|
![]() ![]() ![]() |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Effective Java - Java 8 - Lambda Expressions & Method References | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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 istDiskussionen ü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-ParameterlisteDie 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-BodyDer 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-TypenDie 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. DeduktionskontextEin 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 BindingIm 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-ReferenzenNeben 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 AusblickWir 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 |