Analyser des expressions lambda en Java

image


D'après un traducteur: LambdaMetafactory est probablement l'un des mécanismes Java 8. les plus sous-estimés. Nous l'avons découvert récemment, mais nous apprécions déjà ses capacités. La version 7.0 du framework CUBA améliore les performances en évitant les appels réfléchis en faveur de la génération d'expressions lambda. L'une des applications de ce mécanisme dans notre cadre est la liaison des gestionnaires d'événements d'application par des annotations, une tâche courante, un analogue d' EventListener de Spring. Nous pensons que la connaissance des principes de LambdaFactory peut être utile dans de nombreuses applications Java, et nous nous empressons de partager cette traduction avec vous.


Dans cet article, nous allons montrer quelques astuces peu connues lorsque vous travaillez avec des expressions lambda dans Java 8 et les limites de ces expressions. Le public cible de l'article est les développeurs Java, les chercheurs et les développeurs de boîtes à outils. Seule l'API Java publique sera utilisée sans com.sun.* Et les autres classes internes, le code est donc portable entre différentes implémentations JVM.


Avant-propos court


Les expressions lambda sont apparues dans Java 8 comme un moyen d'implémenter des méthodes anonymes et,
dans certains cas, comme alternative aux classes anonymes. Au niveau du bytecode, l'expression lambda est remplacée par l' invokedynamic . Cette instruction est utilisée pour créer une implémentation d'interface fonctionnelle et sa seule méthode délègue l'appel à la méthode réelle, qui contient le code défini dans le corps de l'expression lambda.


Par exemple, nous avons le code suivant:


 void printElements(List<String> strings){ strings.forEach(item -> System.out.println("Item = %s", item)); } 

Ce code sera converti par le compilateur Java en quelque chose de similaire à:


 private static void lambda_forEach(String item) { // Java  System.out.println("Item = %s", item); } private static CallSite bootstrapLambda(Lookup lookup, String name, MethodType type) { // //lookup =  VM //name = "lambda_forEach",  VM //type = String -> void MethodHandle lambdaImplementation = lookup.findStatic(lookup.lookupClass(), name, type); return LambdaMetafactory.metafactory(lookup, "accept", MethodType.methodType(Consumer.class), //  - MethodType.methodType(void.class, Object.class), //  Consumer.accept    lambdaImplementation, //     - type); } void printElements(List<String> strings) { Consumer<String> lambda = invokedynamic# bootstrapLambda, #lambda_forEach strings.forEach(lambda); } 

L'instruction invokedynamic peut être grossièrement représentée comme un tel code Java:


 private static CallSite cs; void printElements(List<String> strings) { Consumer<String> lambda; //begin invokedynamic if (cs == null) cs = bootstrapLambda(MethodHandles.lookup(), "lambda_forEach", MethodType.methodType(void.class, String.class)); lambda = (Consumer<String>)cs.getTarget().invokeExact(); //end invokedynamic strings.forEach(lambda); } 

Comme vous pouvez le voir, LambdaMetafactory est utilisé pour créer un CallSite qui fournit une méthode d'usine qui renvoie un gestionnaire pour la méthode cible. Cette méthode renvoie l'implémentation de l'interface fonctionnelle à l'aide d' invokeExact . S'il y a des variables capturées dans l'expression lambda, invokeExact accepte ces variables comme paramètres réels.


Dans Oracle JRE 8, la méta-usine génère dynamiquement une classe Java à l'aide d'ObjectWeb Asm, qui crée une classe qui implémente une interface fonctionnelle. Des champs supplémentaires peuvent être ajoutés à la classe créée si l'expression lambda capture des variables externes. Celui-ci ressemble à des classes anonymes Java, mais il existe les différences suivantes:


  • Une classe anonyme est générée par le compilateur Java.
  • La classe d'implémentation de l'expression lambda est créée par la JVM au moment de l'exécution.



L'implémentation de Metafactory dépend du fournisseur et de la version de JVM




Bien sûr, l' invokedynamic n'est pas seulement utilisée pour les expressions lambda en Java. Il est principalement utilisé lors de l'exécution de langages dynamiques dans l'environnement JVM. Le moteur JavaScript Nashorn , qui est intégré à Java, fait un usage intensif de cette instruction.


Ensuite, nous nous concentrerons sur la classe LambdaMetafactory et ses capacités. Suivant
La section de cet article suppose que vous comprenez très bien le fonctionnement des méthodes métafactory et ce qu'est MethodHandle


Astuces avec des expressions lambda


Dans cette section, nous montrerons comment créer des lambdas dynamiques à utiliser dans les tâches quotidiennes.


Exceptions vérifiées et lambdas


Ce n'est un secret pour personne que toutes les interfaces fonctionnelles qui existent en Java ne prennent pas en charge les exceptions vérifiées. Les avantages des exceptions vérifiées par rapport aux exceptions régulières sont un débat de longue date (et toujours d'actualité).


Mais que faire si vous devez utiliser du code avec des exceptions vérifiées dans des expressions lambda en combinaison avec des flux Java? Par exemple, vous devez convertir une liste de chaînes en une liste d'URL comme celle-ci:


 Arrays.asList("http://localhost/", "https://github.com").stream() .map(URL::new) .collect(Collectors.toList()) 

Une exception pouvant être levée est déclarée dans le constructeur de l' URL (chaîne) , elle ne peut donc pas être utilisée directement comme référence de méthode dans la classe Functiion .


Vous direz: "Non, peut-être si vous utilisez cette astuce ici":


 public static <T> T uncheckCall(Callable<T> callable) { try { return callable.call(); } catch (Exception e) { return sneakyThrow(e); } } private static <E extends Throwable, T> T sneakyThrow0(Throwable t) throws E { throw (E)t; } public static <T> T sneakyThrow(Throwable e) { return Util.<RuntimeException, T>sneakyThrow0(e); } //   //return s.filter(a -> uncheckCall(a::isActive)) // .map(Account::getNumber) // .collect(toSet()); 

Ceci est un hack sale. Et voici pourquoi:


  • Le bloc try-catch est utilisé.
  • L'exception est de nouveau levée.
  • L'utilisation sale de l'effacement de type en Java.

Le problème peut être résolu d'une manière plus "légale", en utilisant la connaissance des faits suivants:


  • Les exceptions vérifiées sont reconnues uniquement au niveau du compilateur Java.
  • La section throws n'est que des métadonnées pour une méthode sans valeur sémantique au niveau de la JVM.
  • Les exceptions vérifiées et normales sont indiscernables au niveau du bytecode dans la JVM.

La solution consiste à Callable.call méthode Callable.call dans une méthode sans section throws :


 static <V> V callUnchecked(Callable<V> callable){ return callable.call(); } 

Ce code ne se compile pas car la méthode Callable.call déclaré des exceptions vérifiées dans la section throws . Mais nous pouvons supprimer cette section en utilisant une expression lambda construite dynamiquement.


Nous devons d'abord déclarer une interface fonctionnelle qui n'a pas de section throws .
mais qui pourra déléguer l'appel à Callable.call :


 @FunctionalInterface interface SilentInvoker { MethodType SIGNATURE = MethodType.methodType(Object.class, Callable.class);//  INVOKE <V> V invoke(final Callable<V> callable); } 

La deuxième étape consiste à créer une implémentation de cette interface à l'aide de LambdaMetafactory et à déléguer l'appel de la méthode SilentInvoker.invoke méthode SilentInvoker.invoke . Comme mentionné précédemment, la section SilentInvoker.invoke est ignorée au niveau du bytecode, donc la méthode SilentInvoker.invoke peut appeler la méthode SilentInvoker.invoke sans déclarer d'exceptions:


 private static final SilentInvoker SILENT_INVOKER; final MethodHandles.Lookup lookup = MethodHandles.lookup(); final CallSite site = LambdaMetafactory.metafactory(lookup, "invoke", MethodType.methodType(SilentInvoker.class), SilentInvoker.SIGNATURE, lookup.findVirtual(Callable.class, "call", MethodType.methodType(Object.class)), SilentInvoker.SIGNATURE); SILENT_INVOKER = (SilentInvoker) site.getTarget().invokeExact(); 

Troisièmement, nous écrivons une méthode d'assistance qui appelle Callable.call sans déclarer d'exceptions:


 public static <V> V callUnchecked(final Callable<V> callable) /*no throws*/ { return SILENT_INVOKER.invoke(callable); } 

Vous pouvez maintenant réécrire le flux sans aucun problème avec les exceptions vérifiées:


 Arrays.asList("http://localhost/", "https://dzone.com").stream() .map(url -> callUnchecked(() -> new URL(url))) .collect(Collectors.toList()); 

Ce code se compile sans problème car callUnchecked ne déclare pas d'exceptions vérifiées. De plus, l'appel de cette méthode peut être intégré en utilisant la mise en cache monomorphe en ligne , car ce n'est qu'une classe dans l'ensemble de la JVM qui implémente l'interface SilentOnvoker


Si l'implémentation de Callable.call lève une exception au moment de l'exécution, elle sera Callable.call par la fonction appelante sans aucun problème:


 try{ callUnchecked(() -> new URL("Invalid URL")); } catch (final Exception e){ System.out.println(e); } 

Malgré les possibilités de cette méthode, vous devez toujours vous souvenir de la recommandation suivante:




Masquer les exceptions vérifiées avec callUnchecked uniquement si vous êtes sûr que le code appelé ne lèvera aucune exception




L'exemple suivant montre un exemple de cette approche:


 callUnchecked(() -> new URL("https://dzone.com")); // URL        MalformedURLException 

L'implémentation complète de cette méthode est ici , elle fait partie du projet open source SNAMP .


Travailler avec les Getters et Setters


Cette section sera utile pour ceux qui écrivent la sérialisation / désérialisation pour différents formats de données tels que JSON, Thrift, etc. De plus, cela peut être très utile si votre code s'appuie fortement sur la réflexion pour les Getters et Setters dans JavaBeans.


Un getter déclaré dans JavaBean est une méthode nommée getXXX sans paramètres et avec un type de données de retour autre que void . Un setter déclaré dans JavaBean est une méthode nommée setXXX , avec un paramètre et retournant void . Ces deux notations peuvent être représentées comme des interfaces fonctionnelles:


  • Getter peut être représenté par la classe Function , dans laquelle l'argument en est la valeur.
  • Setter peut être représenté par la classe BiConsumer , dans laquelle le premier argument est this -ci et le second est la valeur transmise à Setter.

Nous allons maintenant créer deux méthodes qui peuvent convertir n'importe quel getter ou setter en ces
interfaces fonctionnelles. Et peu importe que les deux interfaces soient génériques. Après avoir effacé les types
le véritable type de données sera Object . La LambdaMetafactory automatique du type de retour et des arguments peut être effectuée à l'aide de LambdaMetafactory . De plus, la bibliothèque Guava aidera à mettre en cache les expressions lambda pour les mêmes getters et setters.


Première étape: créer un cache pour les getters et les setters. La classe Method de l'API Reflection représente un véritable getter ou setter et est utilisée comme clé.
La valeur de cache est une interface fonctionnelle construite dynamiquement pour un getter ou un setter spécifique.


 private static final Cache<Method, Function> GETTERS = CacheBuilder.newBuilder().weakValues().build(); private static final Cache<Method, BiConsumer> SETTERS = CacheBuilder.newBuilder().weakValues().build(); 

Deuxièmement, nous allons créer des méthodes d'usine qui créent une instance de l'interface fonctionnelle basée sur des références à getter ou setter.


 private static Function createGetter(final MethodHandles.Lookup lookup, final MethodHandle getter) throws Exception{ final CallSite site = LambdaMetafactory.metafactory(lookup, "apply", MethodType.methodType(Function.class), MethodType.methodType(Object.class, Object.class), //signature of method Function.apply after type erasure getter, getter.type()); //actual signature of getter try { return (Function) site.getTarget().invokeExact(); } catch (final Exception e) { throw e; } catch (final Throwable e) { throw new Error(e); } } private static BiConsumer createSetter(final MethodHandles.Lookup lookup, final MethodHandle setter) throws Exception { final CallSite site = LambdaMetafactory.metafactory(lookup, "accept", MethodType.methodType(BiConsumer.class), MethodType.methodType(void.class, Object.class, Object.class), //signature of method BiConsumer.accept after type erasure setter, setter.type()); //actual signature of setter try { return (BiConsumer) site.getTarget().invokeExact(); } catch (final Exception e) { throw e; } catch (final Throwable e) { throw new Error(e); } } 

La conversion automatique de type entre les arguments de type Object dans les interfaces fonctionnelles (après l'effacement du type) et les types réels d'arguments et la valeur de retour est obtenue en utilisant la différence entre samMethodType et samMethodType (les troisième et cinquième arguments de la méthode métafactory, respectivement). Le type de l'instance créée de la méthode - c'est la spécialisation de la méthode qui fournit l'implémentation de l'expression lambda.


Troisièmement, nous créerons une façade pour ces usines avec un support pour la mise en cache:


 public static Function reflectGetter(final MethodHandles.Lookup lookup, final Method getter) throws ReflectiveOperationException { try { return GETTERS.get(getter, () -> createGetter(lookup, lookup.unreflect(getter))); } catch (final ExecutionException e) { throw new ReflectiveOperationException(e.getCause()); } } public static BiConsumer reflectSetter(final MethodHandles.Lookup lookup, final Method setter) throws ReflectiveOperationException { try { return SETTERS.get(setter, () -> createSetter(lookup, lookup.unreflect(setter))); } catch (final ExecutionException e) { throw new ReflectiveOperationException(e.getCause()); } } 

Les informations de méthode obtenues à partir d'une instance de la classe Method à l'aide de l'API Java Reflection peuvent facilement être converties en MethodHandle . Gardez à l'esprit que les méthodes d'instance de classe ont toujours un premier argument caché utilisé pour le transmettre à cette méthode. Les méthodes statiques n'ont pas un tel paramètre. Par exemple, la signature réelle de la méthode Integer.intValue() ressemble à int intValue(Integer this) . Cette astuce est utilisée dans notre implémentation de wrappers fonctionnels pour les getters et les setters.


Et maintenant, il est temps de tester le code:


 final Date d = new Date(); final BiConsumer<Date, Long> timeSetter = reflectSetter(MethodHandles.lookup(), Date.class.getDeclaredMethod("setTime", long.class)); timeSetter.accept(d, 42L); //the same as d.setTime(42L); final Function<Date, Long> timeGetter = reflectGetter(MethodHandles.lookup(), Date.class.getDeclaredMethod("getTime")); System.out.println(timeGetter.apply(d)); //the same as d.getTime() //output is 42 

Cette approche avec des getters et setters mis en cache peut être efficacement utilisée dans les bibliothèques de sérialisation / désérialisation (telles que Jackson) qui utilisent des getters et des setters pendant la sérialisation et la désérialisation.




L'appel d'interfaces fonctionnelles avec des implémentations générées dynamiquement à l'aide de LambdaMetaFactory beaucoup plus rapide que l'appel via l'API Java Reflection




La version complète du code peut être trouvée ici , elle fait partie de la bibliothèque SNAMP .


Limitations et bugs


Dans cette section, nous examinerons certains bogues et limitations associés aux expressions lambda dans le compilateur Java et la JVM. Toutes ces limitations peuvent être reproduites dans OpenJDK et Oracle JDK avec la version javac 1.8.0_131 pour Windows et Linux.


Création d'expressions lambda à partir de gestionnaires de méthodes


Comme vous le savez, une expression lambda peut être construite dynamiquement à l'aide de LambdaMetaFactory . Pour ce faire, vous devez définir un gestionnaire - la classe MethodHandle , qui indique l'implémentation de la seule méthode définie dans l'interface fonctionnelle. Jetons un œil à cet exemple simple:


 final class TestClass { String value = ""; public String getValue() { return value; } public void setValue(final String value) { this.value = value; } } final TestClass obj = new TestClass(); obj.setValue("Hello, world!"); final MethodHandles.Lookup lookup = MethodHandles.lookup(); final CallSite site = LambdaMetafactory.metafactory(lookup, "get", MethodType.methodType(Supplier.class, TestClass.class), MethodType.methodType(Object.class), lookup.findVirtual(TestClass.class, "getValue", MethodType.methodType(String.class)), MethodType.methodType(String.class)); final Supplier<String> getter = (Supplier<String>) site.getTarget().invokeExact(obj); System.out.println(getter.get()); 

Ce code équivaut à:


 final TestClass obj = new TestClass(); obj.setValue("Hello, world!"); final Supplier<String> elementGetter = () -> obj.getValue(); System.out.println(elementGetter.get()); 

Mais que se passe-t-il si nous remplaçons le gestionnaire de méthode qui pointe vers getValue par le gestionnaire que les champs getter représentent:


 final CallSite site = LambdaMetafactory.metafactory(lookup, "get", MethodType.methodType(Supplier.class, TestClass.class), MethodType.methodType(Object.class), lookup.findGetter(TestClass.class, "value", String.class), //field getter instead of method handle to getValue MethodType.methodType(String.class)); 

Ce code devrait, comme prévu, fonctionner car findGetter renvoie un gestionnaire qui pointe vers les champs getter et a la signature correcte. Mais, si vous exécutez ce code, vous verrez l'exception suivante:


 java.lang.invoke.LambdaConversionException: Unsupported MethodHandle kind: getField 

Fait intéressant, le getter pour le champ fonctionne correctement si nous utilisons MethodHandleProxies :


 final Supplier<String> getter = MethodHandleProxies .asInterfaceInstance(Supplier.class, lookup.findGetter(TestClass.class, "value", String.class) .bindTo(obj)); 

Il convient de noter que MethodHandleProxies n'est pas un bon moyen de créer dynamiquement des expressions lambda, car cette classe enveloppe simplement MethodHandle dans une classe proxy et délègue invocationHandler.invoke à MethodHandle.invokeWithArguments . Cette approche utilise Java Reflection et est très lente.


Comme indiqué précédemment, tous les gestionnaires de méthodes ne peuvent pas être utilisés pour créer des expressions lambda au moment de l'exécution.




Seuls quelques types de gestionnaires de méthodes peuvent être utilisés pour créer dynamiquement des expressions lambda.




Les voici:


  • REF_invokeInterface: peut être créé à l'aide de Lookup.findVirtual pour les méthodes d'interface
  • REF_invokeVirtual: peut être créé à l'aide de Lookup.findVirtual pour les méthodes virtuelles de classe
  • REF_invokeStatic: créé à l'aide de Lookup.findStatic pour les méthodes statiques
  • REF_newInvokeSpecial: peut être créé à l'aide de Lookup.findConstructor pour les constructeurs
  • REF_invokeSpecial: peut être créé à l'aide de Lookup.findSpecial
    pour les méthodes privées et la liaison anticipée avec les méthodes virtuelles de classe

D'autres types de gestionnaires LambdaConversionException erreur LambdaConversionException .


Exceptions génériques


Ce bogue est lié au compilateur Java et à la possibilité de déclarer des exceptions génériques dans la section des throws . L'exemple de code suivant illustre ce comportement:


 interface ExtendedCallable<V, E extends Exception> extends Callable<V>{ @Override V call() throws E; } final ExtendedCallable<URL, MalformedURLException> urlFactory = () -> new URL("http://localhost"); urlFactory.call(); 

Ce code doit être compilé car le constructeur de la classe URL lève une MalformedURLException . Mais il ne compile pas. Le message d'erreur suivant s'affiche:


 Error:(46, 73) java: call() in <anonymous Test$CODEgt; cannot implement call() in ExtendedCallable overridden method does not throw java.lang.Exception 

Mais, si nous remplaçons l'expression lambda par une classe anonyme, le code compile:


 final ExtendedCallable<URL, MalformedURLException> urlFactory = new ExtendedCallable<URL, MalformedURLException>() { @Override public URL call() throws MalformedURLException { return new URL("http://localhost"); } }; urlFactory.call(); 

Il en résulte:




L'inférence de type pour les exceptions génériques ne fonctionne pas correctement en combinaison avec les expressions lambda




Limitations du type de paramétrage


Vous pouvez construire un objet générique avec plusieurs restrictions de type en utilisant le signe & : <T extends A & B & C & ... Z> .
Cette méthode de détermination des paramètres génériques est rarement utilisée, mais affecte d'une certaine manière les expressions lambda en Java en raison de certaines restrictions:


  • Chaque contrainte de type, sauf la première, doit être une interface.
  • Une version pure d'une classe avec un tel générique ne prend en compte que la première contrainte de type de la liste.

La deuxième limitation conduit à différents comportements du code au moment de la compilation et au moment de l'exécution, lors de la liaison à l'expression lambda. Cette différence peut être démontrée à l'aide du code suivant:


 final class MutableInteger extends Number implements IntSupplier, IntConsumer { //mutable container of int value private int value; public MutableInteger(final int v) { value = v; } @Override public int intValue() { return value; } @Override public long longValue() { return value; } @Override public float floatValue() { return value; } @Override public double doubleValue() { return value; } @Override public int getAsInt() { return intValue(); } @Override public void accept(final int value) { this.value = value; } } static <T extends Number & IntSupplier> OptionalInt findMinValue(final Collection <T> values) { return values.stream().mapToInt(IntSupplier::getAsInt).min(); } final List <MutableInteger> values = Arrays.asList(new MutableInteger(10), new MutableInteger(20)); final int mv = findMinValue(values).orElse(Integer.MIN_VALUE); System.out.println(mv); 

Ce code est absolument correct et se compile avec succès. La classe MutableInteger satisfait les contraintes du type générique T:


  • MutableInteger hérite de Number .
  • MutableInteger implémente IntSupplier .

Mais le code plantera avec une exception lors de l'exécution:


 java.lang.BootstrapMethodError: call site initialization exception at java.lang.invoke.CallSite.makeSite(CallSite.java:341) at java.lang.invoke.MethodHandleNatives.linkCallSiteImpl(MethodHandleNatives.java:307) at java.lang.invoke.MethodHandleNatives.linkCallSite(MethodHandleNatives.java:297) at Test.minValue(Test.java:77) Caused by: java.lang.invoke.LambdaConversionException: Invalid receiver type class java.lang.Number; not a subtype of implementation type interface java.util.function.IntSupplier at java.lang.invoke.AbstractValidatingLambdaMetafactory.validateMetafactoryArgs(AbstractValidatingLambdaMetafactory.java:233) at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:303) at java.lang.invoke.CallSite.makeSite(CallSite.java:302) 

Cela se produit car le pipeline JavaStream capture uniquement un type pur, qui, dans notre cas, est la classe Number et IntSupplier pas l'interface IntSupplier . Ce problème peut être résolu en déclarant explicitement le type de paramètre dans une méthode distincte, utilisée comme référence à la méthode:


 private static int getInt(final IntSupplier i){ return i.getAsInt(); } private static <T extends Number & IntSupplier> OptionalInt findMinValue(final Collection<T> values){ return values.stream().mapToInt(UtilsTest::getInt).min(); } 

Cet exemple montre une inférence de type incorrecte dans le compilateur et le runtime.




La gestion de plusieurs restrictions de types de paramètres génériques conjointement avec l'utilisation d'expressions lambda au moment de la compilation et à l'exécution n'est pas cohérente



Source: https://habr.com/ru/post/fr432418/


All Articles