web-dev-qa-db-fra.com

Alternatives plus rapides à la réflexion de Java

Comme nous le savons, la réflexion est une méthode flexible mais lente pour maintenir et modifier le comportement du code lors de l'exécution.

Mais si nous devons utiliser une telle fonctionnalité, existe-t-il des techniques de programmation plus rapides dans Java par rapport à l'API Reflection pour les modifications dynamiques? Quels sont les avantages et les inconvénients de ces alternatives contre la réflexion?

55
ovunccetin

Une alternative à Reflection est de générer dynamiquement un fichier de classe. Cette classe générée doit effectuer l'action souhaitée, par exemple invoque la méthode découverte lors de l'exécution et implémente un interface connu au moment de la compilation afin qu'il soit possible d'appeler la méthode générée de manière non réfléchie à l'aide de cette interface. Il y a un hic: le cas échéant, Reflection fait la même astuce en interne. Cela ne fonctionne pas dans des cas particuliers, par ex. lorsque vous appelez une méthode private car vous ne pouvez pas générer un fichier de classe légale en l'invoquant. Ainsi, dans l'implémentation de Reflection, il existe différents types de gestionnaires d'invocation, utilisant soit du code généré soit du code natif. Vous ne pouvez pas battre ça.

Mais le plus important est que Reflection effectue des contrôles de sécurité à chaque appel. Ainsi, votre classe générée sera vérifiée uniquement lors du chargement et de l'instanciation, ce qui peut être un gros gain. Mais vous pouvez également appeler setAccessible(true) sur une instance Method pour désactiver les contrôles de sécurité. Il ne reste alors que la perte de performances mineure de la création de tableaux automatiques et de tableaux varargs.

Puisque Java 7 il existe une alternative aux deux, le MethodHandle. Le gros avantage est que, contrairement aux deux autres, il fonctionne même dans des environnements à sécurité limitée. Les vérifications d'accès pour un MethodHandle sont effectuées lors de son acquisition mais pas lors de son appel. Il a la soi-disant "signature polymorphe", ce qui signifie que vous pouvez l'invoquer avec des types d'arguments arbitraires sans mise en boîte automatique ni création de tableau. Bien sûr, de mauvais types d'arguments créeront un RuntimeException approprié.

( Mise à jour ) Avec Java 8, il est possible d'utiliser le back-end de l'expression lambda et du langage de référence de méthode fonctionnalité à l'exécution. Ce backend fait exactement la chose décrite au début, générant une classe dynamiquement qui implémente un interface que votre code peut appeler directement quand il est connu au moment de la compilation. La mécanique exacte est spécifique à l'implémentation, donc indéfinie, mais vous pouvez supposer que l'implémentation fera de son mieux pour rendre l'invocation aussi rapide que possible. La mise en œuvre actuelle du JRE d'Oracle le fait parfaitement. Non seulement cela vous évite d'avoir à générer une telle classe d'accesseur, mais il est également capable de faire ce que vous ne pourriez jamais faire: invoquer même les méthodes private via le code généré. J'ai mis à jour l'exemple pour inclure cette solution. Cet exemple utilise un interface standard qui existe déjà et qui possède la signature de méthode souhaitée. Si une telle interface n'existe pas, vous devez créer votre propre interface fonctionnelle d'accesseur avec une méthode avec la bonne signature. Mais, bien sûr, maintenant l'exemple de code nécessite Java 8 pour s'exécuter.

Voici un exemple de référence simple:

import Java.lang.invoke.LambdaMetafactory;
import Java.lang.invoke.MethodHandle;
import Java.lang.invoke.MethodHandles;
import Java.lang.invoke.MethodType;
import Java.lang.reflect.Method;
import Java.util.function.IntBinaryOperator;

public class TestMethodPerf
{
  private static final int ITERATIONS = 50_000_000;
  private static final int WARM_UP = 10;

  public static void main(String... args) throws Throwable
  {
 // hold result to prevent too much optimizations
    final int[] dummy=new int[4];

    Method reflected=TestMethodPerf.class
      .getDeclaredMethod("myMethod", int.class, int.class);
    final MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodHandle mh=lookup.unreflect(reflected);
    IntBinaryOperator lambda=(IntBinaryOperator)LambdaMetafactory.metafactory(
      lookup, "applyAsInt", MethodType.methodType(IntBinaryOperator.class),
      mh.type(), mh, mh.type()).getTarget().invokeExact();

    for(int i=0; i<WARM_UP; i++)
    {
      dummy[0]+=testDirect(dummy[0]);
      dummy[1]+=testLambda(dummy[1], lambda);
      dummy[2]+=testMH(dummy[1], mh);
      dummy[3]+=testReflection(dummy[2], reflected);
    }
    long t0=System.nanoTime();
    dummy[0]+=testDirect(dummy[0]);
    long t1=System.nanoTime();
    dummy[1]+=testLambda(dummy[1], lambda);
    long t2=System.nanoTime();
    dummy[2]+=testMH(dummy[1], mh);
    long t3=System.nanoTime();
    dummy[3]+=testReflection(dummy[2], reflected);
    long t4=System.nanoTime();
    System.out.printf("direct: %.2fs, lambda: %.2fs, mh: %.2fs, reflection: %.2fs%n",
      (t1-t0)*1e-9, (t2-t1)*1e-9, (t3-t2)*1e-9, (t4-t3)*1e-9);

    // do something with the results
    if(dummy[0]!=dummy[1] || dummy[0]!=dummy[2] || dummy[0]!=dummy[3])
      throw new AssertionError();
  }

  private static int testMH(int v, MethodHandle mh) throws Throwable
  {
    for(int i=0; i<ITERATIONS; i++)
      v+=(int)mh.invokeExact(1000, v);
    return v;
  }

  private static int testReflection(int v, Method mh) throws Throwable
  {
    for(int i=0; i<ITERATIONS; i++)
      v+=(int)mh.invoke(null, 1000, v);
    return v;
  }

  private static int testDirect(int v)
  {
    for(int i=0; i<ITERATIONS; i++)
      v+=myMethod(1000, v);
    return v;
  }

  private static int testLambda(int v, IntBinaryOperator accessor)
  {
    for(int i=0; i<ITERATIONS; i++)
      v+=accessor.applyAsInt(1000, v);
    return v;
  }

  private static int myMethod(int a, int b)
  {
    return a<b? a: b;
  }
}

L'ancien programme imprimé dans ma configuration Java 7: direct: 0,03s, mh: 0,32s, reflection: 1,05s qui suggère que MethodHandle est une bonne alternative. Maintenant, le programme mis à jour fonctionnant sous Java 8 sur la même machine a imprimé direct: 0,02s, lambda: 0,02s, mh: 0,35s, reflection: 0,40s qui montre clairement que les performances de réflexion ont été améliorées à un point tel qu'il pourrait être inutile de traiter avec MethodHandle, à moins que vous ne l'utilisiez pour faire l'astuce lambda, qui surpasse clairement toutes les alternatives réfléchissantes, ce qui n'est pas surprenant, car ce n'est qu'un appel direct (enfin, presque: un niveau d'indirection). Notez que j'ai créé la méthode cible private pour démontrer la capacité d'appeler des méthodes even private efficacement.

Comme toujours, je dois souligner la simplicité de cette référence et son caractère artificiel. Mais je pense que la tendance est clairement visible et encore plus importante, les résultats sont explicables de manière convaincante.

122
Holger

J'ai créé une petite bibliothèque appelée lambda-factory . Il est basé sur LambdaMetafactory, mais vous évite les tracas de trouver ou de créer une interface qui correspond à la méthode.

Voici quelques exemples d'exécutions pour les itérations 10E8 (reproductibles avec la classe PerformanceTest):

Lambda: 0,02 s, Direct: 0,01 s, Réflexion: 4,64 s pour la méthode (int, int)
Lambda: 0,03 s, Direct: 0,02 s, Réflexion: 3,23 s pour la méthode (Object, int)

Disons que nous avons une classe appelée MyClass, qui définit les méthodes suivantes:

private static String myStaticMethod(int a, Integer b){ /*some logic*/ }
private float myInstanceMethod(String a, Boolean b){ /*some logic*/ }

Nous pouvons accéder à ces méthodes comme ceci:

Method method = MyClass.class.getDeclaredMethod("myStaticMethod", int.class, Integer.class); //Regular reflection call
Lambda lambda = LambdaFactory.create(method);  
String result = (String) lambda.invoke_for_Object(1000, (Integer) 565); //Don't rely on auto boxing of arguments!

Method method = MyClass.class.getDeclaredMethod("myInstanceMethod", String.class, Boolean.class);
Lambda lambda = LambdaFactory.create(method);
float result = lambda.invoke_for_float(new MyClass(), "Hello", (Boolean) null);  //No need to cast primitive results!

Notez que lorsque vous appelez le lambda, vous devez choisir une méthode d'appel qui contient le type de retour de la méthode cible dans son nom. - les varargs et la boxe auto étaient trop chers.

Dans l'exemple ci-dessus, la méthode choisie invoke_for_float Indique que nous invoquons une méthode qui renvoie un flottant. Si la méthode à laquelle vous essayez d'accéder renvoie fx une chaîne, une primitive encadrée (entier, booléen, etc.) ou un objet personnalisé, vous appellerez invoke_for_Object.

Le projet est un bon modèle pour expérimenter avec LambdaMetafactory car il contient du code de travail pour divers aspects:

  1. appels statiques et appels d'instance
  2. Accès aux méthodes privées et aux méthodes d'autres packages
  3. Logique `` invokeSpecial '', c'est-à-dire lorsque l'implémentation créée est telle qu'elle contourne la répartition de méthode dynamique.
15
Hervian

L'alternative de réflexion utilise Interface. Prenant juste de Efficace Java par Joshua Bloch.

Nous pouvons obtenir de nombreux avantages de la réflexion tout en encourant peu de ses coûts en l'utilisant uniquement sous une forme très limitée. Pour de nombreux programmes qui doivent utiliser une classe qui n'est pas disponible au moment de la compilation, il existe au moment de la compilation une interface ou une superclasse appropriée permettant de se référer à la classe. Si tel est le cas, vous pouvez créer des instances de manière réfléchie et y accéder normalement via leur interface ou superclasse. Si le constructeur approprié n'a pas de paramètres, vous n'avez même pas besoin d'utiliser Java.lang.reflect; la méthode Class.newInstance fournit les fonctionnalités requises.

Utilisez la réflexion uniquement pour créer l'objet, c'est-à-dire.

// Reflective instantiation with interface access
   public static void main(String[] args) {
       // Translate the class name into a Class object
       Class<?> cl = null;
       try {
           cl = Class.forName(args[0]);
       } catch(ClassNotFoundException e) {
           System.err.println("Class not found.");
           System.exit(1);
       }
       // Instantiate the class
       Set<String> s = null;
       try {
           s = (Set<String>) cl.newInstance();
       } catch(IllegalAccessException e) {
           System.err.println("Class not accessible.");
           System.exit(1);
       } catch(InstantiationException e) {
           System.err.println("Class not instantiable.");
           System.exit(1);
       }
       // Exercise the set
       s.addAll(Arrays.asList(args).subList(1, args.length));
       System.out.println(s);
}

Bien que ce programme ne soit qu'un jouet, la technique qu'il démontre est très puissante. Le programme de jouets pourrait facilement être transformé en un testeur d'ensemble générique qui valide l'implémentation d'ensemble spécifiée en manipulant agressivement une ou plusieurs instances et en vérifiant qu'elles respectent le contrat d'ensemble. De même, il pourrait être transformé en un outil générique d'analyse des performances d'ensemble. En fait, la technique est suffisamment puissante pour implémenter un cadre complet de fournisseur de services. La plupart du temps, cette technique est tout ce dont vous avez besoin pour réfléchir.

Cet exemple montre deux inconvénients de la réflexion. Tout d'abord, l'exemple peut générer trois erreurs d'exécution, qui auraient toutes été des erreurs de compilation si l'instanciation réfléchie n'avait pas été utilisée. Deuxièmement, il faut vingt lignes de code fastidieux pour générer une instance de la classe à partir de son nom, alors qu'une invocation de constructeur conviendrait parfaitement sur une seule ligne. Ces inconvénients sont cependant limités à la partie du programme qui instancie l'objet. Une fois instancié, il est impossible de le distinguer de toute autre instance de Set.

6
Trying