Java 8 Features

Java 8 Features

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

Einführung

Bereits seit einigen Monaten nutze ich 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 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)

Eigene Erfahrungen im Job

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.

Please follow and like me 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *

Enjoy this blog? Please spread the word :)