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

Effective Java
Java 8
API-Design mit Lambdas
 

Java Magazin, Mai 2016
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 ).

 
 

Im März 2014 wurden mit Java 8 die neuen Sprachmittel der Lambda-Ausdrücke und Methodenreferenzen freigegeben.  Die Syntax dieser neuen Sprachmittel ist wahrscheinlich vielen Java-Entwicklern mittlerweile geläufig.   Jeder, der die Streams aus Java 8 verwendet oder zumindest einmal ausprobiert hat, wird unweigerlich Lambda-Ausdrücke oder Methodenreferenzen als Argumente an die Stream-Operationen übergeben haben.  Wir wollen uns in diesem Beitrag von der reinen Benutzung der neuen Sprachmittel abwenden und uns einen anderen Aspekt der Lambda-Ausdrücke ansehen, nämlich das Design von Schnittstellen, die Lambda-Ausdrücke als Argumente entgegen nehmen.  Neue Sprachmittel ermöglichen neue Ausdrucksmöglichkeiten und neue Programmiertechniken.  Wir wollen der Frage nachgehen:  was würde man mit Lambdas anders machen als ohne ?  Was lässt sich mit den neuen Sprachmitteln besser oder eleganter ausdrücken, als es früher ohne diese Sprachmittel möglich war?
 
 

API-Design mit Lambdas

Mit Lambda-Ausdrücken und Methodenreferenzen lässt sich Funktionalität ausdrücken.  Im Prinzip ist ein Lambda-Ausdruck so etwas wie eine anonyme Methode, die keinen Namen hat und zu keiner Klasse gehört, sondern einfach nur Argumente nimmt und damit etwas tut.  Hier ein Beispiel zur Illustration:

 
 

Comparator<String> lengthComparator = (s1,s2)->Integer.compare(s1.length(),s2.length());
 
 

Der gezeigte Lambda-Ausdruck auf der rechten Seite der Zuweisung implementiert das funktionale Interface  Comparator<String> und beschreibt, wie man zwei Strings ihrer Länge nach vergleicht.  (Der Begriff "funktionales Interface" wird seit Java 8 verwendet für Interfaces mit einer einzigen abstrakten Methode.  Implementierungen solcher funktionalen Interfaces können durch Lambda-Ausdrücke oder Methodenreferenzen ausgedrückt werden.) Zu beachten ist, dass die Funktionalität, die der Lambda-Ausdruck auf der rechten Seite der Zuweisung beschreibt, noch gar nicht ausgeführt worden ist.  Hier wird nichts verglichen, sondern nur spezifiziert, wie der Vergleich gemacht werden soll, wenn er denn irgendwann einmal gebraucht wird.  Die Funktionalität, die der Lambda-Ausdruck formuliert, wird erst ausgeführt, wenn die  compare() -Methode des  lengthComparator s aufgerufen wird, also zum Beispiel hier:
 
 

int result = lengthComparator.compare("abc","12345");
 
 

Jetzt wird die Funktionalität des Lambda-Ausdrucks ausgeführt, die Längen der beiden Strings werden verglichen und es kommt  -1 raus.
 
 

Die Tatsache, dass ein Lambda-Ausdruck Funktionalität nur beschreibt, aber nicht gleich ausführt, ist die entscheidende Eigenschaft, die Lambda-Ausdrücke von allen bislang in Java vorhandenen Sprachmitteln unterscheidet.  Wie hat Cay Horstmann es in seinem Buch "Java SE 8 for the Really Impatient" (/ CAY /) so treffend ausgedrückt: " The point of all lambdas is deferred execution. "  Es geht darum, Funktionalität zu beschreiben, die man jemandem gibt, der sie später ausführt. 
 
 

Genau diese Eigenschaft nutzen APIs, die Lambdas als Argumente entgegen nehmen.  Es wird getrennt zwischen dem API-Benutzer, der Funktionalität mit Hilfe von Lambdas beschreibt und an das API übergibt, und der API-Implementierung, die die Funktionalität entgegen nimmt und ausführt.  Es ist dann der API-Implementierung überlassen, wann genau und wie die Funktionalität angestoßen wird. Je nach Situation ruft die API-Implementierung die Funktionalität vielleicht mehrfach auf, oder vielleicht auch gar nicht, oder nur unter bestimmten Bedingungen, oder möglicherweise parallel in mehreren Threads oder später asynchron in einem Hintergrunds-Thread oder sonst irgendwie.  Der API-Benutzer stellt lediglich die Funktionalität zur Verfügung und die API-Implementierung kümmert sich um die Details der späteren Ausführung ("deferred execution") der Funktionalität.
 
 

Schauen wir uns den oben gezeigten Comparator noch einmal an.  Wir haben mit dem Lambda-Ausdruck beschrieben, wie zwei Strings ihrer Länge nach verglichen werden können.  Wir selbst führen diesen String-Vergleich nie aus, sondern definieren den Comparator nur, um ihn an eine Abstraktion zu übergeben, die den String-Vergleich ausführen will.  Zum Beispiel könnte man den oben gezeigten Comparator zum Sortieren eines String-Arrays verwenden. 
 
 

String[] strings = {"Hückelhoven-Ratheim", "21345", "abc", "XYZ", "Lieschen Müller"};

Arrays.sort(strings,lengthComparator);
 
 

Die  sort() -Methode ruft intern die  compare() -Methode des  lengthComparator s auf.  Als Benutzer der  sort() -Methode spezifizieren wir lediglich die Vergleichsfunktionalität als Lambda-Ausdruck und die  sort() -Methode führt die Funktionalität später mehrfach an geeigneter Stelle im Sortieralgorithmus aus.  Das ist ein Beispiel für "deferred execution".
 
 

Nun ist die  sort()- Methode keineswegs neu in Java und Comparatoren gibt es auch schon seit der Version 1.2 von Java.  Neu in Java 8 ist lediglich, dass es jetzt eine  parallelSort() -Methode gibt, die den Comparator in mehreren Threads gleichzeitig aufruft.  Neu ist auch, dass man das  Comparator -Interface nicht mehr mit einer (anonymen) Klasse implementieren muss, sondern dass man es mit einem Lambda-Ausdruck oder einer Methodenreferenz implementieren kann.  Die Idee, Funktionalität zu übergeben, ist als solche also nicht neu; sie lässt sich aber mit den neuen Sprachmitteln leichter umsetzen. 
 
 

Ganz allgemein gesprochen lassen sich mit Lambda-Ausdrücken flexiblere und andersartige Schnittstellen gestalten, die man ohne Lambda-Ausdrücke nicht gut hätte benutzen können.  Deshalb sind solche lambda-fähigen APIs früher nicht in nennenswerter Zahl entstanden.  Jetzt, mit den neuen Sprachmittel, lassen sich manche Dinge leichter umsetzen.  Deshalb findet man im JDK 8 zahlreiche neue Schnittstellen, die Lambda-Ausdrücke (d.h. Funktionalität) als Argument entgegen nehmen.  Wir wollen im Folgenden einige der Ideen anschauen mit dem Ziel, sich als Entwickler von den neuen JDK-Schnittstellen für das Design eigener lambda-fähiger Schnittstellen inspirieren zu lassen.
 
 

Wir wollen uns die unterschiedlichen API-Ideen ansehen und klassifizieren die Ansätze wie folgt:

Strategies & Policies

Suppliers & Factories

Handlers & Reactions

Pluggable Abstractions
 
 

Strategies & Policies

Das Strategy- oder Policy-Pattern (siehe / GOF /) haben wir oben schon am Beispiel des Sortierens gesehen.  Es geht darum, eine Operation wie z.B.  sort() so zu flexibilisieren, dass sie viele verschiedene Strategien verwenden kann, statt nur eine einzige fest verdrahteten Strategie zu benutzen.

 
 

Betrachten wir das Beispiel einer beliebigen Klasse und schauen uns daran an, wie man sie mit einem flexiblen, lambda-fähigen API ausstatten würde.  Die Klasse heißt  Box und ist äußerst simpel; sie enthält nur einen einzigen Wert.  Diesen enthaltenen Wert möchte man möglicherweise auf eine Eigenschaft hin untersuchen.  Die Eigenschaft könnte man starr festlegen.  Nehmen wir einmal an, wir wollen prüfen, ob die Box eine Zeichenkette mit weniger als drei Zeichen enthält.  Das würde so aussehen:
 
 

class Box<V> {

  private V value;

  ...

  public boolean holdsCharSeqShorterThanThreeChars() {

    return (value instanceof CharSequence) ? 

      ((CharSequence)value).length() < 3 : false;

  }

}
 
 

Hier ist die Eigenschaft, die abgeprüft wird, bis ins Letzte festgelegt.  Vielleicht möchte man es flexibler gestalten.  Eine traditionelle Art der Flexibilisierung wäre die Parametrisierung der Prüfmethode zum Beispiel mit der Länge, die zur Prüfung verwendet werden soll.  Also so:
 
 

class Box<V> {

  ...

  public boolean holdsCharSeqShorterThan(int  length ) {

    return (value instanceof CharSequence) ? 

      ((CharSequence)value).length() <  length : false;

  }

}
 
 

Jetzt können wir auf unterschiedliche Stringlängen abprüfen.  Aber die Prüfung ist immer noch sehr starr.  Sie funktioniert nur für Zeichenketten (d.h. Subtypen von  CharSequence ) und sie prüft eine ganz bestimmte Eigenschaft, nämlich die Länge.   Wenn man die Prüfmethode so gestalten würde, dass sie sich die zu prüfende Eigenschaft als Argument geben ließe, dann wäre die Prüfung völlig beliebig und wir hätten maximale Flexibilität erreicht.  Das könnte so aussehen:
 
 

class Box<V> {

  ...

  public boolean holdsValueWith( Predicate<? super V> property ) {

    return  property.test (value);

  }

}
 
 

Dabei ist  Predicate ein vordefiniertes funktionales Interface aus dem Package  java.util.function des JDK 8.
 
 

Jetzt kann man in der Box prüfen, was man will.  Zum Beispiel:
 
 

Box<String> box = new Box<>("10001010");

boolean hasProperty1 = box.holdsValueWith(cs -> cs.length() < 3);

boolean hasProperty2 = box.holdsValueWith(cs -> cs.charAt(0) == '1');
 
 

Box<BigInteger> bigBox = new Box<>(new BigInteger("10010101"));

boolean hasProperty3 = bigBox.holdsValueWith(b -> b.equals(BigInteger.ZERO));
 
 

Nun ist es dem Benutzer der  Box -Schnittstelle überlassen, welche Eigenschaft er prüfen möchte.  Er übergibt die Prüffunktionalität als Argument an die Prüfmethode und mit Hilfe der neuen Sprachmittel in Java 8 lässt sich die Prüffunktionalität knapp und kurz mit einem Lambda-Ausdruck formulieren.  Solche Schnittstellen hätte man früher (ohne Lambdas) auch anbieten können; das hat man teilweise auch getan, aber jetzt mit den Lambda-Ausdrücken ist die Benutzung deutlich übersichtlicher und lesbarer.
 
 

Im JDK 8 gibt es viele sowohl neue als auch alte Beispiele für Schnittstellen, die Strategien als Argumente entgegen nehmen.  Zum Beispiel:
 
 

Arrays.sort(T[],Comparator<T>)  oder  Stream.sorted(Comparator<T>)
Hier wird die Funktionalität für das Sortierkriterium als Argument übergeben.

Stream <T> .filter(Predicate <T> )
Hier wird die Funktionalität für die Eigenschaft, nach der gefiltert werden soll, als Argument übergeben.

Collection <T> .removeIf(Predicate <T> )
Hier wird die Funktionalität für die Eigenschaft der aus der Collection zu entfernenden Elemente als Argument übergeben.

Collections.min( Collection<T>, Comparator <T> )
Hier wird die Funktionalität für das Vergleichskriterium als Argument übergeben.

File.list(FilenameFilter) 
Hier wird die Funktionalität für die Eigenschaft, nach der gefiltert werden soll, als Argument übergeben.

...
 
 

Die Liste lässt sich beliebig lang fortsetzen.  Praktisch alle Situationen, in denen das Strategy-Pattern Anwendung findet, sind als Schnittstellen gemacht, an denen Funktionalität übergeben wird.  Diese Funktionalität kann der Benutzer der Schnittstelle seit Java 8 bequem als Lambda-Ausdruck oder Methodenreferenz ausdrücken.
 
 

Die Funktionalität, die an ein API übergeben wird, muss aber nicht immer eine Strategy oder Policy sein.  Es gibt eine Reihe anderer Anwendungsfälle.  Schauen wir uns als Nächstes Schnittstellen an, die Funktionalität für das Erzeugen von Objekten  - sogenannte Suppliers oder Factories - entgegen nehmen.

Suppliers & Factories

Betrachten wir wieder unsere  Box -Klasse.  Einen Konstruktor dafür würde man normalerweise so implementieren, dass er den Initialwert für den enthaltenen Wert als Argument nimmt, d.h. so:
 
 

class Box<V> {

  private V value;

  ...

  public Box(V value) { this.value = value; }

}
 
 

Nun könnte es ja sein, dass der Initialwert in manchen Situationen nie gebraucht wird und dass die Beschaffung des Initialwerts in irgendeiner Form teuer ist, so dass man die Initialisierung vielleicht nur dann bzw. erst dann machen möchte, wenn sie wirklich erforderlich ist.  Mit anderen Worten, es soll eine Lazy Initialization gemacht werden.  Man könnte es dadurch erreichen, dass sich der Konstruktor vom Benutzer nicht den Initialwert als Argument geben lässt, sondern nur die Funktionalität, um den Initialwert zu erzeugen.  Dann kann die  Box -Klasse selbst entscheiden, ob und wann sie den Initialwert erzeugen will.  Das könnte so aussehen:
 
 

class Box<V> {

  private V value;

  private Supplier<? extends V> valueProvider;

  ...

  public Box(Supplier<? extends V> valueProvider) {

    this.valueProvider = valueProvider;

  }

  public V getValue() {

    if (value == null) value = valueProvider.get();

    return value;

  }

}
 
 

Dabei ist  Supplier ein vordefiniertes funktionales Interface aus dem Package  java.util.function des JDK 8.
 
 

Jetzt sagt der Benutzer nicht mehr:
 
 

Box<BigInteger> bigBox = new Box<>(new BigInteger("10010101"));
 
 

sondern:
 
 

Box<BigInteger> bigBox = new Box<>( () ->  new BigInteger("10010101"));
 
 

Syntaktisch und von der Lesbarkeit her ist der Unterschied für den Benutzer minimal. Von der Performance her kann die verzögerte Initialisierung unter Umständen jedoch einen großen Unterschied machen.  Dieses Beispiel der verzögerten Initialisierung illustriert sehr schön das Prinzip der "deferred execution".  Die Funktionalität für das Erzeugen des Initialwerts wird nicht sofort verwendet, sondern zunächst einmal nur abgelegt und erst später, wenn der Initialwert wirklich gebraucht wird, ausgeführt.
 
 

Auch für diese Art von Schnittstellen, die Factories oder Suppliers entgegen nehmen, gibt es zahlreiche alte und neue Beispiele im JDK 8:
 
 

ThreadPoolExecutor.setThreadFactory(ThreadFactory factory )
Hier wird die Funktionalität für das Erzeugen eines neuen Pool-Threads als Argument übergeben.  Die Funktionalität wird aber nur ausgeführt, wenn der Thread-Pool zusätzliche Worker-Threads benötigt.

Map<K,V>.computeIfAbsent(K key, Function<K,V> mapper)
Hier wird die Funktionalität als Argument übergeben, die zu einem Schlüssel den assoziierten Wert erzeugen kann.  Die Funktionalität wird aber nur ausgeführt, wenn zu dem Schlüssel noch kein assoziierter Wert in der Map vorhanden ist.

ThreadLocal .withInitial(Supplier<S> supplier)
Hier wird die Funktionalität für das Erzeugen des Initialwerts als Argument übergeben.  Die Funktionalität wird aber erst ausgeführt, wenn ein neuer Thread auf das  ThreadLocal zugreift.

Collectors.toCollection(Supplier<C> collectionFactory) 
Hier wird die Funktionalität für das Erzeugen der  Collection , in die der  Collector die  Stream -Elemente ablegen soll, als Argument übergeben.  Die Funktionalität wird bei parallelen Streams mehrfach ausgeführt, nämlich pro parallelem Worker-Thread einmal.

Optional <T> .orElse Get (Supplier< T value Supplier)
Hier wird die Funktionalität für das Erzeugen des Ersatzwerts als Argument übergeben, der bei einem leeren  Optional anstelle des fehlenden Werts verwendet werden soll.  Die Funktionalität wird nur ausgeführt, wenn  das  Optional leer ist.

...
 
 

Auch hier ließe sich die Liste beliebig fortsetzen.  Allen Beispielen ist gemeinsam, dass sie Funktionalität für das Erzeugen von Objekten entgegen nehmen, die sie aber erst später und/oder unter gewissen Bedingungen anstoßen.  Der Benutzer übergibt nicht mehr den Wert, der ja möglicherweise gar nicht gebraucht wird, sondern nur noch die Funktionalität für die Erzeugung des Werts.
 
 

Besonders schön sieht man das Prinzip der "deferred execution" am Beispiel der Klasse  Optional .  Ein  Optional wird als optionales Ergebnis von Stream-Operationen wie  min() und  max() zurück geliefert.  Wenn der Stream leer ist, dann ist auch das  Optional leer; andernfalls enthält es das kleinste bzw. größte Stream-Element.  Wenn man von einem leeren  Optional mit  get() den nicht vorhandenen Wert abholen will, dann wird eine NoSuchElementException - Exception ausgelöst.  Wenn man anstelle der Exception einen Ersatzwert haben möchte, dann kann man die Methoden  orElse(T other) und  orEls eGet(Supplier<? extend T> other ) verwenden. Beide Methoden sorgen dafür, dass ein leeres  Optional einen Ersatzwert liefert, statt eine Exception zu werfen.  Sie werden wie folgt verwendet:
 
 

Stream<String> emptyStream = …;

Optional<String> optionalMin = emptyStream.min(String::compareTo);

String min = optionalMin.get();                                 //  wirft NoSuchElementException

String min = optionalMin.orElse("");                            //  liefert ""

String min = optionalMin.orElseGet(()->readFrom(System.in));    //  liefert Ersatzwert durch Einlesen
 
 

Die Methode  orElse() bekommt den Ersatzwert als Aufrufargument der Methode übergeben; im Gegensatz dazu erhält die Methode  orElseGet() nur die Funktionalität für das Beschaffen des Ersatzwerts, nicht den Ersatzwert selbst.  Das hat den Vorteil, dass der Ersatzwert dann und nur dann berechnet wird, wenn er auch tatsächlich gebraucht wird.  Wenn es aufwändig ist, den Ersatzwert zu beschaffen, dann kann  orElseGet() günstiger sein.  Umgekehrt wird sicherlich die Methode  orElse() günstiger sein, wenn das Berechnen des Ersatzwerts keinen Aufwand erfordert, denn die zusätzliche Indirektion über den Supplier kostet natürlich auch Zeit.
 
 

Man sieht am Beispiel dieser beiden Methoden aus dem API der Klasse  Optional , welche zusätzlichen Möglichkeiten sich beim API-Design eröffnen: der API-Designer kann dem API-Benutzer die "deferred execution" als Alternative zu herkömmlichen Schnittstellen anbieten und damit Optimierungen erlauben.

Handlers & Reactions

Eine andere Kategorie von Funktionalität, die an Schnittstellen zum Zwecke der späteren Ausführung übergeben wird, sind sogenannte Handler, die nur unter gewissen Umständen ausgeführt werden. Ein klassisches Beispiel sind Error Handler für die Fehlerbehandlung.  Hier steht im Vordergrund, dass die übergebene Funktionalität nur ausgeführt wird, wenn ein bestimmtes Ereignis (z.B. eine Fehlersituation) eintritt.

 
 

Schauen wir es uns wieder am Beispiel unserer  Box -Klasse an.   Nehmen wir mal an, dass der enthaltene Wert eine bestimmte Eigenschaft haben muss, damit er als gültig angesehen wird.  Um die Gültigkeit zu prüfen, gibt es eine  is Legal () -Methode.   Es stellt sich die Frage, was getan werden soll, wenn der Wert als ungültig erkannt wird.  Soll dann eine Exception ausgelöst werden?  Oder soll ein gültiger Ersatzwert verwendet werden?  Oder ein ungültiger Ersatzwert, der als solcher zu erkennen ist, z.B. eine  null -Referenz?   Oft sind Implementierungen so, dass sie eine dieser Reaktionen fest verdrahtet haben, wie zum Beispiel in dieser Fassung unserer  Box -Klasse:
 
 

class Box<V> {

  private V value;

  private boolean isLegal(V v) { … }

  ...

  public void setValue(V v) {

    if (isLegal(v)) value = v;

    else            value = null;

  }

}
 
 

Wenn der neue Wert für die Box ungültig ist, dann ignorieren wir ihn und ersetzen ihn stillschweigend durch eine  null -Referenz.  Die Reaktion auf den ungültigen Wert könnte aber auch konfigurierbar sein, indem ein Handler verwendet wird, der weiß, was im Falle eines ungültigen Wertes zu tun ist.  Das könnte so aussehen:
 
 

class Box<V> {

  private V value;

  private boolean isLegal(V v) { … }

  private UnaryOperator<V> illegalValueHandler = v->{ throw new IllegalValueException(); };
 
 

  public void setIllegalValueHandler(UnaryOperator<V> handler) {

    illegalValueHandler = handler;

  }

  ...

  public void setValue(V v) {

    if (isLegal(v)) value = v;

    else            value = illegalValueHandler.apply(v);

  }

}
 
 

Die  Box -Klasse hat einen Handler für den Fall, dass ein ungültiger Wert erkannt wird.  Der Default-Handler wirft eine  IllegalValueException .  Dieser Default-Handler kann aber vom Benutzer ersetzt werden durch eine andere Fehlerbehandlungsfunktionalität, wie zum Beispiel hier:
 
 

Box<BigInteger> b = … ;

b.setIllegalValueHandler(v->BigInteger.ZERO);
 
 

Der neue Handler wirft keine Exception mehr, sondern verwendet als Ersatz für den ungültigen Wert den gültigen Wert  BigInteger.ZERO .
 
 

Ein Handler muss dabei nicht zwingend für die Fehlerbehandlung gedacht sein.  Es gibt auch andere Ereignisse, für deren Behandlung man Funktionalität bereitstellen möchte.  Ein klassisches Beispiel ist die GUI-Programmierung, wo Handler verwendet werden, die auf ein bestimmtes GUI-Event (z.B. Button-Click) reagieren.  Weitere Beispiele gibt es in der Multithread-Programmierung.  Dort werden häufig Callback-Funktionen verwendet, um asynchron produzierte Daten zu verarbeiten.  Es gibt ganz unterschiedliche Anwendungsfälle für Handler.  Hier einige alte und neue Beispiele aus dem JDK 8:
 
 

Thread.setUncaughtExceptionHandler(UncaughtExceptionHandler handler)
Hier wird die Funktionalität übergeben, die ausgeführt werden soll, wenn der Thread sein  Runnable nicht normal, sondern mit einer Exception verlassen hat.

ThreadPoolExecutor.setRejectedExecutionHandler(RejectedExecutionHander handler)
Hier wird an den Thread-Pool die Funktionalität übergeben, die ausgeführt werden soll, wenn der Pool keine Tasks mehr annehmen kann, z.B. weil er voll ist oder sich bereits im Shutdown befindet.  Der Handler kann die aktuelle Task wegwerfen, oder akzeptieren und dafür eine andere Task aus der Task-Queue wegwerfen, oder eine Exception auslösen, etc.

Map<K,V>.merge(K key, V value, BiFunction<V,V,V> remappingFunction)
Hier wird an die Map die Funktionalität übergeben, die ausgeführt werden soll, wenn zu dem Schlüssel bereits ein Wert in der Map vorhanden ist.  Die  remappingFunction kann den alten Wert behalten oder den neuen, oder beide miteinander vereinen, oder eine Exception auslösen, usw.

Button. addActionListener(ActionListener l istener )
Hier wird die Funktionalität übergeben, die ausgeführt werden soll, wenn der Button angeklickt wird.

CompletableFuture<T> .thenAccept (Consumer<? super T>  action )
Hier wird die Funktionalität zur Verarbeitung eines asynchron produzierten Ergebnisses übergeben.  Die  action wird ausgeführt, sobald das Future feststellt, dass das Ergebnis fertig ist.

Optional<T>.ifPresent(Consumer<? super T> consumer)
Hier wird die Funktionalität übergeben, die ausgeführt werden soll, wenn das Optional einen Wert enthält, d.h. wenn es nicht leer ist.

...
 
 

Auch diese Liste ließe sich fortsetzen.  Funktionalität für die Behandlung von Ausnahmesituationen ist schon immer in Java an entsprechende Schnittstellen übergeben worden.  In Java 8 ist neu, dass es eine zunehmende Zahl von Schnittstellen gibt, an denen man Reaktionen auf ein bestimmtes Ereignis oder einen bestimmten Zustand übergeben kann.   CompletableFuture und  Optional sind Beispiele dafür  Ein Future dient der Übergabe eines Ergebnisses von einem Thread, der das Ergebnis produziert hat, an einen anderen Thread, der das Ergebnis verarbeiten möchte.  Traditionell musste der empfangende Thread sich das Ergebnis aktiv besorgen, nämlich so:
 
 

Future<Result> future = ... ;

Result result = future.get();

processResult(result);
 
 

Der Thread muss aktiv mit  get() nach dem Ergebnis fragen und ggf. warten, denn die  get() -Methode blockiert, solange bis sie mit dem Ergebnis zurückkommt.  Erst danach kann das Ergebnis verarbeitet werden.  In Java 8 hingegen kann der empfangende Thread mit Hilfe von  CompletableFuture die Reaktion auf das Ergebnis spezifizieren, lange bevor das Ergebnis überhaupt fertig ist.  Das geht z.B. so:
 
 

CompletableFuture<Result> future = ... ;

future.thenAccept(result->processResult(result));
 
 

Die Verarbeitung des Ergebnisses wird als Lambda-Ausdruck übergeben und genau dann ausgeführt, wenn das Ergebnis fertig ist. Der empfangende Thread muss nicht mehr aktiv warten, bis das Ergebnis da ist.  Stattdessen spezifiziert er nur noch die Reaktion auf das Ergebnis in Form der Verarbeitungsfunktionalität und dann kann der Thread ohne zu warten sofort etwas anderes tun.  Die Verarbeitung des Ergebnisses erfolgt asynchron in dem Thread, der das Ergebnis fabriziert hat.  Wer sich genauer für CompletableFuture interessiert, kann die Details in einem unserer früheren Beiträge (siehe / KRE /) nachlesen.
 
 

Das  Optional ist ähnlich.  Wie oben schon erwähnt, wird  Optional als optionales Ergebnis von Stream-Operationen wie  min() und  max() zurück geliefert.  Wenn der Stream leer ist, dann ist auch das  Optional leer; andernfalls enthält es das kleinste bzw. größte Stream-Element.  Man kann sich aktiv nach dem Ergebnis erkundigen, z.B. so:
 
 

Optional<Result> result = stream.min();

if (result. isPresent ())

  System.out.print(result. get ());
 
 

Das  Optional hat zusätzlich eine reaktive Schnittstelle, an der der Benutzer die Reaktion auf das Vorhandensein (oder auch das Fehlen) des Ergebnisses als Funktionalität übergeben kann.  Das könnte dann so aussehen:
 
 

Optional<Result> result = stream.min();

result. ifPresent (System.out::print);
 
 

Sowohl beim Future als auch beim Optional hat der Benutzer die Auswahl zwischen Aktion und Reaktion: er kann aktiv Zustände abfragen und Ergebnisse abholen (und dabei in Kauf nehmen, dass er ggf. warten muss) oder er kann reaktiv vorgehen.  Dann formuliert er lediglich die Reaktion und überlässt das Auswerten von Zuständen und das Abholen und Verarbeiten von Ergebnissen der Schnittstelle, an der er seine vorbereitete Reaktionsfunktionalität übergeben hat.
 
 

Allen oben erwähnten Schnittstellen ist gemeinsam, dass sie Funktionalität (Handler oder Reaktionen) entgegen nehmen, die sie in bestimmten Situationen oder beim Eintreten gewisser Ereignisse anstoßen.  Das Ereignis kann eine Ausnahmesituation sein oder aber auch eine wünschenswerte "normale" Situation, die irgendwann eintritt.  In allen Fällen ermöglichen Lambda-Ausdrücke und Methodenreferenzen eine übersichtliche Verwendung solcher Schnittstellen.

Pluggable Abstractions

Da sich mit Lambda-Ausdrücken und Methodenreferenzen Funktionalität gut lesbar ausdrücken lässt, kann man sogar so weit gehen, dass man Klassen ganz anders implementiert.  Wenn zum Beispiel ein Interface implementiert werden soll, dann hat man es traditionell in Java so gemacht, dass man eine oder mehrere Klassen schreibt, die von dem Interface abgeleitet sind und die abstrakten Interface-Methoden implementieren.  Wenn man unterschiedliche Implementierungen haben will, dann muss man mehrere implementierende Klassen schreiben.  Zum Beispiel sieht das traditionelle Vorgehen so aus:

 
 

interface Abstraction<A,R> {

  R method1(A arg);

  R method2(A arg);

}
 
 

class Implementation1<A,R> implements Abstraction<A,R> {

  public R method1(A arg) { ... }

  public R method2(A arg) { ... }

}
 
 

class Implementation2<A,R> implements Abstraction<A,R> {

  public R method1(A arg) { ... }

  public R method2(A arg) { ... }

}
 
 

Mit Hilfe der neuen Sprachmittel könnte man unterschiedliche Implementierungen des Interfaces mit nur einer einzigen implementierenden Klasse bauen, indem die Implementierungen der abstrakten Methoden einfach durch Komposition zusammengestellt werden.  Ein solcher Kompositionsansatz könnte beispielsweise so aussehen:
 
 

class Implementation<A,R> implements Abstraction<A,R> {

  public static <S,T> Implementation<S,T>

   of (Function<S,T> f1, Function<S,T> f2) {

    return new Implementation<S,T>(f1,f2);

  }

  private final Function<A,R> f1, f2;

  private Implementation(Function<A,R> f1, Function<A,R> f2) {

    this.f1=f1; this.f2=f2;

  }

  public R method1(A arg) { return f1.apply(arg); }

  public R method2(A arg) { return f2.apply(arg); }
 
 

}
 
 

Die Klasse  Implementation hat eine Factory-Methode  of() , der zwei Funktionen übergeben werden.  Diese beiden Funktionen liefern die Implementierungen der beiden abstrakten Interface-Methoden.  Mit diesem Ansatz können nun beliebige Implementierungen des Interfaces per Komposition gebaut werden, ohne dass weitere implementierende Klassen gebraucht werden.
 
 

Implementation<String,Long> impl1

Implementation.of (a->{…  functionality for  method m1() …},

                    b->{…  functionality for method m2() …});

Long r1 = impl1.method1("abc");

Long r2 = impl1.method2("xyz");
 
 

Implementation<String,Long> impl2

Implementation.of (a->{…  other  functionality for  method m1() …},

                    b->{…  other  functionality for method m2() …});

Long r3 = impl2.method1("abc");

Long r4 = impl2.method2("xyz");
 
 

Das Beispiel sieht vielleicht etwas abstrakt und esoterisch aus, aber auch für diese Vorgehensweise gibt es praktische Beispiele im JDK.  Ein solches Beispiel ist das Interface  Collector aus dem Package  java.util.stream . Ein Kollektor wird gebraucht, wenn man alle Elemente eines Streams mit der Stream-Operation  collect() in einem Zielcontainer aufsammeln will.  Die Stream-Operation  collect() nimmt als Argument eine Implementierung des  Collector -Interfaces.  Das Interface hat fünf abstrakte Methoden und natürlich könnte man hingehen und einen Kollektor bauen, indem man vom Collector -Interface ableitet und die fünf abstrakten Methoden überschreibt.  Als Alternative bietet das  Collector -Interface eine Factory-Methode  of() an. Dieser Factory-Methode werden die Implementierungen der fünf abstrakten Interface-Methoden übergeben.  Hier zur Illustration ein Auszug aus dem  Collector -Interface:
 
 

public interface Collector<T,A,R> {

  Supplier<A>          supplier();

  BiConsumer<A,T>      accumulator();

  BinaryOperator<A>    combiner();

  Function<A,R>        finisher();

  Set<Characteristics> characteristics();

  …

  public static<T,A,R> Collector<T,A,R>

  of(Supplier<A>        supplier,

     BiConsumer<A,T>    accumulator,

     BinaryOperator<A>  combiner,

     Function<A,R>      finisher,

     Characteristics... characteristics) {

    return new Collectors.CollectorImpl<>

           (supplier, accumulator, combiner, finisher, cs);

  }

}
 
 

Das  Collector -Interface hat fünf abstrakte Methoden und die  of() -Methode nimmt genau fünf funktionale Argumente, nämlich exakt die fünf Funktionalitäten für die Implementierung der fünf abstrakten Methoden.  Hier ist ein Beispiel für einen Kollektor, der mit der  of() -Methode erzeugt wird; er fügt einen Stream von Strings zu einem einzigen String zusammen:
 
 

String concatenate(Stream<String> strings) {

  return strings.collect(Collector.of(StringBuilder::new,

                                      StringBuilder::append,

                                      StringBuilder::append,

                                      StringBuilder::toString)

                        );

}
 
 

Man kann auf einen Blick sehen, dass der Kollektor

- einen  StringBuilder verwendet,

- die Strings mit  append() aneinanderfügt und

- den  StringBuilder am Ende mit  toString() in einen  String verwandelt. 

Eine herkömmliche Implementierung des Kollektors durch Ableiten vom  Collector -Interface und Überschreiben der abstrakten Interface-Methoden wäre sicherlich weit weniger übersichtlich geworden.
 
 

Das Beispiel illustriert, dass die neuen Sprachmittel der Lambda-Ausdrücke und Methodenreferenzen in der Tat zu neuen Implementierungstechniken und alternativen API-Designs einladen, die zur Verständlichkeit und Übersichtlichkeit des Java-Source-Codes beitragen können.

Zusammenfassung

Mit Lambda-Ausdrücken und Methodenreferenzen lässt sich Funktionalität lesbar und übersichtlich formulieren.  Solche neuen Sprachmittel erlauben neue Ausdrucksmöglichkeiten.  Entsprechend bietet es sich an, Schnittstellen zu entwerfen, die Funktionalität in Form von Lambda-Ausdrücken und Methodenreferenzen entgegen nehmen.  Der wesentliche Unterschied zu herkömmlichen Schnittstellen besteht darin, dass die Funktionalität erst einmal nur formuliert und übergeben, aber nicht sofort ausgeführt wird.  Vielmehr entscheidet der Empfänger der Funktionalität, wann und wie die Funktionalität angestoßen wird ("deferred execution"). 

 
 

Zur Ausführung kommt die Funktionalität

- wenn ein bestimmtes Ereignis eintritt (z.B. Fehlerbehandlung oder Ergebnisverarbeitung)

- wenn eine bestimmte Situation vorliegt (z.B. Map.ifAbsent())

- mehrfach (z.B. Stream.forEach())

- gar nicht bzw. nur wenn erforderlich (Optional.ifPresent())

- synchron (z.B Arrays.sort())

- asynchron in einem anderen Thread (z.B. Executor.execute())

- parallel in mehreren anderen Threads (z.B. Arrays.parallelSort())

- usw.
 
 

Wir haben einige Beispiele für solche Schnittstellen aus dem JDK gesehen und es ist zu erwarten, dass in modernem Java ab Version 8 noch viele weitere lambda-fähige APIs entstehen werden.
 
 

Literaturverweise

/CAY/   Java SE 8 for the Really Impatient - Cay Horstmann
Addison-Wesley 2014
Chapter 3, Page 48
/GOF/ Design Patterns - Erich Gamma, Richard Helm, Ralph E. Johnson, John Vlissides
Addison-Wesley 1994
/KRE/  CompletableFuture
Klaus Kreft & Angelika Langer, Java Magazin, März 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/79.Java8.CompletableFuture/79.Java8.CompletableFuture.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/86.Java8.API-Design-With-Lambdas/86.Java8.API-Design-With-Lambdas.html  last update: 26 Oct 2018