MethodHandleを使用して、サブクラスの祖父母クラスのオーバーライドメソッドを呼び出す場合

注:この例は、ZhouZhiming氏の「Java仮想マシンの詳細な理解」-仮想マシンバイトコード実行エンジンの章に最初に登場しました。一部の読者から質問があるため、Javaコードレベルに基づいた説明を示します(元のテキストはで、「Java仮想マシンの深い理解」読書ノート(7)-virtualマシンバイトコード実行エンジン(下))。

ここに直接コードを投稿してください:

package test;

import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;

public class Test {

    static class GrandFather {
        void thinking() {
            System.out.println("i am grandfather");
        }
    }

    static class Father extends GrandFather {
        void thinking() {
            System.out.println("i am father");
        }
    }

    static class Son extends Father {
        void thinking() {
            try {
                MethodType methodType = MethodType.methodType(void.class);
                Field IMPL_LOOKUP = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
                IMPL_LOOKUP.setAccessible(true);
                ((MethodHandles.Lookup) IMPL_LOOKUP.get(null))
                        .findSpecial(GrandFather.class, "thinking", methodType, Father.class)
                        .bindTo(this)
                        .invoke();
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws Throwable {
        Son son = new Son();
        son.thinking();
    }

}

Sonクラスのthinkingメソッドを直接見てみましょう(なぜこのように実装されているのかについては、「Java仮想マシンの詳細な理解」の注(7)を読んで説明されています-仮想マシンのバイトコード実行エンジン(以下) ))。

このコードに関しては、findSpecialメソッドがメソッドを検索することであり、invokeがメソッドを呼び出すことであることが簡単に理解できます。見つかった思考メソッドは非静的であるため、Javaではメソッドのレシーバーと呼ばれる暗黙の入力パラメーター(つまり、スタックフレーム内のローカル変数テーブルの0番目の位置にあるこのパラメーター)が必要です。

通常のメソッド呼び出しでは、このパラメーターは現在のインスタンスオブジェクトを表す仮想マシンによって自動的に処理され、メソッドで直接使用できます。ただし、MethodHandleの例では、invoke *命令の処理をシミュレートするのと同じです。invokeメソッドを手動で呼び出すには、「this」パラメーターを指定する必要があります。実際、「this」パラメーターだけでなく、他のパラメーターもinvokeで渡す必要があります。思考方法が次のように定義されている場合:

void thinking(String str) {
            System.out.println("i am grandfather");
        }

次に、invokeメソッドには、暗黙の「this」とStringの2つのパラメーターが必要です。したがって、次のように呼び出す必要があります。この時点でinvoke(this);またはinvoke( "string")を呼び出すと、エラーが報告されます。

bindToメソッドに関しては、実際には指定されたメソッドのレシーバーです。BindTo(this).invoke()とinvoke(this)は同じ意味を持つと見なすことができます。次のように:

//
((MethodHandles.Lookup) IMPL_LOOKUP.get(null))
                        .findSpecial(GrandFather.class, "thinking", methodType, Father.class)
                        .bindTo(this)
                        .invoke();
//和上面的调用是一个意思
((MethodHandles.Lookup) IMPL_LOOKUP.get(null))
                        .findSpecial(GrandFather.class, "thinking", methodType, Father.class)
                        .invoke(this);

bindToがパラメーターを指定した後、invokeメソッドでこの暗黙のパラメーターを指定する必要はありません。指定しないと、通常のパラメーターとして扱われ、エラーが発生します。bindToを使用してメソッドのレシーバーをバインドする方が、invokeメソッドを渡すよりも使いやすく、プログラマーの一般的な理解に沿っていると思います。Invokeは、メソッドの明示的なパラメーターにのみ焦点を当てることができます。

次に、bindTo(this)でこれについて説明しましょう。前述のように、これはメソッドのレシーバーとして渡されるので、GrandFatherメソッドでこれを印刷してみましょう。

static class GrandFather {
        void thinking() {
            System.out.println("i am grandfather\nthis.class:" + this.getClass());
        }
    }

結果出力:

私は祖父です
       this.class:class test.Test $ Son

ご覧のとおり、GrandFatherのインスタンスオブジェクトを使用しなかったため、このクラスは$ GrandFatherではなく$ Sonであり、実際に渡されたのは、bindToまたはinvokeによって指定されたインスタンスオブジェクトでした。この事実に基づいて、Sonへのリフレクションまたは直接型強制を使用して、GrandFatherの思考メソッドでSonクラスの一意のメソッドを直接呼び出すことができます。

同様に、Sonの思考メソッドのbindToがFatherオブジェクトに変更された場合、つまり、bindTo(this)がbindTo(new Father())に変更された場合:

static class Son extends Father {
        void thinking() {
            try {
                MethodType methodType = MethodType.methodType(void.class);
                Field IMPL_LOOKUP = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
                IMPL_LOOKUP.setAccessible(true);
                ((MethodHandles.Lookup) IMPL_LOOKUP.get(null))
                        .findSpecial(GrandFather.class, "thinking", methodType, Father.class)
                        .bindTo(new Father())
                        .invoke();
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }

実行結果を見てください:

私は祖父です
       this.class:class test.Test $ Father

ただし、bindToメソッドのパラメーターを気軽に渡すことはできません。これは、この例で使用されているFather.classであるfindSpecialメソッドの4番目のクラスパラメーターに戻ります。このパラメーターは、メソッドのレシーバーのタイプを指定します。bindToで指定されたレシーバーのタイプは、このクラスまたはサブクラスである必要があります。そうでない場合、ClassCastExceptionが発生します。処理ロジックで強制する必要があるため、メソッドレシーバーをバインドします。

public MethodHandle bindTo(Object x) {
        Class<?> ptype;
        @SuppressWarnings("LocalVariableHidesMemberVariable")
        MethodType type = type();
        if (type.parameterCount() == 0 ||
            (ptype = type.parameterType(0)).isPrimitive())
            throw newIllegalArgumentException("no leading reference parameter", x);
        x = ptype.cast(x);  // 主要是这里,调用的是Class的cast方法。throw CCE if needed
        return bindReceiver(x);//绑定方法接收者
    }


Class.cast方法如下:

public T cast(Object obj) {
        if (obj != null && !isInstance(obj))
            throw new ClassCastException(cannotCastMsg(obj));
        return (T) obj;
    }

この例では、祖父クラスのメソッドを見つけるために、findSpecialメソッドの4番目のクラスタイプはFather.classまたはGrandFather.classである必要があります。これはinvokespecial命令に関連しています(詳細については、詳細」を参照してください。「Java仮想マシンの理解」リーディングノート(7)-仮想マシンのバイトコード実行エンジン(オン))。findVirtualを使用してこのメ​​ソッドを見つけることもできますが、GrandFatherのインスタンスが必要です(もちろん、リフレクションを使用する必要はありません)。

static class Son extends Father {
        void thinking() {
            try {
                MethodType methodType = MethodType.methodType(void.class);
                MethodHandles.lookup()
                        .findVirtual(GrandFather.class, "thinking", methodType)
                        .bindTo(new GrandFather())
                        .invoke();
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }

しかし、これは実際には新しいGrandFather()。thinking()とほとんど同じです。

おすすめ

転載: blog.csdn.net/huangzhilin2015/article/details/114659298