Java 8 Erste Schritte mit Lambdas und Streams

5 Kommentare

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:

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

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:

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

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:

myList.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:

myList.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.

default void forEach(Consumer<? super T> action) {
   Objects.requireNonNull(action);
   for (T t : this) {
       action.accept(t);
   }
}

 

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?

public interface Int1 {
     default String doSomething () {
        return "Int1.doSomething";
     }
}
public interface Int2 {
     default String doSomething ()  {
        return "Int2.doSomething");
     }
}
public class MyClass implements Int1, Int2 { }

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

MyClass.java:11: error: 
class 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:

public class MyClass implements Int1, Int2 {
    public String doSomething() {
        return Int1.super.doSomething();
    }
}

 

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:

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

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:

Collection<String>myList = Arrays.asList("Hello","Java");
long countLongStrings = myList.stream().filter(new Predicate<String>() {
          @Override
          public boolean test(String element) {
              return element.length() > 4;
          }
}).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:

 

Collection<String> myList = Arrays.asList("Hello","Java");
long 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.

Tags

Lars Rückemann

Lars Rückemann leitet die Niederlassung Solingen der codecentric AG. In seiner fast 20-jährigen IT-Laufbahn hat er in den Rollen Softwareentwickler, Architekt, Solution Engineer und Agile Coach in verschiedenen Branchen und mit unterschiedlichen Technologien Erfahrungen sammeln können. Aktueller Schwerpunkt seiner Arbeit ist es, Unternehmen beim Setup agiler Softwareentwicklungsprojekte zu unterstützen. Dabei liegt sein Fokus sowohl im Bereich der Backlog-Erstellung als auch darauf, Software-Entwicklungs-Praktiken und Continuous Delivery in den Teams zu etablieren.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Artikel von Lars Rückemann

Agile Management

Planlos agil

Java 8 first steps with Lambdas and Streams

Kommentare

  • Chris

    18. März 2014 von Chris

    Ich denke, hier hat sich ein Fehler eingeschlichen:

    Da es sich bei Iterable um ein FunctionalInterface handelt…

    Nicht Iterable ist das Functional-Interface sondern Consumer, was auch irgendwie mehr Sinn ergibt, da Consumer das Interface mit nur einer abstrakten Methode ist und daher auf einen Lambda-Ausdruck abgebildet werden kann.

    siehe Doku der Klassen/Interfaces auf: http://docs.oracle.com/javase/8/docs/api/index.html

    Der Artikel hat mir bei dem Einstieg in Java 8 aber trotzdem geholfen! 😉

    • Lars Rückemann

      Hallo Chris,

      vielen Dank für den Hinweis.
      Iterable ist zwar auch ein FunctionalInterface, an der Stelle wird allerdings tatsächlich ausgenutzt, dass die Methode accept() die einzige abstrakte Methode des Consumer Interface ist.
      Ich werde das im Text entsprechend ändern.

  • Hartmut

    Toller Beitrag, vielen Dank!

  • joe

    Vielen dank, endlich hat mal jemand den Kerngedanken sofort verständlich formuliert. Aber die Reihenfolge: Könnte beim ersten Beispiel auch „element3\nelement2\nelement1\n“ herauskommen? Ist println() wirklich bei allen JVM’s threadsave? Könnte sonst „eeellleeemmmeeennnttt321\n\n\n“ herauskommen?
    Allgemeiner: Verhält List.forEach() sich nach außen „ordered“?

    • Mike

      8. Juni 2017 von Mike

      Wenn die default Implementierung des Interface verwendet wird schon – wie man im Artikel sieht, wird dort die accept(..) Methode des Consumers in einer altbekannten fro-Schleife ausgeführt.

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.