Java リフレクション アプリケーションと最適化

目次

1. Java リフレクションとは:

2. 反射アプリケーションの例:

3. どのような状況で反省が必要ですか?

1. コードを動的にロードして実行する

3. 柔軟な枠組みを構築する

4. シリアル化と逆シリアル化

5. プラグインのアーキテクチャ

4. 反射の最適化

1. リフレクションを頻繁に呼び出さないようにする

2. キャッシュ反映動作

3. setAccessible(true) を使用します。

4. 可能な限りパブリックなメソッドとフィールドを使用する

5. MethodHandle によるリフレクションの実装

  6. リフレクションは、CallSite と MethodHandle の組み合わせによって実現されます。

 5. MethodHandleの例の説明

1.メソッドハンドルの作成

2.メソッドハンドルを呼び出す

3. InvokeExact と Invoke の違い

4. MethodHandles と Java Reflection API の比較

5. 完全な例

6. CallSite 例の説明

1. 基本的な考え方

2. CallSite タイプ

3. 完全な例

7. 通常のリフレクションとmethodHandleの効率比較


1. Java リフレクションとは:

Java のリフレクション メカニズムは、プログラムの実行状態で、任意のクラスのオブジェクトを構築でき、任意のオブジェクトが属するクラスを理解でき、任意のクラスのメンバー変数とメソッドを理解でき、任意のメソッドを呼び出すことができることを意味します。オブジェクトのプロパティとメソッド。この動的にプログラム情報を取得し、動的にオブジェクトを呼び出す機能をJava言語のリフレクション機構と呼びます。リフレクションは動的言語の鍵とみなされます。

Java リフレクション メカニズムは主に次の機能を提供します: 実行時にオブジェクトが属するクラスを決定する; 実行時に任意のクラスのオブジェクトを構築する; 実行時に任意のクラスのメンバー変数とメソッドを決定する; 実行時に任意のクラスを呼び出すtime. オブジェクトのメソッド。動的プロキシを生成します。

リフレクション テクノロジーは Java デザイン パターンとフレームワーク テクノロジーで広く使用されており、最も一般的なデザイン パターンはファクトリ パターンとシングルトン パターンです。

シングルトン モード (シングルトン): このモードの主な機能は、Java アプリケーション内にクラスのインスタンスが 1 つだけ存在するようにすることです。ディレクトリ データベース接続の確立など、多くの操作では、このようなシングル スレッド操作が必要です。これはメモリ領域を節約し、同じオブジェクトに確実にアクセスできるようにするために行われます。

ファクトリ パターン (ファクトリ): ファクトリ パターンは、Java リフレクション メカニズムと Java ポリモーフィック機能を使用して、プログラムをより柔軟にします。ファクトリ モデルを使用して大規模プロジェクトを開発すると、プロジェクトの並行開発が容易になります。


2. 反射アプリケーションの例:

「Person」というクラスを作成し、Java リフレクションを使用してその Class オブジェクトを取得し、そのメソッドを呼び出します。

public class Person {

    private String name;
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void sayHello() {
        System.out.println("Hello, my name is " + name + ", I'm " + age + " years old.");
    }

    private void sayBye() {
        System.out.println("Goodbye!");
    }

    // getters and setters...
}

 関数表示にはリフレクション API を使用します。

public class ReflectionExample {

    public static void main(String[] args) throws Exception {
        // 通过完全限定类名获取 Class 对象
        Class<?> cls = Class.forName("your.package.name.Person");

        // 构造一个具有特定参数类型的新 Person 实例
        Constructor<?> constructor = cls.getDeclaredConstructor(String.class, int.class);
        Object person = constructor.newInstance("John Doe", 20);

        // 获取并调用公开方法
        Method sayHelloMethod = cls.getDeclaredMethod("sayHello");
        sayHelloMethod.invoke(person);

        // 获取并调用私有方法
        Method sayByeMethod = cls.getDeclaredMethod("sayBye");
        sayByeMethod.setAccessible(true);
        sayByeMethod.invoke(person);

        // 通过反射操作属性
        Field nameField = cls.getDeclaredField("name");
        nameField.setAccessible(true);
        // 获取属性值
        System.out.println("Name: " + nameField.get(person));
        // 设置属性值
        nameField.set(person, "Jane Doe");
        System.out.println("Changed name: " + nameField.get(person));
    }
}

このコードは、まず Person クラスの Class オブジェクトを取得し、次に Class オブジェクトを使用して < /span>< a i=4> オブジェクト。  オブジェクトは、  クラスの新しいインスタンスを作成するために使用されます。 ConstructorConstructorPerson

次に、 sayHello メソッドと sayBye メソッドを取得して呼び出します。 sayBye はプライベート メソッドであるため、 setAccessible(true) コード行を通じて操作する必要があることに注意してください。

最後に、リフレクションを通じて name 属性を取得し、その値を取得して、新しい値を設定します。

これは、Java リフレクションを使用してクラス、メソッド、プロパティを操作する方法の基本的な使用法です。


3. どのような状況で反省が必要ですか?

リフレクションはオブジェクト指向プログラミングにおける重要な機能です。パフォーマンスのオーバーヘッドやセキュリティ上の問題がある可能性がありますが、場合によってはリフレクションの使用が必要です。いくつかの例を次に示します:

1. コードを動的にロードして実行する

リフレクションを使用すると、コードを動的にロードして実行できます。つまり、プログラムは、実行するために使用する必要があるすべてのクラスとメソッドの正確な名前を知る必要がありません。これは、柔軟性と拡張性の高いコードを作成する場合に非常に役立ちます。

2. デバッグおよびテストツール

        リフレクションを使用すると、開発者は実行時にプライベート フィールド、メソッド、およびコンストラクターにアクセスして変更できるため、デバッグ ツールやテスト ツールを作成する場合に特に役立ちます。 JUnit などのテスト フレームワークは、リフレクションを使用してテスト メソッドを検出して実行します。

3. 柔軟な枠組みを構築する

Spring や Hibernate などの多くの一般的な Java フレームワークは、リフレクションを使用して柔軟なコードを作成します。これらのフレームワークは通常、コンパイル時に使用される可能性のあるすべてのクラスやメソッドを認識せずに、実行時にオブジェクトを動的に構築およびアセンブルします。

4. シリアル化と逆シリアル化

リフレクションは、オブジェクトを保存可能または転送可能な形式 (JSON や XML など) に変換し、その形式からオブジェクトに戻すプロセスで広く使用されます。

5. プラグインのアーキテクチャ

外部プラグインを受け入れるアプリケーションを作成する場合、コンパイル時にアプリケーション自体がプラグインの特定の実装を認識しなくても、リフレクションを使用してプラグインを動的にロードして使用できます。

ご注意ください:

リフレクションは強力なツールですが、いくつかの欠点があります。通常、リフレクション操作は非リフレクション操作よりも時間がかかるため、パフォーマンスの問題が発生する可能性があります。さらに、リフレクションによって言語のアクセス制御がバイパスされる可能性があり、セキュリティ上の問題が発生する可能性があります。したがって、必要な場合を除いて反射は避けるべきです。


4. 反射の最適化

Java リフレクションのパフォーマンスが低下する場合があるため、最適化が必要になる場合があります。以下に考えられる最適化戦略をいくつか示します。

1. リフレクションを頻繁に呼び出さないようにする

        反射使用の頻度をできるだけ減らします。頻繁に呼び出される場合、パフォーマンスが低下する可能性があります。静的コーディングの代替手段がある場合は、それを優先する必要があります。

2. キャッシュ反映動作

        リフレクションを通じてメソッドを複数回呼び出す必要がある場合は、メソッドの Method オブジェクトをキャッシュする必要があります。この操作にはパフォーマンスのオーバーヘッドが大きいため、 Class.getMethod() を毎回呼び出さないでください。

Method methodToCall = null;
if (cachedMethods.containsKey(methodName)) {
  methodToCall = cachedMethods.get(methodName);
} else {
  methodToCall = clazz.getMethod(methodName, paramClasses);
  cachedMethods.put(methodName, methodToCall);
}

3. 使用する setAccessible(true)

        メソッドをリフレクティブに呼び出す前に、 Method.setAccessible(true) を呼び出すことができます。これにより、Java のアクセス チェックの一部がオフになり、リフレクションのパフォーマンスが向上します。ただし、これにより Java 言語のアクセス制御チェックが無効になることに注意してください。そのため、セキュリティ上の問題が発生しないことが確実な場合にのみ、このメソッドを使用してください。 

Method method = myObject.getClass().getMethod("myMethod");
method.setAccessible(true);
method.invoke(myObject);

4. public メソッドとフィールドをできるだけ使用する

        非パブリックのフィールドやメソッドにアクセスするには、通常、パブリックのフィールドやメソッドにアクセスするよりも多くの処理が必要になるため、可能な限りパブリックのフィールドやメソッドを使用すると、パフォーマンスが向上します。

5. MethodHandle によるリフレクションの実装

   MethodHandle メソッド、コンストラクター、またはフィールドへの型付きの直接参照です。これは、JVM が通常の Java メソッド呼び出しと同じ方法で最適化するように設計されています。

MethodHandle は、状況によってはリフレクションよりも優れたパフォーマンスを発揮する場合があります。まず、 MethodHandle の作成とリンクのプロセスがより効率的です。第 2 に、 MethodHandle は、含まれるメソッドが何を処理するかをランタイムがより明確に認識しているため、より強力なコンパイラの最適化をサポートします。コードはそうなります。

MethodType mt = MethodType.methodType(String.class, int.class, int.class);
MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "substring", mt);
String output = (String) mh.invokeExact("Hello World!", 0, 5);

  6. リフレクションは、CallSite と MethodHandle の組み合わせによって実現されます。

   Java では、CallSite はメソッド呼び出し情報とメソッド呼び出し実装を結び付ける仲介者です。これは、動的言語実装をサポートするために Java 7 の一部として導入されました。 CallSite はメソッドへの参照であり、参照されるメソッドは動的に変更できます。

import java.lang.invoke.*;

public class CallSiteDemo {
    public static void main(String[] args) throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodHandle mh = lookup.findStatic(Math.class, "sqrt", MethodType.methodType(double.class, double.class));
        CallSite site = new ConstantCallSite(mh);
        MethodHandle invoker = site.dynamicInvoker();
        Double result = (Double) invoker.invoke(9d);
        System.out.println(result);  // should print '3.0'
    }
}

 5. MethodHandleの例の説明

Java 7 では、メソッド ハンドル (MethodHandle) が、一般的な Java 仮想マシン (JVM) 操作を表現および実行するための強力な低レベル構造として導入されました。 MethodHandle は基本的に、メソッドが静的、プライベート、またはその他のタイプであるかに関係なく、メソッドへの直接参照です。これは、int や String と同様、JVM の基本的なデータ型です。

1.メソッドハンドルの作成

MethodHandle を取得するには、MethodHandles クラスの内部クラスであるMethodHandles.Lookup のインスタンスを使用する必要があります。一般に、メソッド ハンドルを取得する機能を持つルックアップ オブジェクト自体は、名前と型のタプルとして見なす必要があります。このルックアップ オブジェクトは次の方法で取得できます。

MethodHandles.Lookup lookup = MethodHandles.lookup();

 その後、ルックアップ オブジェクトを使用してメソッド ハンドルを見つけることができます。

MethodHandle handle = lookup.findStatic(Receiver.class, "methodName", methodType(String.class));

ここで、「Receiver.class」は検索するメソッドを含むクラス、「methodName」はメソッドの名前、methodType はメソッドのタイプを定義します。

2.メソッドハンドルを呼び出す

MethodHandle が作成されると、invoke() メソッドまたは invokeExact() メソッドを通じて呼び出すことができます。

String result = (String) handle.invoke("argument");

3. InvokeExact と Invoke の違い

  • invokeExact() はよりタイプセーフです。メソッドを呼び出すとき、渡されたパラメータがメソッドの型シグネチャと正確に一致するかどうかがチェックされます。それ以外の場合は、 WrongMethodTypeException をスローします。

  • invoke()次に、この点でリラックスしてください。型が一致しない場合は、パラメーターをメソッドの型に適合させます (可能な場合)。

これは、MethodHandle の型の特性を反映しています。各メソッド ハンドルにはタイプがあり、受け付けるパラメータのタイプと返す結果のタイプによって定義されます。

注: 厳密に言えば、 invokeExact() は、一部のパラメータ タイプが変更される可能性があり、そのような場合に JVM に本当に「ベスト エフォート」を実行させたい場合にのみ使用してください。 、 invoke() が使用されている場合にのみ使用してください。

4. MethodHandles と Java Reflection API の比較

メソッド ハンドルには、通常のリフレクション API に比べていくつかの利点があります。

  • パフォーマンス: 一部のシナリオでは、MethodHandles は従来のリフレクション API よりも優れたパフォーマンスを示します。
  • タイプ セーフ: MethodHandles は、コンパイル時の型チェックを利用できるタイプ セーフな操作を提供します。
  • 柔軟性: MethodHandle は、メソッド ハンドルの合成や変換などの高度な機能を提供します。

欠点は、MethodHandles が Java 7 で導入されたため、以前の Java バージョンでは使用できないことです。

MethodHandles は Java が提供する強力なツールで、動的なメソッド呼び出しと制御フロー操作を簡単にします。ただし、ほとんどのビジネスおよびアプリケーション プログラミングでは、従来の OOP およびインターフェイス/実装パターンの方が適切な場合があります。 MethodHandle のより一般的な使用例は、高度に動的である必要があるライブラリ、または JVM 命令セット (特定の種類のタスク ディスパッチや複雑な DSL 実装など) を直接操作できる必要があるライブラリを作成する場合です。

5. 完全な例

import java.lang.invoke.*;

public class MethodHandlesExample {
    
    // 定义一个简单的方法
    static class ClassA {
        public void println(String str) {
            System.out.println(str);
        }
    }

    public static void main(String[] args) throws Throwable {
        
        Object obj = new ClassA();
        // 无论obj最终是哪个实现类,下面这句都能正确调用到println方法。
        getPrintlnMethodHandle(obj).invokeExact("Hello, MethodHandle!");
    }

    private static MethodHandle getPrintlnMethodHandle(Object reveiver) throws NoSuchMethodException, IllegalAccessException {
        /* MethodType:代表“方法类型”,包含了方法的返回值(methodType()的第一个参数)和具体参数(methodType()第二个及以后的参数).*/
        MethodType mt = MethodType.methodType(void.class, String.class);
        
        /*
         * findVirtual:在指定类中查找符合给定的方法名称、方法类型,并且符合调用权限的方法句柄。
         * 因为这里调用的是一个虚方法,按照Java语言的规矩,方法第一个参数是隐式的,代表该方法的接收者,也即是this指向的对象。这个参数以前是放在参数列表中进行传递的,而现在提供了bindTo()方法来完成这件事情。
         */
        return MethodHandles.lookup().findVirtual(reveiver.getClass(), "println", mt).bindTo(reveiver);
    }

 この例では、getPrintlnMethodHandle() メソッドは  メソッドの呼び出しをカプセル化する MethodHandle インスタンスを返します。このメソッドの呼び出しは、通常の Java メソッド呼び出しと非常に似ていますが、唯一の違いは、  がより豊富で柔軟な呼び出しメソッドを提供していることです。 printlnMethodHandle


6. CallSite 例の説明

1. 基本的な考え方

JavaCallSite という用語は、主に invokedynamic (Java 7 で導入された新しいバイトコード命令) に由来しています。これは、JVM がメソッド呼び出しをディスパッチする方法を実行後または実行時に決定できるため、ユニークなディレクティブです。

それで、その中での CallSite の役割は何でしょうか?簡単に言えば、CallSite はメソッド呼び出しポイント (つまり、特定の呼び出し位置) を表します。ただし、invokedynamic に関連する Java コア クラス ライブラリは、次の 3 種類の CallSite を提供します。

  1. ConstantCallSite
  2. MutableCallSite
  3. VolatileCallSite

これらのカテゴリの違いは、CallSite の状態を変更することで、実行時にメソッド呼び出しがディスパッチされる方法を変更できることです。

2. CallSite タイプ

以下に、3 種類の CallSite について詳しく説明します。

  1. ConstantCallSite: これは最も単純なもので、一度初期化されると動作は変わりません。これは、通話場所のディスパッチ方法が後で変更されないことを意味します。

  2. MutableCallSite: このタイプCallSiteを使用すると、実行時に動作を変更できます。ディスパッチ方法を変更することはできますが、この操作はスレッドセーフではありません。

  3. VolatileCallSite: これも変数タイプCallSiteです。複数のスレッドが VolatileCallSite を同時に変更し、ディスパッチ メソッドを変更する場合、操作がスレッドセーフであることが保証されます。

3. 完全な例

使用CallSiteは、MethodHandle および関連するブートストラップ メソッドに依存する必要があります。 Bootstrap メソッドは、適切に呼び出すメソッド実装にリンクされている CallSite インスタンスを返すことを行います。 Java CallSite インスタンスを使用するには、Bootstrap メソッドと MethodHandle を深く理解する必要があります。ここでは、実行時に動作を変更できる CallSite の一種である MutableCallSite の使用方法を示します。

import java.lang.invoke.*;

public class Main {
    static class MyCallSite extends MutableCallSite {
        MethodHandle fallback;

        public MyCallSite() throws NoSuchMethodException, IllegalAccessException {
            super(MethodType.methodType(void.class, String.class));
            fallback = lookup().findVirtual(MyCallSite.class, "fallback", MethodType.methodType(void.class, String.class))
                    .bindTo(this);
            setTarget(fallback);
        }

        private void fallback(String arg) throws Throwable {
            System.out.println("Fallback " + arg);
            setTarget(lookup().findVirtual(Main.class, "print", MethodType.methodType(void.class, String.class)));
            fallback.invoke(arg);
        }
    }

    public static void main(String[] args) throws Throwable {
        MyCallSite callSite = new MyCallSite();
        MethodHandle target = callSite.dynamicInvoker();
        target.invoke("Hello World 1");  // 第一次回调,之后更新目标
        target.invoke("Hello World 2");  // 这次调用更新过的目标方法
    }

    public static void print(String arg) {
        System.out.println("Print " + arg);
    }
}

上記のコード スニペットでは次のようになります。

  1. MutableCallSite クラスを継承する MyCallSite クラスを作成しました。
  2. MyCallSite のコンストラクターで、フォールバック MethodHandle を定義し、それを現在の MyCallSite のターゲットとして設定します。
    • ここでのフォールバック メソッドは最初の呼び出しでのみ実行され、後続の呼び出しでは、設定した新しいターゲット メソッドが直接実行されます。
  3. フォールバック メソッド内で、カスタム print メソッドを指すように MyCallSite のターゲットを変更し、この変更された MethodHandle を再度呼び出しました。
  4. main メソッド内で、MyCallSite のインスタンスとその動的呼び出し元を作成し、この呼び出し元を 2 回実行しようとします。
    • 初めて invoke が実行されると、事前に設定されたフォールバック メソッドが実行され、次のターゲットが print メソッドに変更されます。
    • 2 回目に invoke を実行すると、変更されたターゲット (print メソッド) が直接実行されます。

注:上記の例は、実行時にメソッドの動作を変更する方法を示しています。不適切な操作を行うと、予期しない動作やエラーが発生する可能性があるため、この機能は注意して使用してください。 invokedynamic メカニズムと CallSite メカニズムを必ず十分に理解した上で使用してください。


7. 通常のリフレクションとmethodHandleの効率比較

以下は簡単な Java コードの例で、リフレクション ハンドルとメソッド ハンドルをそれぞれ使用してパフォーマンスの違いを観察します。

これは単なる基本的な例であり、すべての使用例におけるパフォーマンスの違いを完全に表しているわけではないことに注意してください。正確な違いは、JVM の最適化や特定の使用例など、多くの要因によって異なります。

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class MethodHandleAndReflectTest {
    static class Test {
        public void hello() {
            // Method body
        }
    }
    
    public static void main(String[] args) throws Throwable {
         Test t = new Test();
            Method method = Test.class.getMethod("hello");

            // Reflection
            long lastTime = System.currentTimeMillis();
            for (int i = 0; i < 1_000_000; i++) {
                method.invoke(t);
            }

            System.out.println("Reflection Time: " + (System.currentTimeMillis() - lastTime));

            // MethodHandle
            MethodHandle handle = MethodHandles.lookup().findVirtual(Test.class, "hello", MethodType.methodType(void.class)).bindTo(t);
            lastTime= System.currentTimeMillis();
            for (int i = 0; i < 1_000_000; i++) {
                handle.invoke();
            }

            System.out.println("MethodHandle Time: " + (System.currentTimeMillis() - lastTime));

             lastTime = System.currentTimeMillis();
            for (int i = 0; i < 1_000_000; i++) {
                t.hello();
            }

            System.out.println("native Time: " + (System.currentTimeMillis() - lastTime));
}

出力は次のとおりです。

リフレクション時間: 92
MethodHandle 時間: 5
ネイティブ時間: 0

結果が依然として非常に明白であることは誰でもわかります。テストでは、Reflection と MethodHandle がキャッシュされていない場合、MethodHandle の効率が Reflection よりも低いことがわかりました。


おすすめ

転載: blog.csdn.net/lejian/article/details/133903276