ご存じのとおり、リフレクションは柔軟ですが、実行時のコードの動作を維持および変更するためのslowメソッドです。
しかし、このような機能を使用する必要がある場合、動的変更のためのReflection APIと比較してJavaでより高速なプログラミング技術がありますか?これらの代替案の長所と短所は何ですか?
Reflectionの1つの代替方法は、クラスファイルを動的に生成することです。この生成されたクラスは、目的のアクションを実行する必要があります。実行時に検出されたメソッドを呼び出し、コンパイル時に既知のinterface
を実装して、そのインターフェースを使用して生成されたメソッドを非反射的な方法で呼び出すことができるようにします。落とし穴が1つあります。該当する場合、Reflectionは内部で同じトリックを行います。これは、特別な場合、たとえばprivate
メソッドを呼び出すとき、それを呼び出す有効なクラスファイルを生成できないため。そのため、Reflectionの実装には、生成コードまたはネイティブコードのいずれかを使用するさまざまな種類の呼び出しハンドラーがあります。あなたはそれを打ち負かすことはできません。
しかし、より重要なのは、Reflectionがすべての呼び出しでセキュリティチェックを行うことです。そのため、生成されたクラスは、ロードとインスタンス化でのみチェックされ、大きなメリットが得られます。ただし、代わりに、Method
インスタンスでsetAccessible(true)
を呼び出して、セキュリティチェックを有効にすることもできます。その後、オートボクシングと可変引数配列の作成によるわずかなパフォーマンスの低下のみが残ります。
Java 7の両方に代わるものがあるため、MethodHandle
。大きな利点は、他の2つとは異なり、セキュリティが制限された環境でも機能することです。 MethodHandle
のアクセスチェックは、取得時には実行されますが、呼び出し時には実行されません。いわゆる「ポリモーフィックシグネチャ」があります。つまり、オートボクシングや配列作成なしで、任意の引数タイプで呼び出すことができます。もちろん、間違った引数タイプは適切なRuntimeException
を作成します。
(Update)Java 8では、ラムダ式とメソッド参照言語のバックエンドを使用するオプションがあります実行時の機能。このバックエンドは最初に説明したことを正確に行い、コンパイル時に既知のコードが直接呼び出す可能性のあるinterface
を実装するクラスを動的に生成します。正確なメカニズムは実装固有であるため未定義ですが、実装はできる限り高速に呼び出しを行うことが最善であると見なすことができます。オラクルの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
を処理する必要がない程度に改善されていることを明確に示しています。それは単なる直接的な呼び出しであるためです(まあ、ほぼ1レベルの間接参照)。ターゲットメソッド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!
ラムダを呼び出すときは、名前にターゲットメソッドの戻り値の型を含む呼び出しメソッドを選択する必要があることに注意してください。 -可変引数と自動ボクシングは高すぎました。
上記の例では、選択されたinvoke_for_float
メソッドは、floatを返すメソッドを呼び出していることを示しています。アクセスしようとしているメソッドが文字列、ボックス化されたプリミティブ(整数、ブールなど)またはカスタムオブジェクトを返す場合、invoke_for_Object
を呼び出します。
このプロジェクトは、LambdaMetafactoryのさまざまな側面の作業コードが含まれているため、実験に適したテンプレートです。
リフレクションの代替手段はインターフェイスを使用することです。 Effective Java by Joshua Bloch。
非常に限られた形式でのみ使用することで、リフレクションの多くのメリットを得ることができ、コストもほとんどかかりません。コンパイル時に使用できないクラスを使用する必要がある多くのプログラムでは、コンパイル時にクラスを参照するための適切なインターフェイスまたはスーパークラスが存在します。この場合、リフレクションを使用してインスタンスを作成し、インターフェイスまたはスーパークラスを介して通常どおりアクセスできます。適切なコンストラクターにパラメーターがない場合、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);
}
このプログラムは単なるおもちゃですが、このプログラムが示す手法は非常に強力です。おもちゃプログラムは、1つ以上のインスタンスを積極的に操作し、それらがSetコントラクトに従うことを確認することで、指定されたSet実装を検証する汎用セットテスターに簡単に変更できます。同様に、汎用のセットパフォーマンス分析ツールに変えることもできます。実際、この手法は本格的なサービスプロバイダーフレームワークを実装するのに十分強力です。ほとんどの場合、この手法は、反射の方法で必要なものすべてです。
この例は、反射の2つの欠点を示しています。まず、この例では3つのランタイムエラーを生成できますが、リフレクションのインスタンス化を使用しなかった場合はすべてコンパイル時エラーになります。第二に、名前からクラスのインスタンスを生成するには、20行の退屈なコードが必要ですが、コンストラクターの呼び出しは1行にきちんと収まります。ただし、これらの欠点は、オブジェクトをインスタンス化するプログラムの一部に限定されます。インスタンス化されると、他のSetインスタンスと区別できなくなります。