Publicité
Publicité

Contenu connexe

Publicité
Publicité

Java 8 : Un ch'ti peu de lambda

  1. Un ch'ti peu de lambda des lambdas à l'ombre du beffroi Rémi Forax Décembre 2012
  2. 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...
  3. 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 ??
  4. Partie I Introduction aux lambdas
  5. 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); } }
  6. 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); } }
  7. 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); } }
  8. 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); } }
  9. 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 ... performance création d'un objet à chaque appel +1 classe sur le disque +1 instance de java.lang.Class + métadata en mémoire
  10. 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 ??
  11. 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 !
  12. 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); } }
  13. 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); } }
  14. 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
  15. 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)); }
  16. 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
  17. 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); } }
  18. 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
  19. 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); } }
  20. 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
  21. 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édiaire filter, map, sorted, distinct, flatMap ... terminales reduce, forEach, into, findFirst ...
  22. 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); } }
  23. 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)); }
  24. 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)
  25. Pipeline d'élements Arrays.asStream(array) collection.stream() filter filter block iterator.stream() false false On pousse les élements à travers le pipeline Arrays.stream(directory.listFiles()). filter(File::isDirectory). filter(dir -> dir.getName().startsWith(name). forEach(dir -> System.out.println(dir));
  26. 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); }
  27. 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
  28. Partie II Changements pour Java
  29. Changements pour Java Gros problèmes Inférence déjà existante pas assez puissante Interface pas extensible Et des petits + Effectively final Plus besoin de déclarer les variables locales utilisée dans les lambdas classes anonymes final Eviter les garbages classes Mettre les méthodes statiques public/private dans les interfaces
  30. Améliorer l'inférence Java 5/7 infére les variables de type des méthodes List<String> list =Array.asList("foo", "bar") List<String> list = Collections.emptyList(); les instantiations de types paramétrés List<String> list = new ArrayList<>(); => mais f(Collections.emptyList()) marche pas !
  31. 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éthode foo(new ArrayList<>()); // ok avec propagation String s = Collections.emptyList().get(0); // ok
  32. Les demi-dieux de l'inférence Dan Smith Maurizio Cimadamore
  33. 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)
  34. 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
  35. but, you broke Java !
  36. 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()); } } }
  37. 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 ! }
  38. 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 }
  39. 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 } }
  40. Partie III dans les entrailles de la VM
  41. 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 ?
  42. 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
  43. Méthode par défaut et erasure interface Foo<T> { default T getFoo() { return ...; } } interface Bar { String getFoo(); } class A implements Bar, Foo<String> { }
  44. 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()
  45. 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 } }
  46. 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); }
  47. 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
  48. 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
  49. 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
  50. 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 ... }
  51. 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é
  52. 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
  53. 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
  54. 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)
  55. dans le monde merveilleux des 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
  56. 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
  57. 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
  58. 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
Publicité