Download - Java 8 : Un ch'ti peu de lambda
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