Blog Einstieg in die Java 8 Features - Bereichern Lambdas und Streams die Sprache?

Einstieg in die Java 8 Features - Bereichern Lambdas und Streams die Sprache?

EinfĂŒhrung

Bereits seit einigen Monaten nutzen wir bei n-design die in Java 8 neu eingefĂŒhrten Sprachmittel in Entwicklungsprojekten, sodass es sich nun anbietet, ein kleines ResĂŒmee zu ziehen. Ich möchte in diesem Blog-Beitrag auf die Möglichkeiten der neuen Features eingehen und dabei auch erlĂ€utern, ob die Neuerungen das Erlernen der Sprache Java erschweren können.

Java ist dem, zugegebenermaßen nicht ganz unumstrittenen, TIOBE-Index nach zu urteilen die populĂ€rste Sprache des Jahres 2015; das, so kann man vermuten, ist vor allem auf die neu eingefĂŒhrten und zuvor lang ersehnten Sprachfeatures zurĂŒckzufĂŒhren. Diese machte Oracle im FrĂŒhjahr 2014 erstmalig fĂŒr die Industrie zugĂ€nglich, welche sie natĂŒrlich nicht unmittelbar in all ihren Entwicklungsprojekten eingesetzt hat. Dies stellt einen möglichen Grund fĂŒr den verspĂ€teten "Schub" im TIOBE-Index dar.

Auch wenn es, wie bei so ziemlich jedem polarisierenden Thema, sowohl FĂŒr- als auch Widersprecher gibt, kann man deutlich erkennen, dass Java 8 grĂ¶ĂŸtenteils positiv in der Community aufgenommen wurde. Dies dĂŒrfte dem Vernehmen nach auch daran liegen, dass seit einigen vorangegangenen Releases vergeblich nach solch großen Erweiterungen gesucht werden musste. Aus diesem Grund ist es nicht besonders auffĂ€llig, dass auch auf der diesjĂ€hrigen JavaLand-Konferenz einige VortrĂ€ge zum Thema Java 8 im Vordergrund standen, die sich unter anderem mit Streams und Lambdas beschĂ€ftigten.

Kurze ErlÀuterung von Lambdas und Streams

Die eingefĂŒhrten Konzepte sind keineswegs Neuerfindungen in der Softwareprogrammierung, sondern stellen vielmehr einen Ansatz dar, aus funktionalen Programmiersprachen bekannte Konzepte in der objektorientierten Javawelt zu integrieren und uns Java-Entwicklern so die Möglichkeiten bereitzustellen, (teilweise) funktional programmieren zu können.

Lambdas

Die grundlegendste Neuerung ist die EinfĂŒhrung des sogenannten Lambda-Ausdrucks, der eine anonyme Methode darstellt. Hiermit schafft Java unter anderem die Möglichkeit, Funktionen als Parameter zu ĂŒbergeben, was bisher zumeist ĂŒber den krĂŒckenhaften und lĂ€stigen Umweg mittels Implementierung eines Callback-Interfaces als anonyme innere Klasse bewerkstelligt wurde. Die Syntax zur Definition von Lambdas ist im Listing 1a abstrakt und im Listing 1b beispielhaft dargestellt:

Listing 1a
(Parameter-Liste) -> {Ausdruck oder Anweisungen}
Listing 1b
(String s) -> System.out.println(s);

Ein Lambda-Ausdruck stellt im Wesentlichen die Implementierung einer abstrakten Methode dar, welche in einem sogenannten Functional Interface beschrieben steht. Ein solches Functional Interface (auch als Single Abstract Method-Typ bezeichnet) definiert genau eine abstrakte Methode, welche seit Java 8 als Lambda realisiert werden kann. Das Konzept eines SAM-Typs ist keineswegs neu. Bekannte Beispiele, die wir bereits vor Java 8 kannten, sind unter anderem Runnable, Callable oder auch Comparator. Im nachfolgenden Listing 2 ist der Unterschied der beiden Realisierungsmöglichkeiten jener Typen dargestellt:

Listing 2
//SAM-Typ als anonyme innere Klasse

new SAMTypeAnonymousClass(){
    public void samTypeMethod(METHOD-PARAMETERS){
        METHOD-BODY
    }
}

//SAM-Typ als Lambda

(METHOD-PARAMETERS) -> {METHOD-BODY}

Neben den bereits explizit erwĂ€hnten Functional Interfaces beinhaltet das JDK 8 viele neue Schnittstellen, die vor allem im Bereich der Java-Streams zum Einsatz kommen, um die Möglichkeiten von Lambda-AusdrĂŒcken auszuschöpfen. Man findet diese Klassen im Package java.util.function, das auf folgender Webseite nĂ€her erlĂ€utert wird: Functions - Oracle Docs

Streams

Ermöglicht durch das Lambda-Konzept, wurde mit dem JDK 8 auch das Interface java.util.stream.Stream eingefĂŒhrt, welches eine Abstraktion von Bearbeitungsschritten auf bestimmte Datenmengen darstellt. Hierdurch ergeben sich viele neue und elegante Möglichkeiten Daten zu filtern, zu konvertieren oder auch zu aggregieren.

Ein Stream stellt dabei eine Art "Pipeline" dar, also eine Aneinanderkettung von auszufĂŒhrenden Operationen, die auf bestimmte Datensammlungen angewendet werden sollen. FĂŒr die Definition dieser Streamoperationen kann nĂŒtzlicherweise auf nutzerspezifische LambdaausdrĂŒcke zurĂŒckgegriffen werden, die von den Stream-Methoden als Parameter erwartet werden.

Listing 3
private static final List<Person> persons = Arrays.asList(
           new Person("Mike", LocalDate.of(1971, MAY, 12)),
           new Person("Micha", LocalDate.of(1971, FEBRUARY, 7)),
           new Person("Andi Bayer", LocalDate.of(1968, JULY, 17)),
           new Person("Andi Severins", LocalDate.of(1970, JULY, 22)),
           new Person("Merten", LocalDate.of(1975, JUNE, 16))
   );

public static void main(String[] args) {
       String reduced = persons.stream().
               filter(person -> person.getBirthDate().getMonth().equals(JULY)).
               map(person -> person.getName()).
               collect(Collectors.joining(", "));
       System.out.println(reduced);
}

ErlÀuterung

Wie im Listing 3 zu sehen, ermöglichen Streams eine sehr kompakte Schreibweise fĂŒr komplexe Operationen, welche wir zuvor mit aufwendigeren Schleifen realisieren mussten. Dies liegt neben den verwendeten Lambdas vor allem an der Tatsache, dass eine alternative Art der Iteration verwendet wird. Streams nĂ€mlich bedienen sich des Konzepts interner Iteration, wobei die Verwendung von Schleifen dem Konzept externer Iteration entspricht. Dies bedeutet konkret, dass einer Streamoperation nicht vermittelt werden muss, dass ĂŒber eine Datenmenge zu iterieren ist, sondern lediglich, was wĂ€hrend der intern und implizit durchgefĂŒhrten Iteration zu tun ist. Die FunktionalitĂ€t steht hierbei deutlich im Vordergrund.

Was geschieht im Beispiel?

Eine Liste von Personen wird mithilfe eines Streams auf einen String reduziert, der auf der Konsole ausgegeben wird. Auf die mithilfe der stream-Operation erzeugte Stream-Darstellung der Liste wenden wir die Methode filter an, mit der die Personen zunĂ€chst auf jene beschrĂ€nkt werden, die ihren Geburtstag im Monat Juli feiern. Der nĂ€chste Schritt ist eine Datenkonvertierung, indem zu jeder Person, die der Filterung entspricht, der Name extrahiert wird. Dieser Stream von Personennamen wird schließlich mit der Methode collect auf einen kommaseparierten String reduziert: "Andi Bayer, Andi Severins".

Vergleich zu einer klassischen funktionalen Programmiersprache

Obwohl es den Java-Machern gelungen ist, funktionale Konzepte in die Sprache zu integrieren, wird es einem alteingesessenen funktionalen Programmierer, der an klassische Sprachen wie Haskell gewöhnt ist, nicht ohne weiteres intuitiv erscheinen, jene FunktionalitÀten wiederzuerkennen. Syntaktisch gesehen nÀmlich, sind die Mittel weiterhin auf die Objektorientierung der Sprache ausgelegt und weisen damit nicht die Leichtigkeit einer reinen funktionalen Sprache auf. Das nachfolgende Beispiel zeigt einen kleinen Vergleich zwischen funktionalem Java und Haskell:

Listing 4
IntStream.rangeClosed(1,10).map(x->x*x).reduce(0, (x,y)->x+y);
Listing 5
foldl (+) 0 (map (\x -> x*x) [1..10])

Zugegebenermaßen fĂ€llt es mir persönlich deutlich leichter den Java-Code zu verstehen, zumal der Haskell-Code von rechts nach links zu lesen ist. Trotzdem fĂ€llt bei nĂ€herem Vergleichen der beiden Sprachen oftmals auf, dass mithilfe von Haskell fĂŒr viele Operationen Standardsprachmittel existieren, die zu knackigem, anspruchsvollem Sourcecode fĂŒhren und selbst Java-Streams in Sachen Kompaktheit in den Schatten stellen.

Es bleibt an dieser Stelle festzuhalten, dass mit der EinfĂŒhrung der Sprachkonzepte des JDK 8 noch lange keine funktionale Programmiersprache geschaffen wurde, da eine solche viel mehr voraussetzt. Weitere Konzepte, die eine reine funktionale Sprache auszeichnen, sind beispielsweise Immutability oder auch die Seiteneffektfreiheit. Diese können mit einem gewissen Maß an Disziplin sicherlich auch in Java realisiert werden, sind allerdings noch lange nicht so fest integriert, wie es bei Haskell der Fall ist. Java schafft es allerdings die Konzepte der funktionalen und objektorientierten Programmierung zu kombinieren, indem keine klare Trennung vorgenommen wird und somit beide AnsĂ€tze angeboten werden.

Man kann sich als Entwickler an dieser Stelle also darĂŒber freuen, dass das objektorientierte Java der "Funktion" nun eine höherwertige Rolle zuspricht, die man einer Variablen zuweisen oder auch in andere Funktionen ĂŒbergeben kann.

Was geschieht im Beispiel?

Das Beispiel liefert in beiden FĂ€llen das Ergebnis 385, was der Summe der Quadrate von 1 bis 10 entspricht (1 + 4 
​ + 100)

Erfahrungen bei n-design

Nach der Umstellung von Java 7 auf Java 8 in unseren Projekten galt es anfĂ€nglich, sich mit dem funktionalen Programmieransatz in Java vertraut zu machen. Dies wurde, neben individueller BeschĂ€ftigung mit diesem Thema, durch firmeninterne Workshops gehandhabt, sodass recht zĂŒgig erste Erfahrungen mit Lambdas und Streams gewonnen werden konnten. ZunĂ€chst fĂŒhlte sich das Anwenden der neuen Konzepte recht ungewöhnlich, aber zugleich ĂŒberaus spannend an, sodass der Großteil unserer Entwickler enormen Spaß an der Verwendung fand. Inzwischen ist der Einsatz dieser Features in unser tĂ€gliches „Doing“ verschmolzen und wir sind der klaren Meinung, dass Java sich hierdurch enorm verbessern konnte. Sei es die neue Möglichkeit SAM-Typen zu realisieren oder die große Anzahl der durch Streams ermöglichten Programmiermodelle, wie dem Filter-Map-Reduce-Framework: All das bereitet zusĂ€tzliche Freude (in seltenen FĂ€llen auch Kopfzerbrechen), wenn es um das Entwickeln unserer Java-Anwendungen geht. BegĂŒnstigt wird diese Tatsache dadurch, dass der produzierte Code durch die Verwendung von Lambdas und Streams deutlich kompakter erscheint als noch zu Zeiten, in denen alternativ auf viele Schleifen zurĂŒckgegriffen werden musste, auch um vergleichsweise simple Ergebnisse zu erzielen.

Es sollte an dieser Stelle allerdings auch hervorgehoben werden, dass die neuen Konzepte nicht umgehend fĂŒr jedermann verstĂ€ndlich und einfach in der Anwendung waren. So kam es doch recht hĂ€ufig vor, dass bei denjenigen, die fĂŒr den ominösen neu programmierten Code verantwortlich waren, um Hilfe und ErklĂ€rung gebeten wurde. Dies könnte vor allem daran liegen, dass die funktionalen Konzepte eine deutliche VerĂ€nderung in der bekannten und seit Jahren praktizierten Java-Programmierung darstellen und somit nicht umgehend einleuchtend sein mĂŒssen.

Fazit

Es ist in meinen Augen deutlich erkennbar, dass Java, seinerseits bekannt als objektorientierte Sprache, seine Schwierigkeiten hat, die Konzepte funktionaler Programmiersprachen in angemessener Weise bereitzustellen. Trotzdem sind die Spracherweiterungen eine klare Bereicherung, die es uns Entwicklern ermöglicht, die Eleganz unseres Codes deutlich zu steigern, wenn auch die Lernkurve der Sprache etwas steiler geworden sein dĂŒrfte. Ich persönlich habe in den vergangenen Wochen und Monaten vor allem das Konzept der Streams sehr zu schĂ€tzen gelernt, wodurch viele Aufgaben effizient und bĂŒndig erledigt werden können.

Java ist und bleibt eine Programmiersprache mit der wir gerne Software entwickeln und es wird bereits mit Spannung auf das neue JDK 9 gewartet. Hier stellen wir vor allem einige Erwartungen an Jigsaw, wodurch nach langem Warten schließlich ein Standardwerkzeug bereitgestellt werden soll, um ModularitĂ€t in Java-Anwendungen zu forcieren. Es bleibt abzuwarten, in wie fern Jigsaw eine Alternative fĂŒr unser derzeit favorisiertes Modularisierungsframework OSGi bieten kann
​