Java 8 : Un ch'ti peu de lambda

Preview:

DESCRIPTION

Slides de la session du Ch'ti JUG animée par Remi Foxax sur Java 8

Citation preview

Un ch'ti peu de lambdades lambdas à l'ombre du beffroi

Rémi ForaxDécembre 2012

Moi

MCF à l'université Paris Est Marne-La-Vallée

Joue avec Java depuis trop longtemp pour l'avouer

Créateur de langage dynamique ou pas

Expert pour les JSR 292 (invokedynamic) et JSR 335 (lambda)

Contributeur OpenJDK, ASM, Tatoo, PHP.reboot, JDart, etc...

Les lambdaaaaahs

Pourquoi

par ce que Java c'était trop simple ??

Comment

on va tout casser Java ??

Sous le capot

surtout ne pas toucher à la VM ??

Partie IIntroduction aux lambdas

Dit papa, c'est quoi des lambdas ?

private static ArrayList<File> subDirectories(File directory) { ArrayList<File> directories = new ArrayList<>(); File[] files = directory.listFiles(); for(File file: files) { if (file.isDirectory()) { directories.add(file); } } return directories;}

public static void main(String[] args) { File file = new File("."); for(File dir: subDirectories(file)) { System.out.println(dir); }}

Parties codantes

private static ArrayList<File> subDirectories(File directory) { ArrayList<File> directories = new ArrayList<>(); File[] files = directory.listFiles(); for(File file: files) { if (file.isDirectory()) { directories.add(file); } } return directories;}

public static void main(String[] args) { File file = new File("."); for(File dir: subDirectories(file)) { System.out.println(dir); }}

Seconde version

private static File[] subDirectories(File directory) { return file.listFiles(new FileFilter() { @Override public boolean accept(File path) { return path.isDirectory(); } });}

public static void main(String[] args) { File file = new File("."); for(File dir: subDirectories(file)) { System.out.println(dir); }}

Parties codantes

private static File[] subDirectories(File directory) { return file.listFiles(new FileFilter() { @Override public boolean accept(File path) { return path.isDirectory(); } });}

public static void main(String[] args) { File file = new File("."); for(File dir: subDirectories(file)) { System.out.println(dir); }}

Problème des classes anonymes

Une classe anonyme est pas super adaptée

visuellement

Verbeux, rapport signal/bruit pas satisfaisant

sémantiquement

On veut envoyer une expression, créont une classe ...

performancecréation d'un objet à chaque appel

+1 classe sur le disque

+1 instance de java.lang.Class + métadata en mémoire

Comment font les autres langages ?

Lisp/Clojure closure's

Ruby block

Groovy Closure

Scala/C# lambda

Javascript/Python anonymous function

Python comprehension/C# Linq

Pourquoi introduire les lambdas en Java ?

les classes anonymes couvrent déjà le besoin ??

L'excuse multicore

On a plein de coeurs et on sait pas quoi en faire !

Si une partie du code est transformable en objet

On peut distribuer / paralleliser l'exécution

Presque magique

Enfin, si on a un effet de bord, on est mort !

Sans lambda ...

private static File[] subDirectories(File directory) { return file.listFiles(new FileFilter() { @Override public boolean accept(File path) { return path.isDirectory(); } });}

public static void main(String[] args) { File file = new File("."); for(File dir: subDirectories(file)) { System.out.println(dir); }}

Avec une lambda ...

private static File[] subDirectories(File directory) { FileFilter filter = (File path) -> path.isDirectory(); return file.listFiles(filter);}

public static void main(String[] args) { File file = new File("."); for(File dir: subDirectories(file)) { System.out.println(dir); }}

Syntaxe pour les expressions

sans paramètre

() -> System.out.println("welcome to the land of shtis")

avec un paramètre (inférence)

employee -> employee.isManager()

avec plusieurs paramètres

en déclarant les types

(int x, int y) -> x == y

sans déclarer les types (inférence)

(x, y) -> x == y

Syntaxe pour les instructions

sans paramètre

() -> { System.out.println("welcome to the land of shtis");}

avec un paramètre (inférence)

employee -> { return employee.isManager();}

avec plusieurs paramètres (inférence)

(index1, index2) -> { list.set(index1, list.get(index2));}

Sémantique

Typé par une functional interface (ex SAM)

Runnable, Callable, Filter, Function, ...

Une lambda n'est pas un objet

“this” représente la classe courante pas la lambda

Une lambda est convertissable en un objet qui implante une functional interface

En utilisant l'inférence

private static File[] subDirectories(File directory) { return file.listFiles( path -> path.isDirectory() );}

public static void main(String[] args) { File file = new File("."); for(File dir: subDirectories(file)) { System.out.println(dir); }}

Comment l'inférence marche ?

le compilo regarde le(s) type(s) target(s)

FileFilter filter = path -> path.isDirectory();

l'interface FileFilter est déclarée

interface FileFilter { boolean accept(File file);}

utilise jamais le body de la lambda !

Java != Haskell

Method Reference

private static File[] subDirectories(File directory) { return file.listFiles( File::isDirectory );}

public static void main(String[] args) { File file = new File("."); for(File dir: subDirectories(file)) { System.out.println(dir); }}

Method Reference

Raccourçi si la lambda délègue juste à une méthode

On utilise :: rien que pour embéter les C++eux

Si la méthode est surchargée, l'inférence utilise le target type pour trouver les types des paramétres

Et les collections

Avoir des lambdas c'est bien, mais sans support au niveau des APIs ...

Nouvelle interface java.util.Stream

Sequentielle ou parallele

Operations

intermédiairefilter, map, sorted, distinct, flatMap ...

terminalesreduce, forEach, into, findFirst ...

Full Stream

private static ArrayList<File> subDirectories( File directory) { return Arrays.stream(directory.listFiles()). filter(File::isDirectory). into(new ArrayList<>());}

public static void main(String[] args) { File file = new File("."); for(File dir: subDirectories(file)) { System.out.println(dir); }}

Avec tout dans le main()

La méthode subdirectories() sert pas vraiment !

public static void main(String[] args) { File directory = new File("."); Arrays.stream(directory.listFiles()). filter(File::isDirectory). forEach(dir -> System.out.println(dir));}

Capturer la valeur de variables locales

Et si je veux tous les sous-répertoires dont le nom commence par args[0]

public static void main(String[] args) { File directory = new File("."); String name = args[0]; Arrays.stream(directory.listFiles()). filter(File::isDirectory). filter(dir -> dir.getName().startsWith(name). forEach(dir -> System.out.println(dir));}

Une lambda peut capturer les valeurs des variables locales (comme avec une classe anonyme)

Pipeline d'élements

Arrays.stream(directory.listFiles()). filter(File::isDirectory). filter(dir -> dir.getName().startsWith(name). forEach(dir -> System.out.println(dir));

filter

Arrays.asStream(array)

collection.stream()

iterator.stream()

block

On pousse les élements à travers le pipeline

filter

false false

Method reference sur une instance

Il est possible de créer une méthode réference sur une instance

public static void main(String[] args) { File directory = new File("."); String name = args[0]; Arrays.stream(directory.listFiles()). filter(File::isDirectory). filter(path -> path.getName().startsWith(name). forEach(System.out::println);}

En résumé

Java permet de définir des lambdas et des références sur des méthodes

Une lambda est une expression ou une fonction anonyme que l'on peut convertir en un objet pour envoyer à une méthode

L'API des collections est mis à jour pour supporter les lambdas

Partie IIChangements pour Java

Changements pour Java

Gros problèmes

Inférence déjà existante pas assez puissante

Interface pas extensible

Et des petits +

Effectively finalPlus besoin de déclarer les variables locales utilisée dans les lambdas classes anonymes final

Eviter les garbages classesMettre les méthodes statiques public/private dans les interfaces

Améliorer l'inférence

Java 5/7 infére

les variables de type des méthodesList<String> list =Array.asList("foo", "bar")

List<String> list = Collections.emptyList();

les instantiations de types paramétrésList<String> list = new ArrayList<>();

=> mais f(Collections.emptyList()) marche pas !

Inference avec Java 8

Inference pour les types des lambdas

donc de gauche à droite comme les <>

Inference pour appel de méthode/instantiation diamond (JEP 101)

si dans un appel de méthodefoo(new ArrayList<>()); // ok

avec propagationString s = Collections.emptyList().get(0); // ok

Les demi-dieux de l'inférence

Dan Smith Maurizio Cimadamore

Extensibilité des interfaces

On veux obtenir un Stream à partir d'une Collection

collection.stream()

On ne peut pas ajouter une méthode dans une interface !

Solution académique: traits (!= Scala traits)

Trait

Un type contenant des méthodes abstraites et des méthodes concrètes (mais pas de champs)

Ajouter une méthode si l'on fournit l'implantation ne casse pas la compatibilité

idée: et si on pouvait mettre du code dans une interface

but, you broke Java !

Default method

Permet de fournir un code par défaut qui est utiliser si il n'en n'existe pas

interface Iterator<T> { public boolean hasNext(); public T next();

public default void remove() { throw new UnsupportedOperationException(); }

public default void forEach(Block<? super T> block) { while(hasNext()) { block.accept(it.next()); } }}

Sémantique

La méthode par défaut n'est utilisée que “par défaut”

interface A { default void m() { ... }}

class B implements A{ void m() { ... } // pas besoin de A::m !}

Héritage mutiple ??

interface A { default void m() { ... }}

interface B { default void m() { ... }}

class C implements A, B { // compile pas, faut choisir A::m ou B::m }

Héritage mutiple

interface A { default void m() { ... }}

interface B { default void m() { ... }}

class C implements A, B { // on doit fournir un code pour m() public void m() { A.super.m(); // on appel m de A B.super.m(); // on appel m de B } }

Partie IIIdans les entrailles de la VM

Au menu ...

Comment les méthodes par défaut fonctionnent ?

Comment les lambdas sont compilés ?

Comment les lambdas sont optimisées par la VM ?

Méthode par défaut

Le compilo ne peut rien faire !

Sinon on doit recompiler toutes les libraries

Doit être fait par la VM

mais● les règles de redéfinition (override) dépendent des generics ● Les générics sont un artifact à la compile pas connu à

l'exécution (erasure)

La VM doit savoir lire les signatures des generics

Méthode par défaut et erasure

interface Foo<T> { default T getFoo() { return ...; }}interface Bar { String getFoo();}class A implements Bar, Foo<String> {}

Méthode par défaut et erasure

interface Foo<T> { default ObjectT getFoo() { return ...; }}interface Bar { String getFoo();}class A implements Bar, Foo<String> {}

la VM doit générer deux méthodes Object getFoo() et String getFoo()

Méthode par défaut et erasure

interface Foo<T> { default ObjectT getFoo() { return ...; }}interface Bar { String getFoo();}class A implements Bar, Foo<String> { Object getFoo() { return getFoo(); // appel String getFoo() } String getFoo() { return ...; // recopie le bytecode de Foo::getFoo }}

Compiler une lambda naïvement

On créé une méthode synthetic pour le corps de la lambda

On crée une classe anonyme lors de la convertion vers la functional interface

iterator.forEach(dir -> System.out.println(dir));

devient

iterator.forEach(new Block<File>() { public void accept(File dir) { return lambda$1(dir); }});

static void lambda$1(File dir) { System.out.println(dir);}

Lambda objet constant ?

A l'exécution, il y a deux sortes de lambdas

Les lambdas qui ne captures pas de variable ou les méthodes référence sur une classe

● path -> path.isDirectory● File::isDirectory

Celles qui capture des variables ou les méthode référence sur des instances

● path -> path.getName().startsWith(args[0)● System.out::println

Compiler vers une classe anonyme ?

Et on se récupère tous les problèmes de perf des classes anonymes

De plus, si une lambda ne capture pas de variable, on pourrait au moins la crée que une seule fois

Mais si on utilise un champ static final, l'initialisation à lieu même si on ne l'utilise pas

Invokedynamic to rule them all

On veut un mécanisme qui délai l'initialisation au premier accès

invokedynamic

On veut un mécanisme qui permet d'indiquer que le résultat d'un calcul est constant

invokedynamic

On veut un pointeur de fonction pour éviter la création des classe anonymes

java.lang.invoke.MethodHandle

Compiler une lambda

iterator.forEach(dir -> System.out.println(dir));

devient

iterator.forEach(invokedynamic bootstrap [lambda$1]);

static void lambda$1(File dir) { System.out.println(dir);}CallSite bootstrap(Lookup lookup, String name, MethodType type, MethodHandle mh) { if (type.parameterCount() == 0) { return new ConstantCallSite(proxy(mh)); } return ...}

Lambda Proxy

Instance d'une classe qui contient le pointeur de fonction (MethodHandle) vers la lambda à exécuter

Il n'y a besoin que d'une classe proxy par functional interface

Le proxy est généré dynamiquement par la VM,pas forcément besoin de bytecode associé

Lambda Proxy en pseudo code

Le code Java correspondant est à peu près :

public class Proxy$Block implements Block { private final @Stable MethodHandle mh;

public void accept(Object o) { mh.invokeExact(o); }}

invokeExact() est un pointer check + un appel à un pointeur de fonction

Optimizations lors de l'appel

Avoir 1 seul classe pour 1 interface permet à la VM de remplacer l'appel à l'interface par le code de la classe(Class Hierarchy Analysis)

interface Iterator<T> { ... public default void forEach(Block<? super T> block) { while(hasNext()) { block.accept(it.next()); } }}

ici, block est toujours une instance de Proxy$Block

Optimizations lors de l'appel

Si un objet constant dans une boucle, il est sortie de la boucle

interface Iterator<T> { ... public default void forEach(Block<? super T> block) { while(hasNext()) { mh.invokeExact(it.next()); } }}

Dans le cas d'un method handle, il faut inliner le code référencé par le pointer de fonction (fairy tale mode)

dans le monde merveilleuxdes licornes

On guarde la boucle avec un test sur le method handle(comme pour l'On Stack Replacement)

interface Iterator<T> { ... public default void forEach(Block<? super T> block) { if (block.mh == lambda$1) { while(hasNext()) { System.out.println(it.next()); } } else deopt(); }}

et comme block est pris en paramètre, l'idée est de spécialiser le code à l'endroit de l'appel ce qui permet de ne pas faire le test

Le léger hic

Le code de la VM qui crée le lambda proxy et qui l'optimise est pas prêt

Le plan est prêt depuis longtemps, c'est la réalisation qui prend du temps

On doit quand même livrer un truc pour le JDK8

Solution temporaire: la méthode de bootstrap génère une classe anonyme dynamiquement en utilisant ASM

On implantera les lambda proxies dans une update du jdk8

En résumé

La première version des lambdas sera pas la plus optimisée

L'API dans java.util doit être fini pour fin janvier

La spec de l'inférence pas fini même si ça se précise

Bref on est grâve à la bourre

Si vous avez du temps libre

et même si vous n'en avez pas !

Downloader la dernière version du jdk8 avec les lambdas

http://jdk8.java.net/lambda/

Tester l'API, jouer avec, remonter tous les bugs

lambda-dev@openjdk.java.net

Recommended