Sobre el uso de MethodHandle para llamar al método de anulación de la clase de abuelos en la subclase

Nota: Este ejemplo apareció originalmente en el capítulo "Comprensión en profundidad de la máquina virtual Java" del Sr. Zhou Zhiming: Motor de ejecución de código de bytes de máquina virtual. Dado que algunos lectores tienen preguntas, aquí hay una explicación basada en el nivel de código Java (el texto original es en "Comprensión profunda de la máquina virtual Java" leyendo notas (7) -motor de ejecución de código de bytes de máquina virtual (abajo) ).

Publique el código directamente aquí:

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

}

Veamos directamente el método de pensamiento de la clase Son (acerca de por qué se implementa de esta manera, también se explica en las notas de lectura de "Comprensión profunda de la máquina virtual Java" (7) - motor de ejecución de código de bytes de máquina virtual (abajo ) ).

Con respecto a este código, simplemente puede entender que el método findSpecial es encontrar el método y la invocación es llamar al método. Dado que el método de pensamiento encontrado no es estático, necesita un parámetro de entrada implícito (es decir, este parámetro en la posición 0 de la tabla de variables locales en el marco de la pila), que se denomina receptor del método en Java.

En las llamadas a métodos ordinarios, este parámetro es procesado automáticamente por la máquina virtual, que representa el objeto de instancia actual, y podemos usarlo directamente en el método. Pero en nuestro ejemplo de MethodHandle, es equivalente a simular el procesamiento de la instrucción invoke *. Para llamar manualmente al método invoke, necesita especificar el parámetro "this". De hecho, no solo el parámetro "this", también deben pasarse otros parámetros en la invocación. Si el método de pensamiento se define como:

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

Entonces, el método de invocación requiere dos parámetros, un "esto" implícito y un String. Por lo tanto, debería llamarse así: invoke (this, "cadena"), si llama a invoke (this) o invoke ("string") en este momento, se informará un error.

En cuanto al método bindTo, en realidad es el receptor del método especificado. Se puede considerar que BindTo (this) .invoke () e invoke (this) tienen el mismo significado. como sigue:

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

Después de que bindTo especifica los parámetros, no es necesario especificar este parámetro implícito en el método de invocación; de lo contrario, se tratará como un parámetro normal y se producirá un error. Creo que usar bindTo para vincular el receptor del método es más amigable que pasar el método invoke, y está más en línea con la comprensión general de los programadores. Invoke solo puede enfocarse en los parámetros explícitos del método.

Entonces hablemos de esto en bindTo (this). Como se mencionó anteriormente, esto se pasa como el receptor del método, así que intentemos imprimir esto en el método GrandFather:

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

Salida de resultado:

Soy el abuelo
       this.class: class test.Test $ Son

Como puede ver, esta clase es $ Son, no $ GrandFather, porque no usamos el objeto de instancia de GrandFather, y lo que realmente pasó fue el objeto de instancia especificado por bindTo o invoke. Basándonos en este hecho, podemos llamar directamente al método único de la clase Son en el método de pensamiento de GrandFather, usando la reflexión o la coerción de tipo directa a Son.

De manera similar, si bindTo en el método de pensamiento de Son se modifica al objeto Father, es decir, bindTo (this) se modifica a 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();
            }
        }
    }

Mira los resultados de ejecución:

soy el abuelo
       this.class: class test.Test $ Father

Pero los parámetros del método bindTo no se pueden pasar casualmente. Esto volverá al cuarto parámetro de clase en el método findSpecial, que es Father.class usado en este ejemplo. Este parámetro especifica el tipo de receptor del método, y el tipo de receptor especificado por bindTo debe ser de esta clase o subclase, de lo contrario se producirá una ClassCastException. Debido a que debe forzarse en la lógica de procesamiento y luego vincular el receptor del método:

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

En este ejemplo, para encontrar el método de la clase abuelo, el cuarto tipo de clase del método findSpecial debe ser Father.class o GrandFather.class. Esto está relacionado con la instrucción invookespecial (para obtener más detalles, consulte la sección "En profundidad Comprensión de las notas de lectura de la máquina virtual Java (siete) - motor de ejecución de código de bytes de la máquina virtual (activado ). También podemos usar findVirtual para encontrar este método, pero necesitamos una instancia de GrandFather (por supuesto, no necesitamos usar la reflexión):

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

Pero esto en realidad es casi lo mismo que el nuevo Abuelo () .pensamiento ().

Supongo que te gusta

Origin blog.csdn.net/huangzhilin2015/article/details/114659298
Recomendado
Clasificación