Beliebte Suchanfragen
|
//

Java 8 Erste Schritte mit Lambdas und Streams

20.10.2013 | 5 Minuten Lesezeit

Bald ist es soweit: Die neue Version Java 8 wird laut Oracle im März 2014 veröffentlicht. Mit Java 8 werden die größten Änderungen an der Sprache seit der Version 1.5 (Einführung von Generics) umgesetzt. Höchste Zeit sich einige der wichtigsten Neuerungen einmal anzuschauen.

Eins der tollen neuen Features, auf das die Java Gemeinde schon lange wartet, ist die Einführung von Lambdas (oder auch Closures). Schauen wir uns ohne große Theorie gleich mal ein paar Codebeispiele an.

Seit Java 1.5 verwenden wir den extendet for loop um über die Elemente einer Collection zu iterieren:

1List<String> myList = Arrays.asList("element1","element2","element3");
2for (String element : myList) {
3  System.out.println (element);
4}

Dieser Code ist recht kurz und übersichtlich, hat jedoch den entscheidenden Nachteil, dass die Verarbeitung der Elemente nicht parallelisiert werden kann. Für den Fall, dass wir eine sehr große Liste haben, die wir gerne parallel in mehreren Threads abarbeiten möchten, müssen wir die Liste zunächst manuell in mehrere aufteilen um diese anschließend in unterschiedlichen Threads gleichzeitig verarbeiten zu können.

Wäre es nicht schön, wenn wir eine Listen Implementierung hätten, die uns diese aufwändge Arbeit abnimmt?

Genau für diesen Anwendungsfall wurde das Iterable Interface in Java 8 um die Methode forEach erweitert. Damit wird das folgende Konstrukt möglich:

1myList.forEach(new Consumer<String>() {
2   public void accept(String element) {
3      System.out.println(element);
4   }
5});

Der Code ist zwar wesentlich länger und nicht mehr so übersichtlich, hat jetzt aber den Vorteil, dass die Logik des Iterierens über die Liste und die Logik für die Verarbeitung jedes einzelnen Elements sauber voneinander getrennt sind. Die jeweilige Implementierung der forEach Methode kann jetzt die Steuerung übernehmen und sich um die Verteilung auf mehrere Threads kümmern.

Das Ganze haben wir uns allerdings mit einem sehr viel unübersichtlicherem Code erkauft.

An dieser Stelle kommen die Lambda Expressions ins Spiel. Da es sich bei Consumer um ein FunctionalInterface handelt können wir den obigen Code wie folgt vereinfachen:

1myList.forEach((String element) -> System.out.println(element));

In diesem Sonderfall können wir den Ausdruck noch weiter vereinfachen, da element unser einziger Parameter für die Lambda Expression ist und dadurch der Typ implizit aus dem Kontext abgeleitet werden kann:

1myList.forEach(element -> System.out.println(element));

Eine detaillierte Beschreibung der formellen Syntax von Lambdas würde den Rahmen dieses Artikels sprengen. Allen, die dazu mehr erfahren wollen kann ich das entsprechende Java Tutorial , sowie den Quick Start emfpehlen.

Aber halt! Das Interface Iterable wurde um eine Methode erweitert?
Bedeutet das, dass alle Implementierungen ebenfalls um diese Methode erweitert werden müssen oder nicht mehr kompatibel mit Java 8 sind?

Zum Glück nicht. Denn, eine weitere Neuerung in Java 8 ermöglicht „default“ Implementierungen von Methoden in Interfaces.

1default void forEach(Consumer<? super T> action) {
2   Objects.requireNonNull(action);
3   for (T t : this) {
4       action.accept(t);
5   }
6}

Hier sieht man, dass die default Implementierung für forEach nichts anderes tut, als in einer  extended for loop die accept() Methode des übergebenen Consumer aufzurufen.

Default Implementierungen in Interfaces werfen jedoch ein neues Problem auf:
Was passiert, wenn eine Klasse zwei Interfaces implementiert, die beide eine default Implementierung derselben Methode enthalten?

1public interface Int1 {
2     default String doSomething () {
3        return "Int1.doSomething";
4     }
5}
1public interface Int2 {
2     default String doSomething ()  {
3        return "Int2.doSomething");
4     }
5}
1public class MyClass implements Int1, Int2 { }


Eine solche Konstellation führt unweigerlich zu einem Fehler. Der Code kann nicht kompiliert werden:

1MyClass.java:11: error: 
2class MyClass inherits unrelated defaults for doSomething() from types Int1 and Int2

Die Lösung liegt darin, den Konflikt explizit aufzuheben, indem die Methode doSomething() in MyClass überschrieben wird:

1public class MyClass implements Int1, Int2 {
2    public String doSomething() {
3        return Int1.super.doSomething();
4    }
5}

Die Befürchtung, dass über den default Mechanismus eine Mehrfachvererbung in die Sprache eingeführt wird ist also nicht berechtigt.

Gerade bei den Collections werden die default Implementierungen in Java 8 bereits intensiv genutzt. Neben der schon gezeigten forEach() Methode im Interface Iterable wurde beispielsweise das Collection Interface um die Methoden stream() und parallelStream() erweitert:

1default Stream<E> stream() {
2   return StreamSupport.stream(spliterator(), false);
3}

Streams ermöglichen es dem Benutzer Kommandos in einer Art Pipeline zusammenzubauen. Der Stream selbst stellt dabei keine Datenstruktur dar, sondern definiert nur, welche Operationen auf den Daten ausgeführt werden. Neben einer wesentlich besseren Lesbarkeit des Codes erreicht man mit diesem Konstrukt auch eine bessere Parallelisierbarkeit. Nehmen wir an, wir wollen die Elemente einer Liste zählen, die einem bestimmten Kriterium entsprechen:

1Collection<String>myList = Arrays.asList("Hello","Java");
2long countLongStrings = myList.stream().filter(new Predicate<String>() {
3          @Override
4          public boolean test(String element) {
5              return element.length() > 4;
6          }
7}).count();

Ok, das ist nicht besonders übersichtlich. Man muss viel Code lesen und Zeit investieren, um zu erkennen, was eigentlich passiert. Aber zum Glück gibt es ja Lambdas:

1Collection<String> myList = Arrays.asList("Hello","Java");
2long countLongStrings = myList.stream().filter(element -> element.length() > 4).count();

Dieser Code liest sich schon sehr viel einfacher. Das fachliche Problem (Zählen der Elemente mit mehr als 4 Zeichen) rückt in den Vordergrund, während der Code, zum Iterieren über die Elemente nicht mehr relevant ist.
Ein weiterer Vorteil der Variante 2 besteht darin, dass der Kompiler für den Lambda Ausdruck keine innere Klasse mehr erzeugen muss. Während javac beim kompilieren der ersten Variante zwei Ausgabedaten erzeugt:

ForEach$1.class        ForEach.class

wird bei der zweiten Variante nur noch ForEach.class erzeugt. Das liegt daran, dass für die Lambda Ausdrücke das seit Java 7 verfügbare Sprachfeature „invoke dynamic“ genutzt wird.

Schauen wir uns die Streams noch etwas genauer an:
Bei Stream.filter handelt es sich um eine sogenannte “intermediate operation”. Diese Operationen geben als Ergebnis einen modifzierten Stream zurück (stream-producing), auf dem eine weitere Operation ausgeführt werden kann. Weitere Beispiele für intermediate operations sind:

  • map()
  • sorted()
  • unsorted()
  • distinct()
  • limit()
  • peek().

Die Methode count() hingegen ist eine “terminal operation“. Das bedeutet, diese Operation steht auf jeden Fall am Ende der Pipeline (value-producing).
Beispiele für terminal operations sind

  • sum()
  • min()
  • max()
  • reduce()
  • findFirst()

Neben Lambdas und Streams wird es viele weitere Neurungen in Java 8 geben, denen wir mit Sicherheit einige weitere Artikel hier im Blog widmen werden. Die wichtigsten aus meiner Sicht sind die neue Date and Time API , JavaScript Integration (Project Nashorn) sowie der Verzicht auf die Permanent Generation in der Hotspot VM .


Mehr zum Thema Java gibt es auch auf unserer Seite: Infos zur Java Virtual Machine , Memory Leaks und dem Java Profiler .

|

Beitrag teilen

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.