Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | Twitter | Lanyrd | Linkedin
 
HOME 

  OVERVIEW

  BY TOPIC
    JAVA
    C++

  BY COLUMN
    EFFECTIVE JAVA
    EFFECTIVE STDLIB

  BY MAGAZINE
    JAVA MAGAZIN
    JAVA SPEKTRUM
    JAVA WORLD
    JAVA SOLUTIONS
    JAVA PRO
    C++ REPORT
    CUJ
    OTHER
 

GENERICS 
LAMBDAS 
IOSTREAMS 
ABOUT 
CONTACT 
Effective Java - Java 8 - Lambda Expressions & Method References

Effective Java - Java 8 - Lambda Expressions & Method References  
Java 8
Lambda-Ausdrücke und Methoden-Referenzen
 

Java Magazin, November 2013
Klaus Kreft & Angelika Langer

Dies ist die Überarbeitung eines Manuskripts für einen Artikel, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im Java Magazin erschienen ist.  Die übrigen Artikel dieser Serie sind ebenfalls verfügbar ( click here ).

 

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

LambdaParameters:
  Identifier
  '(' ParameterList ')'

LambdaBody:
  Expression
  Block
 
 

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:

 
 
 
(int x)       -> x+1 Parameterliste mit einem einzigen Parameter mit expliziter Typangabe .
int x        -> x+1 Falsch: Wenn man den Parametertyp angibt, muss man die runden Klammern verwenden.
(x)           -> x+1  Parameterliste  mit einem einzigen Parameter  ohne explizite Typangabe ; der Compiler deduziert den fehlenden Parametertyp aus dem Kontext.  Das sehen wir uns später noch genauer an.
x            -> x+1 Parameterliste mit einem einzigen Parameter ohne explizite Typangabe.  I n diesem Fall darf man die runden Klammern weglassen.
(int x,int y) -> x+y Parameterliste mit  zwei Parameter n mit expliziter Typangabe .
int x,int y  -> x+y Falsch: Wenn man den Parametertyp angibt, muss man die runden Klammern verwenden.
(x,y)         -> x+y Parameterliste mit  zwei Parameter n ohne explizite Typangabe.  
x,y          -> x+y Falsch: Bei mehr als einem Parameter muss die runde Klammer verwendet werden. 
(x, int  y)     -> x+y Falsch: Man darf Parameter mit und ohne Typangabe nicht mischen. Entweder alle haben eine explizite Typangabe oder keiner.
()            -> 42 Die Parameterliste darf leer sein.

Lambda-Body

Der Rumpf ist entweder ein einzelner Ausdruck oder eine Liste von Anweisungen in geschweiften Klammern.  Hier ein paar Beispiele:

 
 
 
() -> System.gc() Body bestehend aus einem einzigen Ausdruck.
(String[] args) 

-> (args != null) ? args.length : 0

Der  ?: -Operator ergibt auch einen einzigen Ausdruck.
(String[] args) 

-> { if(args != null) 

         return  args.length ;

      else 

         return  0 ;

     }

Body bestehend aus  einer  if -Anweisung .   Hier werden geschweifte Klammern gebraucht , weil es  eine  Anweisung und kein Ausdruck ist.
(int x) ->  x+1 Body bestehend aus einem einzigen Ausdruck.
(int x) -> return x+1 Falsch: Mit  return fängt eine Anweisung an und kein Ausdruck; die  return -Anweisung muss mit einem Semikolon enden und gehört in geschweifte Klammen.
(int x) ->  return x+1 ; } So ist es richtig.

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

/PAR/ Brian Goetz
Accelerate sorting and searching with the ParallelArray classes in Java 7
URL: http://www.ibm.com/developerworks/java/library/j-jtp03048/index.html
/CLO/ Klaus Kreft, Angelika Langer
Understanding the closures debate
URL: http://www.javaworld.com/javaworld/jw-06-2008/jw-06-closures.html
/LAM/ Project Lambda
URL: http://openjdk.java.net/projects/lambda/
/TUT/ Angelika Langer, Klaus Kreft
Lambda Tutorial
URL: http://www.AngelikaLanger.com/Lambdas/Lambdas.html

Die gesamte  Serie über Java 8:

/JAV8-0/ Neue Features in Java 8 - Überblick
Klaus Kreft & Angelika Langer, Java Magazin, März 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/73.Java8.Overview/73.Java8.Overview.html
/JAV8-1/ Funktionale Programmierung in Java
Klaus Kreft & Angelika Langer, Java Magazin, September 2013
URL: http://www.angelikalanger.com/Articles/EffectiveJava/70.Java8.FunctionalProg/70.Java8.FunctionalProg.html
/JAV8-2/ Lambda-Ausdrücke und Methoden-Referenzen
Klaus Kreft & Angelika Langer, Java Magazin, Dezember 2013
URL: http://www.angelikalanger.com/Articles/EffectiveJava/71.Java8.Lambdas/71.Java8.Lambdas.html
/JAV8-3/ Default-Methoden und statische Methoden in Interfaces
Klaus Kreft & Angelika Langer, Java Magazin, Februar 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/72.Java8.DefaultMethods/72.Java8.DefaultMethods.html
/JAV8-4/ Übersicht über das Stream API in Java 8
Klaus Kreft & Angelika Langer, Java Magazin, Mai 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/74.Java8.Streams-Overview/74.Java8.Streams-Overview.html
/JAV8-5/ Stream-Erzeugung und Stream-Operationen
Klaus Kreft & Angelika Langer, Java Magazin, Juli 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/75.Java8.Fundamental-Stream-Operations/75.Java8.Fundamental-Stream-Operations.html
/JAV8-6/ Stream-Kollektoren und die Stream-Operation collect()
Klaus Kreft & Angelika Langer, Java Magazin, September 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/76.Java8.Stream-Collectors/76.Java8.Stream-Collectors.html
/JAV8-7/ Stateful Lambdas - Regeln für die Seiteneffekte in Lambda-Ausdrücken, die an Stream-Operationen übergeben werden
Klaus Kreft & Angelika Langer, Java Magazin, November 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/77.Java8.Streams-and-Statefulness/77.Java8.Streams-and-Statefulness.html
/JAV8-8/ Das Date/Time API
Klaus Kreft & Angelika Langer, Java Magazin, Januar 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/78.Java8.Date-Time-API/78.Java8.Date-Time-API.html
/JAV8-9/ CompletableFuture
Klaus Kreft & Angelika Langer, Java Magazin, März 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/79.Java8.CompletableFuture/79.Java8.CompletableFuture.html
/JAV8-10/ Optional<T>
Klaus Kreft & Angelika Langer, Java Magazin, Mai 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/80.Java8.Optional-Result/80.Java8.Optional-Result.html
/JAV8-11/ Parallel Streams
Klaus Kreft & Angelika Langer, Java Magazin, Juli 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/81.Java8.Parallel-Streams/81.Java8.Parallel-Streams.html
/JAV8-12/ Das Performance-Modell der Streams
Klaus Kreft & Angelika Langer, Java Magazin, September 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/82.Java8.Performance-Model-of-Streams/82.Java8.Performance-Model-of-Streams.html
/JAV8-13/ reduce() vs. collect()
Klaus Kreft & Angelika Langer, Java Magazin, November 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/83.Java8.Reduce-vs-Collect-Stream-Operations/83.Java8.Reduce-vs-Collect-Stream-Operations.html
/JAV8-14/ User-Defined Collectors
Klaus Kreft & Angelika Langer, Java Magazin, Januar 2016
URL: http://www.angelikalanger.com/Articles/EffectiveJava/84.Java8.User-Defined-Stream-Collectors/84.Java8.User-Defined-Stream-Collectors.html
/JAV8-15/ Parallele Streams und Blockierende Funktionalität
Klaus Kreft & Angelika Langer, Java Magazin, März 2016
URL: http://www.angelikalanger.com/Articles/EffectiveJava/85.Java8.Streams-and-Blocking-Functionality/85.Java8.Streams-and-Blocking-Functionality.html
/JAV8-16/ API-Design mit Lambdas
Klaus Kreft & Angelika Langer, Java Magazin, Mai 2016
URL: http://www.angelikalanger.com/Articles/EffectiveJava/86.Java8.API-Design-With-Lambdas/86.Java8.API-Design-With-Lambdas.html
/JAV8-17/ Low-Level-Aspekte beim API Design mit Lambdas
Klaus Kreft & Angelika Langer, Java Magazin, Juli 2016
URL: http://www.angelikalanger.com/Articles/EffectiveJava/87.Java8.Programming-With-Lambdas/87.Java8.Programming-With-Lambdas.html
/JAV8-18/ Benutzer-definierte Stream-Sourcen und Spliteratoren
Klaus Kreft & Angelika Langer, Java Magazin, September 2016
URL: http://www.angelikalanger.com/Articles/EffectiveJava/88.Java8.User-Defined-Stream-Sources-And-Spliterators/88.Java8.User-Defined-Stream-Sources-And-Spliterators.html

 
 

If you are interested to hear more about this and related topics you might want to check out the following seminar:
Seminar
Lambdas & Streams - Java 8 Language Features and Stream API & Internals
3 day seminar ( open enrollment and on-site)
Java 8 - Lambdas & Stream, New Concurrency Utilities, Date/Time API
4 day seminar ( open enrollment and on-site)
Effective Java - Advanced Java Programming Idioms 
4 day seminar ( open enrollment and on-site)
 
Related Reading
Lambda & Streams Tutorial & Reference
In-Depth Coverage of all aspects of lambdas & streams
Lambdas in Java 8
Conference Presentation at JFokus 2012 (slides)
Lambdas in Java 8
Conference Presentation at JavaZone 2012 (video)
 

 
 
  © 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