| 
 | ||||||||||||||||||||||||||||||||
| HOME
| COURSES 
| TALKS
| ARTICLES 
| GENERICS
| LAMBDAS
| IOSTREAMS
| ABOUT
| CONTACT
|  |  |   | ||||||||||||||||||||||||||||||||
| 
 | Effective Java - Java 8 - Functional Programming in Java   | |||||||||||||||||||||||||||||||
| 
   Im April 2013 hat Oracle die Java Community darüber informiert, dass der Java-8-Releasetermin von September 2013 auf März 2014 verschoben wird. Diese Verschiebung gibt uns Zeit, uns auf die neuen Sprachmittel und die neuen JDK-Abstraktionen von Java 8 in Ruhe vorzubereiten. Mit diesem Beitrag beginnen wir deshalb eine Serie von Beiträgen zu den Neuerungen in Java 8. Unter anderem wird es neue Sprachelemente geben, die einen eher funktionalen Programmierstil in Java unterstützen werden. Dabei geht es um die sogenannten Lambda-Ausdrücke. Ehe wir uns jedoch die Lambda-Ausdrücke in einem der nachfolgenden Beiträge genauer ansehen, wollen wir uns zunächst damit befassen, was funktionale Programmierung generell ausmacht, wo man funktionale Ansätze praktisch anwenden kann und wie funktionale Programmierung konkret in Java aussehen wird. Objekt-Orientierte vs. Funktionale Programmierung
In objekt-orientierten Programmiersprachen
(wie zum Beispiel Java) spielen Objekte eine wesentliche Rolle.  Als Java-Entwickler
beschreiben wir, wie Objekte aussehen, wenn wir eine Klasse definieren,
d.h. wir legen fest, welche Daten den Zustand eines Objekts beschreiben
und welche Methoden die Fähigkeiten eines Objekts ausmachen.  Wir erzeugen
Objekte, wenn wir von den Klassen Instanzen bilden.  Wir verändern Objekte,
z.B. wenn wir die Felder ändern oder Methoden aufrufen, die dies tun. 
Wir reichen Objekte herum, z.B. wenn wir sie als Argumente an Methoden
übergeben.  Mit Objekten sind wir als Java-Entwickler bestens vertraut. 
In funktionalen Sprachen (wie zum Beispiel
Erlang, Haskell, ...) stehen nicht Objekte, sondern Funktionen im Vordergrund. 
Funktionen ähneln Methoden; sie repräsentieren ausführbare Funktionalität. 
Sowohl Methoden als auch Funktionen werden aufgerufen und ausgeführt. 
Funktionen in funktionalen Sprachen werden aber darüber hinaus herumgereicht. 
Man übergibt sie beispielsweise als Argumente an Operationen; diese Operationen
können dann die übergebenen Funktionen in einem bestimmten Kontext aufrufen. 
Funktionen können auch als Returnwert einer Operation zurückgegeben werden. 
Das heißt, in funktionalen Sprachen werden Funktionen herumgereicht, wie
Objekte in objekt-orientierten Sprachen.  Dieses Prinzip des Herumreichens
von Funktionen wird auch als "code-as-data" bezeichnet. 
 
Funktionen werden aber nicht nur übergeben
und aufgerufen, sondern auch kombiniert und verkettet oder manipuliert
und verändert.  Es gibt z.B. das sogenannte 
Currying (
benannt
nach Haskell Brooks Curry), bei dem aus einer Funktion mit mehreren Argumenten
durch Argument-Binding eine Funktion mit einem Argument gemacht wird. 
In einer reinen funktionalen Sprache ("pure functional language") haben
die Funktionen nicht einmal Seiteneffekte.  Das heißt insbesondere, dass
Funktionen keine Daten verändern, sondern bestenfalls neue Daten erzeugen.
 Soviel zur Theorie. Was fängt man damit in der Praxis an? Kann man funktionale Prinzipien in Java überhaupt gebrauchen? Zur Illustration wollen wir uns ein Idiom ansehen, bei dem Funktionen eine wesentliche Rolle spielen und das auch in Java recht nützlich sein kann. Es geht um das Execute-Around-Method-Pattern . Das Execute-Around-Method-Pattern
Bei dem Execute-Around-Method-Pattern (siehe
/
EAM1
/, /
EAM2
/) geht es darum,
strukturell ähnlichen Code so zu zerlegen, dass die immer wiederkehrende,
identische Struktur heraus gelöst und in eine Hilfsmethode ausgelagert
wird.  Der Teil, der in dieser Struktur variiert, wird an die Hilfsmethode
übergeben und in dieser Hilfsmethode eingebettet in die Struktur an der
richtigen Stelle aufgerufen.  Beispiele dafür solche wiederkehrenden
Strukturen gibt es viele: Verwendung von Ressourcen : Wenn eine Ressource verwendet wird, dann ergibt sich oft eine wiederkehrende Struktur, nämlich acquire resource use resource 
release resource
 
Das Anfordern und Freigeben der Ressource
ist oft identisch, aber die Benutzung dazwischen variiert. Ein Beispiel
für solche Ressourcen sind die expliziten Locks wie zum Beispiel das 
ReentrantLock
. 
Hier ist die immer gleiche Struktur, die sich bei der Benutzung von expliziten
Locks ergibt:
 lock.lock(); try { ... critical region ... } finally { lock.unlock(); 
              }
 
Das Anfordern und Freigeben des Locks ist immer gleich, nur die Anweisungen
dazwischen variieren. 
 
Exception Handling
: Wenn Operationen aus einem bestimmten Framework
verwendet werden, dann werfen sie oft die gleichen Exceptions, die immer
gleich behandelt werden.
 try { ... invoke operations ... } catch (ExceptionType_1 e1) { ... } catch (ExceptionType_2 e2) { ... } 
                catch (ExceptionType_3
e3) { ... }
 
Die 
catch
-Klauseln sind immer gleich,
aber die im 
try
-Block aufgerufenen Operationen
sind unterschiedlich.
 
Iterierung
: Es wird ein Iterator angefordert
und aufs jeweils nächste Element in einer Sequenz weitergeschaltet, bis
das letzte Element erreicht ist.
 Iterator iter = seq.iterator(); while (iter.hasNext()) { Object elem = iter.next(); ... use element ... 
              }
 
Die Handhabung des Iterators ist immer gleich; lediglich die Verwendung
des jeweiligen Elements variiert.
 
Beim Execute-Around-Method-Pattern wird der
gemeinsame, wiederkehrende Teil in eine Hilfsmethode ausgelagert. Der veränderliche
Teil wird als Argument an die Hilfsmethode übergeben. Wir wollen es einmal
am Beispiel der Iteration demonstrieren.
 
Wir definieren eine Hilfsmethode 
forEach
:
 public class Utilities { public static <E> void forEach(Iterable<E> seq, Consumer<E> block) { Iterator<E> iter = seq.iterator(); while (iter.hasNext()) { E elem = iter.next(); block.accept (elem); } } 
              }
 
Die Hilfsmethode 
forEach
enthält den strukturell wiederkehrenden Teil, nämlich das Anfordern des
Iterators, die Abfrage auf das Ende der Sequenz und das Weiterschalten
des Iterators auf das jeweils nächste Element.  Die Verwendung des Elements
ist der sich unterscheidende Teil.  Er wird von Außen an die Hilfsmethode
übergeben und in der Hilfsmethode an entsprechender Stelle aufgerufen. 
Die Hilfsmethode 
forEach
 bekommt deshalb
als Argumente die Sequenz der Elemente, auf der man iterieren will, und
die Funktionalität, die während der Iterierung auf jedes Element in der
Sequenz angewandt werden soll. 
 
Für die Beschreibung der Funktionalität,
die auf jedes Element angewandt wird, definieren wir ein Interface 
Consumer
:
 public interface Consumer<T> { void accept(T t); 
              }
 
Dieses Interface gibt es tatsächlich in Java 8 im Package 
java.util.function
.
 
Mit dieser Zerlegung in die Hilfsmethode
mit dem strukturell identischen Teil und das Interface mit dem variierenden
Teil brauchen wir die Iterierung nicht mehr redundant hinschreiben.  Hier
ist ein Benutzungsbeispiel.  Wir wollen alle Elemente aus einer Liste
von Zahlen ausgeben.
 
Herkömmlich sieht es so aus:
 List<Integer> numbers = new ArrayList<>(); ... populate list ... Iterator iter = numbers.iterator(); while (iter.hasNext()) { Integer elem = iter.next(); System.out.println(elem); 
              }
 
Gemäß Execute-Around-Method-Pattern sieht
es so aus:
 List<Integer> numbers = new ArrayList<>(); ... populate list ... Utilities.forEach(numbers, new Consumer<Integer>() { public void accept(Integer elem) { System.out.println(elem); } 
                               
});
 
Nun mag man sich fragen, was an der Execute-Around-Method-Version
besser sein soll als an der guten alten Iterierung per Schleife.  Mit
klassischen Java-Mitteln, so wie sie uns in Java 7 zur Verfügung stehen,
ist nichts gewonnen.  Man muss eine Implementierung des 
Consumer
-Interfaces
definieren, um den Consumer an die Hilfsmethode 
forEach
zu übergeben, und das ist selbst unter Verwendung von anonymen inneren
Klassen noch recht umständlich.
 
Genau diese syntaktische Umständlichkeit
wird in Java 8 mit den Lambda-Ausdrücken verschwinden (siehe /
LAM
/,
/
TUT
/).
 
In Java 8 mit einem Lambda-Ausdruck sieht
es viel eleganter aus:
 List<Integer> numbers = new ArrayList<>(); ... populate list ... 
              Utilities.forEach(numbers, 
e
-> System.out.println(e
)
);
 
Es geht auch noch eleganter mit Hilfe von
Methoden-Referenzen - einem weiteren neuen Sprachmittel in Java 8.
 
So sieht es dann in Java 8 mit einer Methoden-Referenz
aus:
 List<Integer> numbers = new ArrayList<>(); ... populate list ... 
              Utilities.forEach(numbers, 
System
::
println
);
 
Auf die Syntax von Lambda-Ausdrücken wie 
e
-> System.out.println(e)
 und Methoden-Referenzen wie 
System
.out
::println
wollen wir in diesem Beitrag nicht näher eingehen.  Das besprechen wir
im nächsten Artikel der Serie im Detail.  Aber auch ohne große Erläuterung
kann man intuitiv verstehen, dass der Lambda-Ausdruck so etwas Ähnliches
wie eine Funktion ist.  Er nimmt ein Argument mit Namen 
e
,
dessen Typ sich der Compiler selbst überlegen kann.  Augenscheinlich
soll es ein 
Integer
 aus der Liste sein. 
Dieses Argument 
e
 wird per 
System.out.println
-Methode
ausgegeben.  Auf diese Weise wird die 
forEach
-Methode
alle Zahlen in der Liste 
numbers
 mit 
println
auf 
System.out
 ausgeben.
 
Die Methoden-Referenz ist auch selbsterklärend.  
System
.out
::println
ist die 
println
-Methode von 
System.out
. 
In der 
forEach
-Methode sollen also alle
Elemente mit 
println
 nach 
System.out
ausgegeben werden.
 
Im JDK 8 wird es solche Hilfsmethoden wie 
forEach
geben. Sie sind dann aber nicht in irgendwelchen Utility-Klassen definiert,
sondern die Collections selbst sind erweitert worden. In Java 8 hat jede
Collection aus dem 
java.util
 Package
eine 
forEach
-Methode, und zwar erbt sie
diese von ihrem Super-Interface 
Iterable
.
  Das 
Iterable
-Interface ist für Java
8 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.
 
Um die existierenden Interfaces im JDK so
wie oben gezeigt erweitern zu können, hat man mit Java 8 die sogenannten
Default-Methoden
erfunden. Darauf werden wir in einem der Folgebeiträge genauer eingehen. 
Hier nur ganz kurz: Normalerweise kann man ein Interface nicht problemlos
erweitern.  Wenn man Methoden hinzufügt, dann müssen alle abgeleiteten
Klassen diese Methode implementieren.  Andernfalls gibt es Fehlermeldungen
bei der Compilierung.  Die Default-Methoden sind nun Methoden, die eine
Implementierung haben.  Das heißt, sie sind nicht abstrakt und müssen
von den abgeleiteten Klassen auch nicht implementiert werden.  Alle Klassen,
die keine Implementierung für die neue zusätzliche Methode haben, erben
einfach die Default-Implementierung aus dem Interface.  Auf diese Weise
kann man existierende Interfaces erweitern, ohne die abgeleiteten Klassen
ändern zu müssen.  Die 
forEach
-Methode
im 
Iterable
-Interface ist eine solche
Default-Methode.  Sie hat eine Implementierung. Sie verwendet in der Implementierung
die for-each-Schleife, die es seit Java 5 gibt und die intern einen Iterator
verwendet.  Die 
forEach
-Methode im Interface 
Iterable
entspricht der 
forEach
-Methode aus unserer 
Utilities
-Klasse,
mit dem kleinen Unterschied, dass sie das erste Argument nicht braucht,
weil sie als nicht-statische Methode der Collection ohnehin über die 
this
-Referenz
auf die Collection zugreifen kann.
 
Das Beispiel von oben sieht in Java 8 letztendlich
so aus:
 List<Integer> numbers = new ArrayList<>(); ... populate list ... 
              numbers.forEach(
System
.out
::println
);
 
Die herkömmliche Art der Iterierung mit
einem expliziten Iterator bezeichnet man im Übrigen als 
externe Iterierung
,
im Gegensatz zur 
internen Iterierung
 in der 
forEach
-Methode
einer Collection (siehe /
GOF
/).  Bei der externen Iterierung
wird der Iterator an den externen Benutzer einer Collection gegeben und
der Benutzer bestimmt, wie der den Iterator verwendet, um alle Elemente
der Sequenz zu besuchen.  Bei der internen Iterierung bestimmt die Collection
selbst, wie sie in ihrer 
forEach
-Methode
alle Elemente besucht.  Das kann sie mit einem Iterator machen, so wie
wir es im Beispiel gesehen haben.  Sie kann es aber auch ganz anders machen,
zum Beispiel parallel mit vielen Threads statt sequentiell mit nur einem
Thread.
 Genau die parallele Ausführung von Operationen wie forEach sind der wesentliche Grund dafür, dass man die Sprache um Lambda-Ausdrücke erweitert hat. Eines der Ziele in Java 8 ist die bessere Unterstützung von Parallelverarbeitung. Deshalb wird es neue Abstraktionen im JDK-Collection-Framework geben, nämlich sogenannte Streams . Diese Streams haben Operationen wie forEach (oder auch sort , filter , etc.) mit interner Iterierung, die wahlweise sequentiell oder parallel ausgeführt werden können. Die Streams und ihr umfangreiches API werden wir uns in einem der nachfolgenden Beiträge im Detail ansehen. Funktionale Programmierung in Java
In dem oben geschilderten Beispiel der
internen Iterierung bzw. des Execute-Around-Method-Patterns sieht man typische
Elemente der funktionalen Programmierung.  Beispielsweise sieht man das
"code-as-data"-Prinzip: die 
forEach
-Methode
benötigt als Argument eine Funktion, die auf alle Elemente der Collection
angewandt werden soll.  Es wird zwar streng genommen ein Objekt als Argument
übergeben, aber dieses Objekt repräsentiert Funktionalität. Das einzige,
was an dem Objekt interessant ist, ist die eine Methode, die es mitbringt
und die auf alle Elemente der Sequenz angewandt werden soll.  In diesem
Sinne ist das 
Consumer
-Argument der 
forEach
-Methode
eine Funktion. In Java 7 muss dafür umständlich eine anonyme innere Klasse
definiert werden.  In Java 8 mit den Lambda-Ausdrücken und Methoden-Referenzen
sieht die Funktionalität optisch und syntaktisch so aus, wie man sich
eine Funktion vorstellt 
 Target Typing und SAM Types
Sehen wir uns die Einbettung von Lambda-Ausdrücken
und Methoden-Referenzen ins Java-Typsystem anhand unseres Beispiels an. 
Noch einmal das Beispiel von oben:
 List<Integer> numbers = new ArrayList<>(); ... populate list ... 
              numbers.forEach(
System.out::println
);
 
Der Compiler geht prinzipiell so vor:  er
schaut sich den Kontext an, in dem ein Lambda-Ausdruck oder eine Methoden-Referenz
steht, überlegt, welcher Typ von Objekt an dieser Stelle benötigt wird,
und deduziert daraus den Typ für den Lambda-Ausdruck oder die Methoden-Referenz. 
 
In unserem Beispiel findet er die Methoden-Referenz 
System.out::println
als Argument im Aufruf der 
forEach
-Methode. 
Der Compiler schaut sich also den deklarierten Argumenttyp der 
forEach
-Methode
an.  Weil es die 
forEach
-Methode einer 
List<Integer>
ist, stellt der Compiler fest, dass für den Methodenaufruf ein Objekt
vom Typ 
Consumer<Integer>
 benötigt
wird.  
Consumer<Integer>
 ist ein
Interface mit einer einzigen abstrakten Methoden, nämlich der 
accept
-Methode. 
Nun prüft der Compiler, ob die Signatur der 
accept
-Methode
kompatibel zur Methoden-Referenz 
System.out::println
ist.  Die 
accept
-Methode von 
Consumer<Integer
>
nimmt ein Argument vom Typ 
Integer
, gibt
void zurück und wirft keine checked-Exceptions.  Das passt zu unserer
Methoden-Referenz 
System.out::println
. 
Die 
println
-Methode ist überladen und
unter all den vielen 
println
-Varianten
gibt es eine, die ein Argument vom Typ 
Integer
nimmt, 
void
zurück gibt und keine checked-Exceptions wirft.  Das heißt, die 
accept
-Methode
aus dem 
Consumer<Integer>
-Interface
hat dieselbe Signatur wie die Methoden-Referenz 
System.out::println
. 
Der Compiler schließt daraus, dass die -Referenz 
System.out::println
in
diesem Kontext vom Typ 
Consumer<Integer>
ist. 
 
Diesen Prozess der Deduktion des Typs eines
Lambda-Ausdrucks oder einer Methoden-Referenz wird als 
Target Typing
bezeichnet, weil dabei der Zieltyp (Target Type) für den Ausdruck oder
die Referenz aus dem Kontext ermittelt wird. Für Lambda-Ausdrücke funktioniert
das Target Typing ganz analog.
 
Interfaces wie 
Consumer
mit einer einzigen abstrakten Methode heißen übrigens 
Functional Interface
Types
 (oder auch 
SAM Types
, wobei SAM für Single Abstract Method
steht).  Die SAM-Typen spielen beim Target Typing eine wesentliche Rolle. 
Sie sind nämlich die einzigen Typen, die als Zieltypen in Frage kommen.
 Über diesen Trick mit den SAM-Typen und der Deduktion eines kontext-abhängigen Zieltyps konnte es vermieden werden, gravierend in das Typsystem von Java einzugreifen. Deshalb gibt es in Java - anders als in funktionalen Sprachen - keine spezielle Kategorie von Typen, mit denen man Funktionen oder Funktions-Signaturen beschreiben könnte. Seiteneffekte
Das Fehlen von echten Funktionstypen ist
aber nur eine Eigenart, die funktionale Programmierung in Java von funktionalen
Sprachen unterscheidet.  In reinen funktionalen Sprachen sind die Funktionen
stets frei von Seiteneffekten.  Insbesondere modifiziert eine reine Funktion
keine Daten, sondern produziert ein Ergebnis.  Das ist in Java natürlich
anders.  Es gibt in Java gar keine Möglichkeit, eine Funktion daran zu
hindern, Felder oder Variablen zu modifizieren.  
In unserem Beispiel haben unsere Lambda-Ausdrücke
und Methoden-Referenzen zwar nichts modifiziert, aber einen Seiteneffekt,
nämlich die Ausgabe auf 
System.out
,
haben sie dennoch produziert.  Für diese Funktionen macht es einen Unterschied,
ob sie  mehrfach aufgerufen werden oder in welche Reihenfolge sie aufgerufen
werden, denn es hat Einfluss auf die Ausgabe.  Bei einer reinen Funktion,
die keinerlei Seiteneffekte hat, wäre es völlig egal, wie oft und in
welcher Reihenfolge sie ausgeführt wird.  Eine reine Funktion wäre beispielsweise
folgender Lambda-Ausdruck:
 
              IntPredicate isEven = (int
i) -> { return i%2==0; };
 
Dabei ist 
IntPredicate
ein SAM-Typ aus dem Package 
java.util.function
mit einer einzigen abstrakte Methode, die so aussieht:  
boolean
test(int value)
.
 
Dieser Lambda-Ausdruck 
(int
i) -> { return i%2==0; } 
nimmt einen 
int
-Wert
und liefert 
true
 zurück, wenn es eine
gerade Zahl ist, und andernfalls 
false
. 
Hier wird überhaupt kein Seiteneffekt ausgelöst.  Es wird einfach nur
ein Wert genommen und ein Boolesches Ergebnis zurück geliefert.  Diese
Funktion kann aufrufen werden, so oft man will und in jeder beliebigen
Reihenfolge.  Es macht überhaupt keinen Unterschied. 
 
Hier zum Kontrast ein Lambda-Ausdruck, der
Modifikationen macht:
 List<Point> points = new ArrayList<>(); ... populate list ... 
              points.forEach(p -> { p.x
= 0; });
 
Hier werden alle Elemente der Sequenz modifiziert;
es sind 
Point
-Objekte, deren x-Koordinate
in dem Lambda-Ausdruck geändert wird.  Das ist im Sinne der funktionalen
Programmierung schlechter Stil, kann aber in Java nicht verhindert werden. 
Wenn das Argument einer Funktion eine Referenz auf ein veränderliches
Objekt ist, dann kann die Funktion Modifikationen machen.  Java hat einfach
keine Sprachmittel, um solche Modifikationen zu verhindern.
 
Solche Lambda-Ausdrücke sind u.U. problematisch. 
Wir werden in nachfolgenden Beiträgen erläutern warum.  Aber bereits
hier sollte schon klar sein, dass man mit modifizierenden Lambda-Ausdrücken
leicht Fehler machen kann.   Hier ist eine solche Fehlersituation:
 List<Point> points = new ArrayList<>(); ... populate list ... 
              points.forEach(p -> points.add(new
Point(0,p.y)));
 In dem Lambda-Ausdruck werden während der Iterierung neue Elemente in die Collection eingefügt. Das scheitert zur Laufzeit mit einer ConcurrentModificationException . Zusammenfassung und Ausblick
Java 8 wird neue Sprachmittel haben, die
in gewissem Umfang funktionale Programmierung in Java unterstützen. 
Die betreffenden Sprachmittel sind Lambda-Ausdrücke und Methoden-Referenzen. 
Wir haben uns in diesem Beitrag das Execute-Around-Method-Pattern sowie
die Interne Iterierung als Spezialfall davon angesehen.  Beides sind Idiome,
die mit den neuen Sprachmitteln profitieren.  Mit Lambda-Ausdrücken und
Methoden-Referenzen sind sie wesentlich einfacher zu benutzen.  In Java
8 werden die Collections interne Iterierung unterstützen.  Genau für
diese Erweiterungen der Collections im JDK sind die neuen Sprachmittel
entwickelt worden. 
Im nächsten Beitrag sehen wir und die
Lambda-Ausdrücke und Methoden-Referenzen genauer an. Literaturverweise
 
 Die gesamte Serie über Java 8:
 | ||||||||||||||||||||||||||||||||
| © Copyright 1995-2018
by 
Angelika Langer.  All Rights Reserved.  URL: 
<
http://www.AngelikaLanger.com/Articles/EffectiveJava/70.Java8.FunctionalProg/70.Java8.FunctionalProg.html>  last update: 26 Oct 2018 | ||||||||||||||||||||||||||||||||