Lambda-Ausdrücke und Methodenreferenzen

Effective Java: Let’s Lambda!

Klaus Kreft, Angelika Langer
© istockphoto.com/jamtoons

Wie bereits im letzten Beitrag „Effective Java: Funktionale Programmierung in Java“ unserer Reihe gesehen, gibt es in Java 8 neue Sprachmittel, die einen eher funktionalen Programmierstil in Java unterstützen werden. Diese neuen Sprachmittel sind die Lambda-Ausdrücke und die Methodenreferenzen. Damit wollen wir uns in diesem Beitrag näher befassen.

Diskussionen über Spracherweiterungen in Java für funktionale Programmierung hat es schon vor einigen Jahren gegeben. Seit der Freigabe von Java 5 wurde intensiv darüber nachgedacht, wie solche Erweiterungen aussehen könnten. Es gab drei konkrete Vorschläge in dieser als „Closure-Debatte“ bekannt gewordenen Anstrengung [1]. Neil Gafter, vormals als Compilerexperte 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. Als Sun Microsystems dann auch noch von Oracle übernommen wurde, verlief die Closure-Diskussion ergebnislos im Sande. Es sah zunächst so aus, als würde es in Java keine Erweiterungen für die funktionale Programmierung geben.

Im Jahr 2009 setzte sich dann die Erkenntnis durch, 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 laufen. 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 sequenziell und nur in geringem Umfang parallel arbeitet?

Nun bietet das 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 für die parallele Ausführung von Operationen auf Sequenzen von Elementen aussehen könnte [2]. 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 bzw. 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 das JDK-Collection-Framework spezifiziert und implementiert hat [3]. Ein ParallelArray gibt es in Java 8 zwar nicht; das war nur ein Prototyp, der Ideen lieferte. An seine Stelle treten so genannte Streams. Und aus dem anfänglich als Closure bezeichneten Sprachmittel sind im Laufe der Zeit Lambda-Ausdrücke sowie Methoden- und Konstruktorreferenzen entstanden. Diese Lambda-Ausdrücke bzw. Methoden-/Konstruktorreferenzen wollen wir uns im Folgenden genauer ansehen [4].

Java 8 Poster

Was eignet sich besser für einen Rundumblick auf Lambdas und Streams als ein großes Poster, das in Ihrem Büro aufgehängt werden kann? In Zusammenarbeit mit den Java-Experten Angelika Langer und Klaus Kreft haben wir den ultimativen Spickzettel für Ihre tägliche Arbeit entwickelt. Alle Infos unter: http://jaxenter.de/specials

Copyright @ 2013-2014 by Angelika Langer & Klaus Kreft. All rights reserved[ header = Seite 2: Wie sieht ein Lambda-Ausdruck aus? ]

 

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 Superinterface 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 (i -> System.out.println(i)), 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 (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:

LambdaExpression:
       LambdaParameters '->' LambdaBody
LambdaParameters:
       Identifier
       '(' ParameterList ')'
LambdaBody:
       Expression
       Block

Copyright @ 2013-2014 by Angelika Langer & Klaus Kreft. All rights reserved

[ header = Seite 3: Lambda-Parameterliste, Lambda-Body ]

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. Tabelle 1 zeigt 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
 x            -> x+1 Parameterliste mit einem einzigen Parameter ohne explizite Typangabe, in diesem Fall darf man die runden Klammern weglassen
(int x,int y) -> x+y Parameterliste mit zwei Parametern 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 Parametern 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

Tabelle 1: Beispiele für Parameterlisten

Lambda-Body

Der Rumpf ist entweder ein einzelner Ausdruck oder eine Liste von Anweisungen in geschweiften Klammern. Tabelle 2 zeigt einige 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

Tabelle 2: Lambda-Body-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.

Copyright @ 2013-2014 by Angelika Langer & Klaus Kreft. All rights reserved

[ header = Seite 4: Typdeduktion und SAM-Typen ]

Typdeduktion und SAM-Typen

Die Syntax für Lambda-Ausdrücke ist kurz und knapp. Es stellt sich die Frage: Wo nimmt der Compiler all die Information her, die wir weglassen dürfen? Wenn wir beispielsweise in der Parameterliste die Typen weglassen, dann muss der Compiler sich die Typen selbst ü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ückgibt und IOExceptions wirft. Diesen heftigen Eingriff ins Typsystem wollte man aber vermeiden. Stattdessen hat man nach einer Möglichkeit gesucht, herkömmliche Typen für die Lambda-Ausdrücke zu verwenden.

Man hat sich also überlegt, welche schon existierenden Typen in Java einem Funktionstyp am ähnlichsten sind und dabei 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 im Rahmen einer Typdeduktion 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 BiFunction-Interface in Zeile //2 hat eine apply-Methode mit der Signatur Boolean(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 Strings 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 mithilfe 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.

Copyright @ 2013-2014 by Angelika Langer & Klaus Kreft. All rights reserved

[ header = Seite 5: Deduktionskontext und Variable Binding ]

Deduktionskontext

Ein Lambda-Ausdruck kann im Sourcecode 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 Methodenaufruf
  • als Returnwert in einer return-Anweisung
  • 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 kennengelernt und gesehen, dass der Typ eines Lambda-Ausdrucks 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 lokalen Variablen des Lambda-Ausdrucks. Manchmal möchte man aber auch auf Variablen des umgebenden Kontexts 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 mithilfe der Funktion times1000 auf einen neuen int-Wert abzubilden. 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. 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 Kontexts. 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 Kontexts. 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 Methode 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.

Copyright @ 2013-2014 by Angelika Langer & Klaus Kreft. All rights reserved

[ header = Seite 6: Methoden- und Konstruktorreferenzen ]

Methoden- und Konstruktorreferenzen

Neben den Lambda-Ausdrücken gibt es die Methoden- und Konstruktorreferenzen, 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 Methodenreferenz ersetzen. Das lässt sich an unserem forEach-Beispiel von oben demonstrieren. Hier ist noch einmal das Originalbeispiel:

List<Integer> numbers = new ArrayList<>();
... populate list ...
numbers.forEach(i -> System.out.println(i));

Anstelle des Lambda-Ausdrucks kann man eine Methodenreferenz 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 Methodenreferenzen: Sie dürfen nur in einem Kontext vorkommen, in dem der Compiler eine Typdeduktion machen und einen SAM Type für die Methodenreferenz bestimmen kann. Der Deduktionsprozess ist ähnlich, lediglich mit dem Unterschied, dass der Compiler für eine Methodenreferenz noch mehr Informationen deduzieren muss. Beispielsweise fehlt bei einer Methodenreferenz nicht nur der Typ der Parameter, sondern auch jegliche Information über die Anzahl der Parameter.

Syntaktisch betrachtet besteht eine Methodenreferenz 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 Konstruktoren referenziert. Sehen wir uns einige Beispiele an.

StringBuilder::new ist eine Konstruktorreferenz. Der Receiver ist in diesem Fall kein Objekt, sondern ein Typ, nämlich die Klasse StringBuilder. Offensichtlich wird ein Konstruktor der StringBuilder-Klasse referenziert. Die StringBuilder-Klasse hat aber eine ganze Reihe von überladenen Konstruktoren. Welcher der Konstruktoren mit StringBuilder::new gemeint ist, hängt vom Kontext ab, in dem die Konstruktorreferenz auftaucht. Hier ist ein Beispiel für einen Kontext, in dem die Konstruktorreferenz StringBuilder::new vorkommt:

ThreadLocal<StringBuilder> localTextBuffer = ThreadLocal.withInitial(StringBuilder::new);

Die 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 Konstruktorreferenz StringBuilder::new vom Typ Supplier<StringBuilder> sein muss, d. h. eine Funktion, die keine Argumente nimmt und einen StringBuilder zurückgibt. Es ist also in diesem Kontext der No-Argument-Konstruktor der StringBuilder-Klasse gemeint. Hier ist ein anderer Kontext, in dem die Konstruktorreferenz StringBuilder::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 Konstruktorreferenz als Argument der map-Methode eines Stream<String> auf. 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 folgendermaßen aus:

@FunctionalInterface
public interface Function<T, R> {
  R apply(T t);
}

In diesem Kontext deduziert der Compiler, dass die Konstruktorreferenz StringBuilder::new vom Typ Function<String,StringBuilder> sein muss, also eine Funktion, die einen String als Argument nimmt und einen StringBuilder zurückgibt. Es ist also in diesem Kontext der Konstruktor der StringBuilder-Klasse gemeint, der einen String als Argument akzeptiert.

Wie man sieht, sind Methoden- und Konstruktorreferenzen sehr flexibel, weil mit einem einzigen syntaktischen Gebilde wie StringBuilder::new eine ganze Reihe von Methoden bzw. Konstruktoren bezeichnet werden und der Compiler den richtigen von allein herausfindet.

In den obigen Beispielen haben wir Konstruktorreferenzen gesehen. Der Receiver ist dabei immer ein Typ. Bei Methodenreferenzen ist als Receiver neben einem Typ alternativ auch ein Objekt erlaubt. Das sieht man am Beispiel von System.out::println. Wir haben diese Methodenreferenzen 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);
} 

Und der verwendete SAM-Typ Function so:

@FunctionalInterface
public interface Consumer<T> {
  void accept(T t); 
}

In diesem Kontext muss die Methodenreferenz 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 Methodenreferenz 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 Methodenreferenz 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 Methodenreferenz 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.

Copyright @ 2013-2014 by Angelika Langer & Klaus Kreft. All rights reserved

[ header = Seite 7: Zusammenfassung und Ausblick ]

Zusammenfassung und Ausblick

Wir haben uns in diesem Beitrag die Lambda-Ausdrücke und Methoden- bzw. Konstruktorreferenzen näher angesehen. Betrachtet haben wir die Syntaxvarianten, die automatische Typdeduktion, die besondere Bedeutung der Functional Interface Types (aka SAM Types) und den Zugriff auf Variablen des umgebenden Kontexts aus einem Lambda-Body heraus. Damit hat man alle Mittel in der Hand, um Lambda-Ausdrücke und Methoden- bzw. Konstruktorreferenzen nutzen zu können.

Im nächsten Beitrag sehen wir uns weitere Sprachneuerung an, die mit Java 8 freigegeben wurden: 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. Wir sehen uns an, was das bedeutet.

Copyright @ 2013-2014 by Angelika Langer & Klaus Kreft. All rights reserved

Geschrieben von
Klaus Kreft
Klaus Kreft
  Klaus Kreft arbeitet selbstständig als Consultant und Trainer. Kontakt: http://www.AngelikaLanger.com.
Angelika Langer
Angelika Langer
  Angelika Langer arbeitet selbstständig als Trainer mit einem eigenen Curriculum von Java- und C++-Kursen. Kontakt: http://www.AngelikaLanger.com.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: