JAVA反射-1 初识

143 阅读6分钟

1.Field方式

package com.qihongze.dw.flow.joinDim;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

class MyClass {
    private String name;
    private int age;
    private double height;

    public void myMethod(String message) {
        System.out.println(message);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public double getHeight() {
        return height;
    }

    public void setHeight(double height) {
        this.height = height;
    }
}

public class test {
    public static void main(String[] args) throws Exception {
        // 1. 创建 MyClass 的实例
        MyClass obj = new MyClass();

        // 2. 调用公共方法 myMethod
        Method myMethod = MyClass.class.getMethod("myMethod", String.class);
        myMethod.invoke(obj, "Hello, Reflection!");

        // 3. 获取并调用私有字段 name
        Field nameField = MyClass.class.getDeclaredField("name");
        // 也可以用下面的代码,获取obj对象的name属性Field
        Field nameField = obj.getClass().getDeclaredField("name");

        // 设置字段可访问
        nameField.setAccessible(true);

        // 使用 setter 方法设置字段值
        nameField.set(obj, "John");

        // 使用 getter 方法获取字段值
        String name = (String) nameField.get(obj);
        System.out.println("Name: " + name);  // 输出: Name: John
    }
}

2.MethodHandle方式

  • MethodHandle 是 Java 7 引入的一个强大的反射机制,它提供了一种比传统的 Method 对象更灵活和强大的方法调用方式。与 Method 对象不同,MethodHandle 可以像函数指针一样直接调用方法,并且支持对方法的参数和返回值进行更灵活的操作,包括类型转换、参数重排序等。

(1) 创建 MethodHandle

  • 使用 MethodHandles.lookup()
  • 首先,你需要使用 MethodHandles.lookup() 方法来创建一个 MethodHandles.Lookup 对象,它是创建 MethodHandle 的起点。
MethodHandles.Lookup lookup = MethodHandles.lookup();
  • lookup 对象提供了一系列方法来创建不同类型的 MethodHandle,例如 unreflect 方法可以将已有的 MethodConstructor 转换为 MethodHandle

  • 通过 unreflect 系列方法创建 MethodHandle

  • 从方法创建 MethodHandle

Method method = MyClass.class.getMethod("myMethod", String.class);// 获取MyClass类下面参数类型是String的myMethod方法
MethodHandle methodHandle = lookup.unreflect(method);
  • 从构造函数创建 MethodHandle
Constructor<MyClass> constructor = MyClass.class.getConstructor(String.class);
MethodHandle constructorHandle = lookup.unreflectConstructor(constructor);

(2) 调用 MethodHandle

  • 使用 invokeinvokeExact 方法
  • invoke 方法会自动进行类型转换,而 invokeExact 要求严格的类型匹配。
MyClass obj = new MyClass();
Method addMethod = MyClass.class.getMethod("add", int.class, int.class);
// 使用 invoke 方法,会进行类型转换,obj参数表示要调用方法的对象实例,如果是静态方法可以为null;第二个及以后参数是要调用的方法传入的参数
addMethod.invoke(obj, 3, 4);
// 使用 invokeExact 方法,要求严格的类型匹配
addMethod.invokeExact((Object) obj, (Object) "argument");
  • 使用invokeWithArguments 方法,该方法接受一个 Object... 可变参数列表,将这些参数传递给 MethodHandle 所代表的方法进行调用。
MyClass obj = new MyClass();
MethodHandle methodHandle = MethodHandles.lookup().findVirtual(MyClass.class, "myMethod", MethodType.methodType(void.class, String.class));
// 使用 invokeWithArguments 方法调用方法句柄
methodHandle.invokeWithArguments(obj, "test argument");
  • 使用invokeSpecial 方法,主要用于调用实例方法中的特殊方法,比如构造函数、super 关键字调用的父类方法等。
MethodHandle lookup = MethodHandles.lookup();
MethodHandle superMethod = lookup.findSpecial(ParentClass.class, "parentMethod",
        MethodType.methodType(void.class), ChildClass.class);
superMethod.invokeSpecial(this);//调用父类的parentMethod方法
  • 使用invokeStatic 方法,用途:用于调用类的静态方法。
MethodHandle staticMethod = MethodHandles.lookup().findStatic(UtilityClass.class, "add",
MethodType.methodType(int.class, int.class, int.class));
int result = (int) staticMethod.invokeStatic(2, 3);
System.out.println("调用静态方法的结果: " + result);
  • 使用unreflect系列
Field field = MyClass.class.getDeclaredField("myField");
// 假设MyClass类有一个名为myField的字段	
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle getterHandle = lookup.unreflectGetter(field);//获取MyClass对象的myField的值
String fieldValue = (String) getterHandle.invoke(obj);
MethodHandle setterHandle = lookup.unreflectSetter(field);
setterHandle.invoke(obj, "new value");//设置MyClass对象的myField的值为"new value"

(3) 类型转换和参数重排序

  • 使用 asType 方法

    • 可以使用 asType 方法选择创建之前旧的MethodHandle 对应的方法名,并指定参数和返回值类型。

    • 如果没有这个参数和返回值类型的方法,那么会抛异常InvalidMethodTypeException

MethodHandle targetMethodHandle = methodHandle.asType(MethodType.methodType(void.class, int.class));
  • 这在需要将 MethodHandle 应用于不同类型的参数或在不同上下文中使用时非常有用。

(4) 组合 MethodHandle

  • 使用 filterArguments 等方法

    • 可以使用 filterArgumentsMethodHandle 的参数进行过滤或修改。
MethodHandle mh1 = MethodHandles.lookup().findStatic(Math.class, "sqrt", MethodType.methodType(double.class, double.class)); //findStatic查找Math这个类的一个静态方法叫sqrt,并且描述了返回值类型是double,参数类型也是double,然后生成对应的MethodHandle对象
MethodHandle mh2 = MethodHandles.constant(double.class, 2.0);// 创建一个表示2.0的常量
// 将第二个参数替换为常量 2.0
MethodHandle combined = MethodHandles.filterArguments(mh1, 1, mh2); //mh1是方法,1是要替换的参数的索引位置,mh2是修改后的参数,==> Math.sqrt(2.0)
  • 使用compose方法,实现将一个MethodHandle的返回值作为另一个MethodHandle
// 获取一个返回double类型的方法句柄(这里假设是获取一个固定值的方法)
MethodHandle getValue = MethodHandles.constant(double.class, 4.0);
// 获取Math.sqrt方法的句柄
MethodHandle sqrt = MethodHandles.lookup().findStatic(Math.class, "sqrt", MethodType.methodType(double.class, double.class));
// 使用compose组合两个方法句柄
MethodHandle composed = sqrt.compose(getValue);
// 调用组合后的方法句柄,相当于先获取4.0,再对其求平方根
double result = (double) composed.invoke();
System.out.println(result);
  • 使用collectArguments将多个参数合并成一个数组作为 MethodHandle 的参数。
// 定义一个接受数组参数的方法句柄(假设是一个打印数组元素的方法)
MethodHandle printArray = MethodHandles.lookup().findVirtual(System.out.getClass(), "println", MethodType.methodType(void.class, Object.class));
// 创建两个参数的方法句柄
MethodHandle arg1 = MethodHandles.constant(int.class, 1);
MethodHandle arg2 = MethodHandles.constant(int.class, 2);
// 使用collectArguments将两个参数合并成数组作为printArray的参数
MethodHandle combined = MethodHandles.collectArguments(printArray, 0, MethodType.methodType(int[].class, int.class, int.class));//对printArray这个方法的参数从0索引位置开始收集参数
// 调用组合后的方法句柄,相当于打印[1, 2]
combined.invoke(arg1.invoke(), arg2.invoke());
  • 使用bindTo方法,将MethodHandle的一个参数绑定为一个固定值
// 获取一个有两个参数的方法句柄(假设是一个加法方法)
MethodHandle add = MethodHandles.lookup().findStatic(Math.class, "addExact", MethodType.methodType(int.class, int.class, int.class));
// 将第一个参数绑定为5
MethodHandle bound = add.bindTo(5);
// 调用绑定后的方法句柄,相当于计算5 + 3
int result = (int) bound.invoke(3);
System.out.println(result);
  • 使用insertArguments 方法,在 MethodHandle 的参数列表中插入固定值。
// 获取一个有两个参数的方法句柄(假设是一个乘法方法)
MethodHandle multiply = MethodHandles.lookup().findStatic(Math.class, "multiplyExact", MethodType.methodType(int.class, int.class, int.class));
// 在第一个参数位置插入固定值2
MethodHandle inserted = MethodHandles.insertArguments(multiply, 0, 2);
// 调用插入参数后的方法句柄,相当于计算2 * 3
int result = (int) inserted.invoke(3);
System.out.println(result);
  • 使用asCollector 方法,可以将多个参数收集为一个集合类型的参数(如 List 等),然后传递给目标方法。
MethodHandle originalMethod = MethodHandles.lookup().findVirtual(ListProcessingClass.class, "processList", MethodType.methodType(void.class, List.class));  
MethodHandle collectedMethod = originalMethod.asCollector(List.class, 3);  
ListProcessingClass obj = new ListProcessingClass();  
collectedMethod.invoke(obj, 1, 2, 3);

(5) 异常处理

  • 捕获和处理异常

    • 调用 MethodHandle 时可能会抛出异常,需要使用 try-catch 语句进行捕获。
try {
    methodHandle.invoke(obj, "argument");
} catch (Throwable throwable) {
    // 处理异常
}

(6) 案例代码

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;


class MyClass {
    public void myMethod(String message) {
        System.out.println(message);
    }
}


public class MethodHandleExample {
    public static void main(String[] args) throws Throwable {
	//1.查找和创建 MethodHandle:使用 MethodHandles.lookup() 找到 MyClass 的 myMethod 方法,并使用 unreflect 将其转换为 MethodHandle。
        MethodHandles.Lookup lookup = MethodHandles.lookup();

	// getMethod第一个参数是方法的名称,后面的参数是方法要传入的参数类型
	// method ==> public void com.qihongze.dw.flow.joinDim.MyClass.myMethod(java.lang.String)
        Method method = MyClass.class.getMethod("myMethod", String.class);
	
	// unreflect是为了定义下面的invoke调用,需要传入的参数是method对象的类名和参数列表
	// methodHandle ==> MethodHandle(MyClass,String)void
        MethodHandle methodHandle = lookup.unreflect(method);

        MyClass obj = new MyClass();
	//2.调用 MethodHandle:使用 invoke 方法调用 MethodHandle,传入 MyClass 的实例和参数。
        methodHandle.invoke(obj, "Hello, MethodHandle!");

	//3.修改 MethodHandle 类型:使用 asType 方法将 MethodHandle 的参数类型修改为 Object.class,并使用 invoke 调用修改后的 MethodHandle。
	// 下面代码会报错WrongMethodTypeException: cannot convert MethodHandle(MyClass,String)void to (Object)void 因为methodHandle的类型是MethodHandle(MyClass,String)void,然后想要转为MethodHandle(Object)void,发生类型不匹配,因为少一个参数,应该转为(Object,String)void类型
        MethodHandle modifiedHandle = methodHandle.asType(MethodType.methodType(void.class, Object.class));
	
	//正确代码
MethodHandle modifiedHandle = methodHandle.asType(MethodType.methodType(void.class, Object.class, String.class));

        // 调用修改后的 MethodHandle
        modifiedHandle.invoke(obj, (Object) "Hello, Modified MethodHandle!");

        Field nameField = obj.getClass().getDeclaredField("name");
        nameField.setAccessible(true);
	// 往obj对象的name字段中调用set方法设置值为John
        MethodHandle setNameMH = lookup.unreflectSetter(nameField);
        setNameMH.invoke(obj, "John");
	
	// 获取obj对象的name字段属性值
        MethodHandle nameMH = lookup.unreflectGetter(nameField);
        Object invoke = nameMH.invoke(obj);
        System.out.println(invoke);
    }
}

(7) 注意事项

  • 性能开销
  • 由于 MethodHandle 涉及反射,性能开销可能比直接方法调用大,因此在性能敏感的代码中要谨慎使用。
  • 类型匹配
  • invoke是要看MethodHandle 是get还是set,如果是get只传一个参数;如果是set需要传多个参数,第一个参数是Field,后面都是构造函数。千万注意WrongMethodTypeException类型不匹配问题。
  • 使用 invokeExact 时,要确保参数类型严格匹配,否则会抛出 WrongMethodTypeException