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