On the use of MethodHandle to call the grandparent class override method in the subclass

Note: This example originally appeared in Mr. Zhou Zhiming's "In-depth Understanding of Java Virtual Machine"-Virtual Machine Bytecode Execution Engine Chapter. Since some readers have questions, here is an explanation based on the Java code level (the original text is in "In-depth understanding of Java Virtual machine" reading notes (7)-virtual machine bytecode execution engine (below) ).

Post the code directly here:

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();
    }

}

Let's look directly at the thinking method of the Son class (about why it is implemented this way, it is also explained in the "In-depth understanding of the Java virtual machine" reading notes (7)-virtual machine bytecode execution engine (below) ).

Regarding this code, you can simply understand that the findSpecial method is to find the method, and the invoke is to call the method. Since the thinking method found is non-static, it needs an implicit input parameter (that is, the this parameter at the 0th position of the local variable table in the stack frame), which is called the receiver of the method in Java.

In ordinary method calls, the this parameter is automatically processed by the virtual machine, which represents the current instance object, and we can use it directly in the method. But in our example of MethodHandle, it is equivalent to simulating the processing of the invoke* instruction. To manually call the invoke method, you need to specify the "this" parameter. In fact, not only the "this" parameter, other parameters also need to be passed in invoke. If the thinking method is defined as:

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

Then the invoke method requires two parameters, an implicit "this" and a String. So it should be called like this: invoke(this,"string"); if you call invoke(this); or invoke("string") at this time, an error will be reported.

Regarding the bindTo method, it is actually the receiver of the specified method. BindTo(this).invoke() and invoke(this) can be considered to have the same meaning. as follows:

//
((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);

After bindTo specifies the parameters, there is no need to specify this implicit parameter in the invoke method, otherwise it will be treated as a normal parameter, and an error will occur. I think using bindTo to bind the receiver of the method is more friendly than passing in the invoke method, and it is more in line with the general understanding of programmers. Invoke can only focus on the explicit parameters of the method.

Then let's talk about this in bindTo(this). As mentioned earlier, this this is passed as the receiver of the method, so let's try to print this in the GrandFather method:

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

Result output:

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

As you can see, this class is $Son, not $GrandFather, because we didn't use the instance object of GrandFather, and what actually passed in was the instance object specified by bindTo or invoke. Based on this fact, we can directly call the unique method of the Son class in the thinking method of GrandFather, using reflection or direct type coercion to Son.

Similarly, if the bindTo in Son's thinking method is modified to the Father object, that is, bindTo(this) is modified to 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();
            }
        }
    }

Look at the running results:

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

But the parameters of the bindTo method cannot be passed casually. This will go back to the fourth class parameter in the findSpecial method, which is Father.class used in this example. This parameter specifies the type of the receiver of the method, and the type of the receiver specified by bindTo must be of this class or subclass, otherwise a ClassCastException will occur. Because it needs to be forced in the processing logic, and then bind the method receiver:

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;
    }

In this example, to find the method of the grandfather class, the fourth class type of the findSpecial method must be Father.class or GrandFather.class. This is related to the invokespecial instruction (for details, please refer to the "In-depth Understanding of the Java Virtual Machine" reading notes ( Seven)-virtual machine bytecode execution engine (on) ). We can also use findVirtual to find this method, but we need an instance of GrandFather (of course we don’t need to use reflection):

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();
            }
        }
    }

But this is actually almost the same as new GrandFather().thinking().

Guess you like

Origin blog.csdn.net/huangzhilin2015/article/details/114659298