众所周知,反射是一种在运行时维护和修改代码行为的灵活但缓慢的方法。
但是如果我们必须使用这样的功能,那么在 Java 中是否有比反射 API 更快的编程技术来进行动态修改?这些反对反思的替代方案的优缺点是什么?
众所周知,反射是一种在运行时维护和修改代码行为的灵活但缓慢的方法。
但是如果我们必须使用这样的功能,那么在 Java 中是否有比反射 API 更快的编程技术来进行动态修改?这些反对反思的替代方案的优缺点是什么?
反射的一种替代方法是动态生成类文件。这个生成的类应该执行所需的操作,例如调用在运行时发现的方法,并interface
在编译时实现一个已知的方法,以便可以使用该接口以非反射方式调用生成的方法。有一个问题:如果适用,Reflection 在内部执行相同的技巧。这在特殊情况下不起作用,例如在调用private
方法时,因为您无法生成调用它的合法类文件。因此,在反射实现中有不同类型的调用处理程序,使用生成的代码或本机代码。你不能打败那个。
但更重要的是,Reflection 会对每次调用进行安全检查。因此,您生成的类将仅在加载和实例化时进行检查,这可能是一个巨大的胜利。或者,您可以调用setAccessible(true)
一个Method
实例来关闭安全检查。然后只剩下自动装箱和可变参数数组创建的轻微性能损失。
由于Java 7存在两者的替代方案,MethodHandle
. 最大的优势是,与其他两个不同,它甚至可以在安全受限的环境中工作。a 的访问检查MethodHandle
在获取它时执行,但在调用它时不执行。它具有所谓的“多态签名”,这意味着您可以使用任意参数类型调用它,而无需自动装箱或创建数组。当然,错误的参数类型会创建一个合适的RuntimeException
.
(更新)在Java 8中,可以选择在运行时使用 lambda 表达式和方法参考语言的后端功能。这个后端完全按照开头描述的那样做,动态生成一个类来实现interface
你的代码,当它在编译时知道时可以直接调用。确切的机制是特定于实现的,因此未定义,但您可以假设实现将尽力使调用尽可能快。Oracle 的 JRE 的当前实现完美地做到了这一点。这不仅使您免于生成这样一个访问器类的负担,而且它还能够做您永远做不到的事情——甚至调用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;
}
}
在我的 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
不需要处理的程度,除非你使用它来执行 lambda 技巧,这显然优于所有反射替代方案,这并不奇怪,因为它只是一个直接调用(嗯,几乎是:一级间接)。请注意,我创建目标方法private
是为了演示有效调用偶数private
方法的能力。
与往常一样,我必须指出这个基准的简单性以及它的人工性。但我认为,趋势是清晰可见的,更重要的是,结果是可以令人信服地解释的。
我创建了一个名为lambda-factory的小型库。它基于 LambdaMetafactory,但省去了查找或创建与方法匹配的接口的麻烦。
以下是 10E8 迭代的一些示例运行时(可使用类 PerformanceTest 重现):
Lambda: 0.02s, Direct: 0.01s, Reflection: 4.64s for method(int, int)
Lambda: 0.03s, Direct: 0.02s, Reflection: 3.23s for method(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!
请注意,在调用 lambda 时,您必须选择在其名称中包含目标方法的返回类型的调用方法。- 可变参数和自动装箱太贵了。
在上面的示例中,选择的invoke_for_float
方法表明我们正在调用一个方法,该方法返回一个浮点数。如果您尝试访问的方法返回 fx 字符串、盒装原语(整数、布尔值等)或一些自定义对象,您可以调用invoke_for_Object
.
该项目是试验 LambdaMetafactory 的一个很好的模板,因为它包含各个方面的工作代码:
反射的替代方法是使用接口。刚刚取自Joshua Bloch 的 Effective 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);
}
虽然这个程序只是一个玩具,但它展示的技术非常强大。玩具程序可以很容易地变成一个通用的集合测试器,它通过积极地操作一个或多个实例并检查它们是否遵守集合契约来验证指定的集合实现。同样,它可以变成一个通用的集合性能分析工具。事实上,该技术足以实现一个成熟的服务提供者框架。大多数时候,这种技术就是你在反思中所需要的。
这个例子展示了反射的两个缺点。首先,该示例可以生成三个运行时错误,如果不使用反射实例化,所有这些都将是编译时错误。其次,需要 20 行繁琐的代码才能从类的名称生成一个类的实例,而构造函数调用则可以整齐地放在一行中。然而,这些缺点仅限于实例化对象的程序部分。一旦实例化,它就与任何其他 Set 实例没有区别。