关于使用MethodHandle在子类中调用祖父类重写方法的探究

注:这个例子原本出现在周志明先生的《深入理解Java虚拟机》--虚拟机字节码执行引擎章节,介于有读者朋友有疑问,这里基于Java代码层面解释一下(原文在《深入理解Java虚拟机》读书笔记(七)--虚拟机字节码执行引擎(下))。

这里直接把代码贴出来:

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虚拟机》读书笔记(七)--虚拟机字节码执行引擎(下)中也解释了)。

关于这段代码,可以简单的理解findSpecial方法是为了找到方法,invoke是为了调用方法。由于找到的thinking方法是非static的,需要一个隐式入参(也就是栈帧中局部变量表第0个位置的this参数),在java中这叫做该方法的接收者。

在普通的方法调用中,这个this参数是虚拟机自动处理的,表示的是当前实例对象,我们在方法中可以直接使用。但是在我们这个MethodHandle的例子中,相当于是模拟了invoke*指令的处理,手动调用invoke方法就需要指定这个"this"参数。其实不只是"this"参数,其它参数也需要在invoke中传递。如果thinking方法的定义为:

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

那么invoke方法就需要两个参数,一个隐式的"this",一个String。所以应该这样调用:invoke(this,"string");如果这时调用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方法中就不用指定this隐式参数了,不然会被当做普通参数去处理,就会出错。我觉得使用bindTo绑定方法接收者要比在invoke方法中传递更加友好,也更加符合程序员的大众理解,invoke可以只专注方法显式的入参。

然后再来说bindTo(this)中的this。前面提到了,这个this是我们当做方法接收者传过去的,那我们尝试在GrandFather的方法中把this打印出来看看:

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

结果输出:

i am grandfather
       this.class:class test.Test$Son

可以看到,这个class是$Son,并不是$GrandFather,因为我们没有使用GrandFather的实例对象,实际传入的是bindTo或invoke指定的实例对象。基于这个事实,我们这时可以直接在GrandFather的thinking方法中调用Son类独有的方法,使用反射或者直接类型强制转换为Son就行了。

同样的,如果将Son的thinking方法中的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();
            }
        }
    }

再看运行结果:

i am grandfather
       this.class:class test.Test$Father

但是bindTo方法的参数可不能随便传。这就要回到findSpecial方法中的第四个class类型的参数,即本例中使用的Father.class。这个参数中指定的是方法接收者的类型,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方法的第四个class类型必须是 Father.class或GrandFather.class了,这个就和invokespecial指令有关了(具体可以参照《深入理解Java虚拟机》读书笔记(七)--虚拟机字节码执行引擎(上))。我们也可以使用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();
            }
        }
    }

不过这个其实就和new GrandFather().thinking()差不多了。

你可能感兴趣的:(深入理解Java虚拟机读书笔记,JVM,JAVA,java,jvm,MethodHandle)