Ausdrucksstark: Ein vertiefter Blick auf die neuen Lambda-Ausdrücke in Java 8 (JSR 335)
Martin Lehmann, Markus Günther: „Ausdrucksstark: Ein vertiefter Blick auf die neuen Lambda-Ausdrücke in Java 8 (JSR 335)“.
Java User Group Frankfurt, 26. März 2014
Studium der Informatik an der Universität Würzburg Berufsstart 1997 Einstieg bei sd&m 2001 Verschiedene industrielle Softwareprojekte als Entwickler, Architekt, Projektmanager, Berater Mitarbeit bei sd&m Research zu Quasar von 2006-2007 Seit 2010 Mitglied der Geschäftsleitung und CTO beim IT-Dienstleister Accso – Accelerated Solutions GmbH in Darmstadt www.accso.de Interne Weiterbildung, Praktikum zu Java 8 im Sommer 2013 auf Basis JDK1.8b86 und IntelliJ 12 Veröffentlichungen zu Java 7 u. Java 8:
Weg zu Java 8 (c) Helge Schütt, 2013, Hafencity HH Java 8 GA seit 18. März 2014! Java 6: Dezember 2006 Java 7: Juli 2011 Größte Neuerung und erste wesentliche Sprachänderung seit den Generics in Java 5: Lambda Expressions Umfrage von Typesafe im Feb‘14: Größte Vorfreude bei Entwicklern Lambdas mit 83% Collections/Streams mit 30%
Java 8 mit JSR 335? Lambda-Ausdrücke Verbesserte Typinferenz Methodenreferenzen Collections & Streams Filter/Map/Reduce, Parallelisierung Funktionale Interfaces Default- Methoden in Interfaces java.util.function Nötig, um in Java 8 die Lambda-Ausdrücke für Java-Collections nutzbar zu machen bei gleichzeitiger Abwärtskompatibilität.
einem Interface eine Methode hinzufügen möchte? public class FooImpl implements Fooable { void foo() { System.out.println(“Hello!”); } } class App { // Version 1 public static void main(...) { new FooImpl().foo(); } } public interface Fooable { void foo(); // Version 1 }
einem Interface eine Methode hinzufügen möchte? public interface Fooable { void foo(); // Version 2 void bar(); } class App { // Version 2 public static void main(...) { new FooImpl().bar(); } } public class FooImpl implements Fooable { void foo() { System.out.println(“Hello!”); } } class App { // Version 1 public static void main(...) { new FooImpl().foo(); } } public interface Fooable { void foo(); // Version 1 } java.lang. NoSuchMethodError: Fooable.bar()V
bei Kompatibilitätsbetrachtungen von modifizierten Klassen. Laufzeitsicht Hinzufügen einer Interface-Methode ist binärkompatibel Wir können Fooable (Version 2) kompilieren … … und alle Class-Dateien, die das Interface nutzen, linken weiterhin VM „webt“ fehlende Methode zur Link-Zeit in die Klassen – diese Methode wirft NoSuchMethodError Quelltextsicht FooImpl muss Vertrag von Fooable (Version 2) erfüllen Hinzufügen einer Interface-Methode ist nicht abwärtskompatibel Default-Methods in Java 8 Default-Methode ist eine Methodenimplementierung im Interface VM „webt“ Default-Methode zur Link-Zeit in implementierende Klasse Schlüsselwort default zeigt Default-Methode im Interface an public interface Fooable { void foo(); default void bar() { /* Default-Implementierung im Interface */ } } Schlüsselwort default
sein, welche Default- Methode zur Link-Zeit in Implementierungen genutzt wird. Regel 1 Class wins (Super-)Klasse hat immer Vorrang vor Interface („Class wins“) Gilt für konkrete und abstrakte Methoden in einer (Super-)Klasse Gibt es keine überschriebene Default-Methode in einer (Super)-Klasse, dann greift Regel 2 Regel 2 Subtype wins Spezifischstes Interface mit Default-Methode wählen („Subtype wins“) Beispiel: Default-Methode in List<E> hat Vorrang vor Default-Methode in Collection<E> Gibt es mehrere gleich-spezifische Interfaces, dann greift Regel 3 Regel 3 Konflikt kann nicht durch den Compiler aufgelöst werden Behandle Default-Methode, als wäre sie abstrakt Erfordert Implementierung in konkreter Klasse Nicht eindeutig?! Klasse kann mehrere Default-Methoden mit gleicher Signatur haben … weil Interface von anderem Interface erben kann … weil eine Klasse mehrere Interfaces implementieren kann Welche Default-Methode wählt der Compiler?
der Compiler die spezifischste Implementierung einer Default-Methode. public interface A { default void sayHello() { System.out.println(“Hallo aus A”); } } public interface B extends A { default void sayHello() { System.out.println(“Hallo aus B”); } } public class C1 implements A, B { public static void main(String[] args) { C1 c = new C1(); c.sayHello(); } } Ergebnis: „Hallo aus B“ B.sayHello ist spezifischste Implementierung aus Sicht von C1!
Kompilierfehler an, falls er die spezifischste Default-Methode nicht zuordnen kann. public interface D { default void sayHello() { System.out.println(“D”); } } public interface E { default void sayHello() { System.out.println(“E”); } } public class C2 implements D, E { public static void main(String[] args) { C2 c = new C2(); c.sayHello(); } } C2 kompiliert nicht, da D und E „gleich-spezifisch“
explizit auflösen. public interface D { default void sayHello() { System.out.println(“D”); } } public interface E { default void sayHello() { System.out.println(“E”); } } public class C2 implements D, E { public static void main(String[] args) { C2 c = new C2(); c.sayHello(); } @Override public void sayHello() { D.super.sayHello(); } } C2 kompiliert nun wg. expliziter Auflösung
Ja, aber… Mehrfachvererbung gibt es schon immer in Java: Klasse B kann von Klasse A ableiten und … zusätzliche Interfaces implementieren Aber das ist nicht Mehrfachvererbung von Zustand … sondern Mehrfachvererbung von Verhalten! Achtung: Default-Methoden sind virtuelle Methoden Spezifischste Default-Methode wird aus Sicht des dynamischen Typs ermittelt Kann zu überraschenden Resultaten führen
A { default void result(int i, int j) { System.out.println( “C.result = “ + calc(i, i)); } } public interface B extends A { default int calc(int i, int j) { return i * j; } } public interface C extends A { default void result(int i, int j) { System.out.println( “C.result = “ + calc(i, j)); } } public interface B extends A { default int calc(int i, int j) { return i * j; } } Die Mehrfachvererbung von Verhalten kann zu überraschenden Ergebnissen führen. public interface A { default void result(int i, int j) { System.out.println(“A.result = “ + calc(i, j)); } default int calc(int i, int j) { return i + j; } } public class Impl implements B, C { public static void main(String[] args) { new Impl().result(3, 4); // Ergebnis? } } Ergebnis: 12, nicht 7
(Object o) -> o.toString() Eigenschaften Lambda-Ausdruck ist eine anonyme Methode Hat Parameter, einen Rückgabetyp, einen Körper Operator für Lambda-Ausdrücke Parameterliste Körper Kann den umschließenden Scope nutzen (variable capture) (Person p) -> p.getName().equals(name) Kann existierende Methoden referenzieren Object::toString Operator für Methodenreferenzen
Ohne Typangabe x -> x + 1; Mit Typangabe (Integer i) -> list.add(i); Block, explizites return (Integer a, Integer b) -> { if (a < b) return a + b; return a; } Ohne Parameter () -> System.out.println(“Hallo Lambda!”);
in Java? Code-as-Data Verhalten in Form eines Lambda-Ausdrucks kodieren… … und als Parameter an eine generische Methode wie map oder filter übergeben Entkoppelt Kodierung von Ausführung (vgl. innere Klassen) Lesbarkeit Code ist Kommunikationsmittel Beschreibt das „was“, nicht das „wie“ Mächtigere, ausdrucksstärkere APIs möglich Funktionaler Programmierstil Höherer Abstraktionsgrad durch Funktionen höherer Ordnung Eleganter, kürzer, prägnanter Keine Seiteneffekte
Klassen. Bedeutung für bisherigen Code Äquivalenz zwischen Lambdas und anonyme innerer Klasse mit Single-Abstract-Method (SAM). Beispiele: Runnable, Comparator, ActionListener, … Lambda Führt keinen zusätzlichen Scope ein Damit kein Shadowing von Bezeichnern this bezieht sich auf die umschließende Klasse Ein return aber nicht! Anonyme innere Klasse Führt zusätzlichen Scope ein Daher Shadowing von Bezeichnern in verschiedenen Scopes this bezieht sich auf anonyme innere Klasse, umschließender Kontext über ClassName.this erreichbar
sich einfach in äquivalente Lambda-Ausdrücke migrieren. Comparator<Integer> sortAscending = new Comparator<Integer>() { @Override public int compare(Integer a, Integer b) { return a-b; } }; Typischer Code für einen Comparator, der Ganzzahlen vergleicht … Comparator<Integer> sortAscending = new Comparator<Integer>() { @Override public int compare(Integer a, Integer b) { return a-b; } }; Warum muss ich compare überschreiben? Es ist die einzige Methode des Interface, mein Vorhaben ist doch klar?
sich einfach in äquivalente Lambda-Ausdrücke migrieren. Comparator<Integer> sortAscending = new Comparator<Integer>() { @Override public int compare(Integer a, Integer b) { return a-b; } }; Comparator<Integer> sortAscending = (Integer a, Integer b) -> { return a-b; }; Reduktion auf das Wesentliche: Eingabedaten und Code-Block Die Definition eines Comparator-Interfaces erfordert viel Boilerplate-Code Comparator<Integer> sortAscending = new Comparator<Integer>() { @Override public int compare(Integer a, Integer b) { return a-b; } };
sich einfach in äquivalente Lambda-Ausdrücke migrieren. Comparator<Integer> sortAscending = (Integer a, Integer b) -> { return a-b; }; Comparator<Integer> sortAscending = (Integer a, Integer b) -> a-b; Comparator<Integer> sortAscending = (a, b) -> a-b; … wir führen allerdings immer noch redundante Informationen mit … Block-Schreibweise und explizites return entfernen Der Compiler kann Typ- informationen inferieren
D. Darcy‘s Präsentation On the road to JDK 8 @ Devoxx 2012, Antwerpen Operationen / s Single-threaded Multi-threaded (saturiert) Faktor Anonyme innere Klasse 160 1407 8,8 Capturing Lambda 160 1400 8,8 Non Capturing Lambda 636 23201 36,4 Performance Lambda-Funktionen sind im Worst-Case so effizient wie anonyme innere Klassen, im Best-Case deutlich schneller. Oracle Performance Team hat Effizienzbetrachtungen durchgeführt (Daten von Herbst 2012). Lambdas ohne Zugriff auf umschließenden Kontext: Um Faktoren schneller!
D. Darcy‘s Präsentation On the road to JDK 8 @ Devoxx 2012, Antwerpen Kosten Anonyme innere Klasse Lambda-Ausdruck Link-Zeit Laden der Klasse Einmalige Kosten beim Aufsetzen der capture Konstruktion Konstruktor aufrufen Erzeugung des Lambda-Ausdrucks Aufruf invokeinterface invokedynamic
Typsystem für einen Lambda-Ausdruck. Funktionale Interfaces Single-Abstract-Method-Typen (SAM-Typen) Darf nur eine abstrakte Methode enthalten … aber beliebig viele Default-Methoden Annotation java.lang.FunctionalInterface Beispiele: Runnable, Comparator, ActionListener, … Evaluation eines Lambda-Ausdrucks Funktionales Interface ist sog. Target Type für Lambda-Ausdruck Rückgabetyp der abstrakten Methode Parametertypen der abstrakten Methode Deklarierte Exceptions der abstrakten Methode Verbesserte Typinferenz von Java 8 bringt Lambda und Interface zusammen
einem funktionalen Interface. Comparator<Integer> sortAscending = (Integer a, Integer b) -> a-b; @FunctionalInterface public interface Comparator<T>() { int compare(T o1, T o2); ... }; Rückgabetyp stimmt überein Parametertyp stimmt überein
funktionale Interfaces, die für typische Use-Cases bestimmt sind. Neue funktionale Interfaces 1. Function - Zur Abbildung von Funktionen 2. Consumer - Zur Abbildung von Prozeduren mit Seiteneffekten 3. Predicate - Zur Abbildung von Prädikaten 4. Supplier - Zur Abbildung von Objektfabriken Neue Klasse Optional<T> Vereinfacht Null-Behandlung im Code Methoden können einfach Optional<T> liefern Optional bietet viele Convenience-Methoden an Arbeiten mit funktionalen Interfaces zusammen @NotNull Optional<T> opt = ... opt.ifPresent(t -> doSomething(t));
Function<T,R> ist ein Transformator eines Eingabetyps T in einen Ausgabetyp R. @FunctionalInterface public interface Function<T, R> { public R apply(T t); public default <V> Function<T, V> compose( Function<? super R, ? extends V> after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); } } Function<Integer, Integer> square = n -> n*n; assertEquals(9, square.apply(3)); Definition Einfaches Beispiel Definition Evaluation
für solche Function<T,R> -Instanzen sind Transformatoren bei map() public static <I, O> List<O> map(Function<I, O> mapper, List<I> input) { final List<O> outputList = new ArrayList<>(); input.forEach(i -> { O o = mapper.apply(i); outputList.add(o); }); return outputList; } Code-as-Data Nicht Function<T,R>.apply(arg) direkt aufrufen, … … sondern Function<T,R> als Lambda-Ausdruck übergeben! map(x -> Integer.valueOf(x), input); generische Funktion Lambda-Ausdruck Daten
eine Funktion, die den Programmzustand durch Seiteneffekte verändert. @FunctionalInterface public interface Consumer<T> { public void accept(T t); public default Consumer<T> chain( Consumer<? super T> other) { Objects.requireNonNull(other); return (T t) -> { accept(t); other.accept(t);}; } } Definition Eigenschaften Analog zu Function<T,R>, aber keine Rückgabe sondern Seiteneffekt Mehrere Consumer<T> kann man miteinander verketten - Ausführung von links nach rechts Einfaches Beispiel Consumer<String> out = s -> System.out.println(s); out.accept("Test");
für Consumer<T> -Instanzen ist die Verarbeitung von Collections mit forEach. public static <T> void forEach(Consumer<T> consumer, List<T> input) { for (T t : input) { consumer.accept(t); } } Code-as-Data Nicht Consumer<T>.accept(arg) direkt aufrufen, … … sondern Consumer<T> als Lambda-Ausdruck übergeben! forEach(s -> System.out.println(s), input); generische Funktion Lambda-Ausdruck Daten
eine Funktion, die ein Objekt vom Typ T einem logischen Test unterzieht. @FunctionalInterface public interface Predicate<T> { public boolean test(T t); public default Predicate<T> and(...) { ... } public default Predicate<T> negate() { ... } public default Predicate<T> or(...) { ... } public default Predicate<T> xor(...) { ... } } Definition
für Predicate<T> -Instanzen ist die Filterung von Collections mit filter() public static <T> List<T> filter(Predicate<T> criterion, List<T> input) { final List<T> filteredList = new ArrayList<>(); input.forEach(i -> { if (criterion.test(i)) { filteredList.add(i); } }); return filteredList; } Code-as-Data Nicht Predicate<T>.test(arg) direkt aufrufen, … … sondern Predicate<T> als Lambda-Ausdruck übergeben! filter(n -> n % 2 == 0, input); generische Funktion Lambda-Ausdruck Daten
eine Factory-Funktion, die Objekte vom Typ T liefert. @FunctionalInterface public interface Supplier<T> { public T get(); } Definition Einfaches Beispiel Supplier<Integer> generator = () -> (int) (Math.random() * NUMBER_RANGE); int randomNumber = generator.get(); assertTrue( randomNumber > 0 && randomNumber <= NUMBER_RANGE); Nutzung? Zufallszahlengeneratoren braucht man jetzt nicht so oft … Am ehesten als … Closure um Collection, aus der Elemente geholt werden Objektfabrik
basiert auf einer Pipes-and- Filters-Architektur. prices.stream().filter(p -> p >= 40).mapToDouble(p -> p * 0.9).sum(); Source IntermediateOp Stream<Integer>
basiert auf einer Pipes-and- Filters-Architektur. prices.stream().filter(p -> p >= 40).mapToDouble(p -> p * 0.9).sum(); Source IntermediateOp Stream<Integer> IntermediateOp Stream<Integer>
sind Wrapper um einen Lambda-Ausdruck. prices.stream().filter(p -> p >= 40).mapToDouble(p -> p * 0.9).sum(); Source FilterOp Stream<Integer> IntermediateOp Stream<Integer> TerminalOp Stream<Double> Double Instanz von Predicate<Integer>
sind Wrapper um einen Lambda-Ausdruck. prices.stream().filter(p -> p >= 40).mapToDouble(p -> p * 0.9).sum(); Source FilterOp Stream<Integer> MapOp Stream<Integer> TerminalOp Stream<Double> Double Instanz von Predicate<Integer> Instanz von Instanz von ToDoubleFunction <Integer>
sind Wrapper um einen Lambda-Ausdruck. prices.stream().filter(p -> p >= 40).mapToDouble(p -> p * 0.9).sum(); Source FilterOp Stream<Integer> MapOp Stream<Integer> ReduceOp Stream<Double> Double Instanz von Predicate<Integer> Instanz von Instanz von ToDoubleFunction <Integer> Instanz von DoubleBinaryOperator
Operationen erfolgt erst, wenn der Stream durch eine terminale Operation angestossen wird. String[] txt = { "State", "of", "the", "Lambda", "Libraries", "Edition" }; IntStream is = Arrays.stream(txt). filter(s -> s.length() > 3). mapToInt(s -> { System.out.println(s + ","); return s.length(); }); // erste terminale Operation is.forEach(l -> System.out.print(l + ", ")); Mit der Variable is halten und nutzen wir eine Referenz auf den Stream. Das ist schlechter Stil und potentiell gefährlich, denn zwei oder mehr terminale Operationen könnten darauf aufgerufen werden, was nicht erlaubt ist. Nur eine terminale Operation ist möglich! Danach ist der Stream “verbraucht”.
"State", "of", "the", "Lambda", "Libraries", "Edition" }; IntStream is = Arrays.stream(txt). filter(s -> s.length() > 3). mapToInt(s -> { System.out.println(s + ","); return s.length(); }); // erste terminale Operation is.forEach(l -> System.out.print(l + ", ")); // zweite terminale Operation int sum = is.reduce(0, (l1, l2) -> { return (l1 + l2); }); Laufzeitfehler daher bei der zweiten terminalen Operation! Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:221) at java.util.stream.IntPipeline.reduce(IntPipeline.java:453) . . . Die Auswertung von intermed. Operationen erfolgt erst, wenn der Stream durch eine terminale Operation angestossen wird.
(1/3) ohne Exception-Handling in try/catch String[] txt = { "abc", "abcde", "abc" }; // Stream wird aufgebaut Stream<Character> s1 = Arrays.stream(txt). map(t -> { // gezielte Exception fuer alle Strings mit Laenge > 3 if (t.length()>3) throw new RuntimeException("test too long"); return t.charAt(0); }); // Stream wird konsumiert s1.forEach( c -> System.out.print(c.charValue() + ",") ); Ausgabe: a, danach Exception (da abcde zu lange ist). abc wird nicht mehr verarbeitet.
parallelisierende Probleme Teile und Herrsche Zerlege Problem sukzessive in Teilprobleme ... bis Teilproblem klein genug, so dass es direkt gelöst werden kann ... und führe dann die Ergebnisse zusammen „embarassingly parallel“ // join loeseProblemMitForkJoin (Problem p) if (p ist klein genug) loese p direkt und sequentiell else teile p in unabhängige Teilprobleme p1 und p2 // split loese p1 und p2 unabhängig voneinander (rekursiv) // fork führe Teilergebnisse von p1 und p2 zusammen // join
der Parallelisierung ist Fork/Join-Framework aus Java7 (JSR166y). T rekursive Aufteilung in Teilprobleme in der Fork-Phase Zusammenführung der Teilergebnisse in der Join-Phase Problem klein genug? Sequentielle Berechnung T1 T2 T1 T2 T1 T2 T1 T2 T T t T11 T21 T22 T12 T11 T21 T22 T12
Zustandslose intermediäre Operationen Zustandslose intermediäre Operationen bearbeiten ein einzelnes Element Beispiele filter map Problemlos parallelisierbar, keine Synchronisierung erforderlich Zustandsbehaftete intermediäre Operationen benötigen einen zusätzlichen Kontext Beispiele limit nur die ersten k Elemente distinct nur disjunkte Elemente sorted Sortierung, siehe Folgefolie Schwierig parallelisierbar Zustandsbehaftete intermediäre Operationen
.sorted() .statelessOps() .terminal() Barriere: Parallelisierung wird hier gezielt sequentialisiert. Ergebnisse werden zwischengepuffert (auch bei seq. Stream!) Parallelisierung wird neu aufgesetzt Sequentielle Bearbeitung (in einem Thread).
uns bei JavaSPEKTRUM und Heise Developer… und die heutigen Folien auf accso.de Markus Günther, Martin Lehmann „Lambda-Ausdrücke in Java 8“. In JavaSPEKTRUM, 03/2013 PDF-Download hier: http://www.sigs- datacom.de/fachzeitschriften/javaspektrum/archiv/artikelansicht.html?tx_mwjournals_pi 1[pointer]=0&tx_mwjournals_pi1[mode]=1&tx_mwjournals_pi1[showUid]=7237 Markus Günther, Martin Lehmann „Java 7: Das Fork-Join-Framework für mehr Performance“ In JavaSPEKTRUM, 05/2012 PDF-Download hier: http://www.sigs- datacom.de/fachzeitschriften/javaspektrum/archiv/artikelansicht.html?tx_mwjournals_pi 1[pointer]=0&tx_mwjournals_pi1[mode]=1&tx_mwjournals_pi1[showUid]=7396 Markus Günther, Martin Lehmann „Streams und Collections in Java 8“. Heise Developer, http://www.heise.de/developer/artikel/Streams-und- Collections-in-Java-8-2151376.html
zu Java 8 https://jdk8.java.net/ http://www.techempower.com/blog/2013/03/26/everything- about-java-8/ http://www.heise.de/developer/artikel/Was-Entwickler-mit-Java-8- erwartet-1932997.html Weitere Links