Подтвердить что ты не робот

Более быстрые альтернативы отражению Java

Как мы знаем, отражение - это гибкий, но медленный метод для поддержания и изменения поведения кода во время выполнения.

Но если нам нужно использовать такую ​​функциональность, есть ли какие-либо более быстрые методы программирования в Java по сравнению с Reflection API для динамических изменений? Каковы плюсы и минусы этих альтернатив против отражения?

4b9b3361

Ответ 1

Один из вариантов Reflection - генерировать файл класса динамически. Этот сгенерированный класс должен выполнять требуемое действие, например. вызывает метод, обнаруженный во время выполнения, и реализует interface, известный во время компиляции, так что его можно вызвать сгенерированный метод не отражающим образом с использованием этого интерфейса. Theres один catch: если применимо, Reflection делает тот же трюк внутри. Это не работает в особых случаях, например. при вызове метода private, поскольку вы не можете создать файл юридического класса, вызывающий его. Таким образом, в реализации Reflection существуют разные типы обработчиков вызовов, используя либо сгенерированный код, либо собственный код. Вы не можете победить.

Но более важно то, что Reflection выполняет проверки безопасности при каждом вызове. Таким образом, ваш сгенерированный класс будет проверяться только при загрузке и создании экземпляров, что может стать большой победой. Но в качестве альтернативы вы можете вызвать setAccessible(true) в экземпляре Method, чтобы включить проверки безопасности. Тогда остается только незначительная потеря производительности при создании автобоксинга и массиве varargs.

Так как Java 7 есть альтернатива обоим, MethodHandle. Большим преимуществом является то, что, в отличие от двух других, он даже работает в среде с ограничениями безопасности. Проверки доступа для MethodHandle выполняются при его приобретении, но не при его вызове. Он имеет так называемую "полиморфную подпись", что означает, что вы можете ссылаться на нее с произвольными типами аргументов без автоматического бокса или создания массива. Конечно, неправильные типы аргументов создадут соответствующий RuntimeException.

(Обновление) С помощью Java 8 существует возможность использовать фоновый интерфейс выражения языка lambda и язык ссылки на метод во время выполнения. Этот бэкэнд выполняет именно то, что описано в начале, генерируя класс динамически, который реализует interface, который ваш код может вызывать непосредственно, когда он известен во время компиляции. Точная механика специфична для реализации, поэтому undefined, но вы можете предположить, что реализация попытается изо всех сил сделать вызов как можно быстрее. Нынешняя реализация Oracles JRE делает это отлично. Мало того, что это избавляет вас от бремени генерации такого класса accessor, оно также способно делать то, что вы никогда не могли сделать, - вызывать даже private методы с помощью сгенерированного кода. Я обновил пример, чтобы включить это решение. В этом примере используется стандартный interface, который уже существует и имеет желаемую подпись метода. Если такого соответствия interface не существует, вам необходимо создать свой собственный интерфейс доступа с помощью метода с правильной подписью. Но, конечно, теперь код примера требует запуска Java 8.

Вот простой пример:

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;
  }
}

Th старой программы, напечатанной в моей настройке Java 7: direct: 0,03s, mh: 0,32s, reflection: 1,05s, которая предположила, что MethodHandle была хорошей альтернативой. Теперь обновленная программа, работающая под Java 8 на том же компьютере, напечатанном direct: 0,02s, lambda: 0,02s, mh: 0,35s, reflection: 0,40s, которая наглядно показывает, что производительность отражения была улучшена до такой степени, что это может сделать ненужным работу с MethodHandle, если вы не используете ее для выполнения лямбда-трюка, что явно превосходит все отражающие альтернативы, что не вызывает удивления, поскольку это просто прямой вызов (ну, почти: один уровень косвенности). Обратите внимание, что я сделал целевой метод private, чтобы продемонстрировать возможность эффективного вызова методов private.

Как всегда, я должен указать на простоту этого теста и насколько он искусственен. Но я думаю, что эта тенденция четко видна и даже более важна, результаты убедительно объяснимы.

Ответ 2

Я создал небольшую библиотеку под названием lambda-factory. Он основан на LambdaMetafactory, но сэкономит вам трудность поиска или создания интерфейса, соответствующего методу.

Вот несколько примеров времени выполнения для итераций 10E8 (воспроизводимых с помощью класса PerformanceTest):

Lambda: 0.02s, Direct: 0.01s, Reflection: 4.64s для метода (int, int)
Lambda: 0.03s, Direct: 0,02s, Reflection: 3.23s для метода (Object, int)

Скажем, у нас есть класс под названием MyClass, который определяет следующие методы:

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

Мы можем получить доступ к следующим методам:

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!

Обратите внимание, что при вызове лямбда вы должны выбрать метод вызова, содержащий тип возвращаемого метода в его имени. - varargs и auto boxing были слишком дорогими.

В приведенном выше примере выбранный метод invoke_for_float указывает, что мы вызываем метод, который возвращает float. Если метод, к которому вы пытаетесь получить доступ, возвращает fx a String, примитив в штучной упаковке (Integer, Boolean и т.д.) Или какой-либо пользовательский объект, вы должны называть invoke_for_Object.

Проект является хорошим шаблоном для экспериментов с LambdaMetafactory, поскольку он содержит рабочий код для различных аспектов:

  • статические вызовы и вызовы экземпляров
  • Доступ к закрытым методам и методам из других пакетов
  • 'invokeSpecial', т.е. когда созданная реализация такова, что она обходит отправку динамических методов.

Ответ 3

Альтернативой для отражения является использование интерфейса. Просто взяв из Эффективная Java от Джошуа Блоха.

Мы можем получить много преимуществ отражения, в то время как несколько из его затрат, используя его только в очень ограниченной форме. Для многих программы, которые должны использовать класс, который недоступен во время компиляции, во время компиляции существует соответствующий интерфейс или суперкласс которые относятся к классу. Если это так, вы можете создать экземпляры отражаются и обычно получают доступ к ним через интерфейс или суперкласс. Если соответствующий конструктор не имеет параметров, то вы даже не нужно использовать java.lang.reflect; метод Class.newInstance обеспечивает требуемую функциональность.

Используйте отражение только для создания объекта, т.е.

// 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);
}

Хотя эта программа является просто игрушкой, техника, которую она демонстрирует, очень могущественный. Игрушечную программу можно было легко превратить в общий установить тестер, который проверяет указанную агрессивно манипулируя одним или несколькими экземплярами и проверяя, что они подчиняться контракту "Комплект". Аналогично, его можно было бы превратить в общий установите инструмент анализа производительности. На самом деле техника достаточно чтобы реализовать полномасштабную инфраструктуру поставщика услуг. Наиболее времени, эта техника - все, что вам нужно на отражение.

Этот пример демонстрирует два недостатка отражения. Во-первых, пример может генерировать три ошибки времени выполнения, все из которых будут иметь были ошибки времени компиляции, если рефлексивный экземпляр не использовался. Во-вторых, для генерации экземпляра требуется двадцать строк утомительного кода класса от его имени, тогда как вызов конструктора аккуратно вписывается в одну строку. Эти недостатки, однако, ограничивается частью программы, которая создает экземпляр объекта. После создания экземпляра он неотличим от любого другого набора экземпляр.