[Java]关于反射的理解与运用

发布时间 2024-01-13 01:36:08作者: 进步·于辰

【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://www.cnblogs.com/cnb-yuchen/p/17960654
出自【进步*于辰的博客

参考笔记一,P74.6;笔记二,P74.2、P75.3;笔记三,P15.2、P43.2、P44.2/3。

1、什么是“反射”?

关于类加载,详述可查阅博文《Java知识点锦集》的第5项。

1.1 概述

大家先看一个图,
在这里插入图片描述
过程说明:

  1. A → B。当JVM运行,将 java 源文件编译成 class 字节码文件。
  2. B → D。执行如下代码,通过类加载器 ClassLoader 将 class 字节码文件加载进JVM方法区、生成 class 信息、进而创建 Class 对象,这个过程就是“类加载”。(注:只有对类的主动使用才会触发类加载,例如:反射、实例化)。
1、A.class
2、new A().getClass()
3、Class.forName()
  1. D → E。通过调用newInstance(),使用 Class 对象创建实例。

结论:反射是一种通过类加载加载JVM方法区中的 class 信息、创建实例的机制。

补充一点:
class字节码文件(图中B)中包含字面量和符号引用。字面量指为变量所赋的值;符号引用指变量在编译时的一个地址标识不是确切的地址,因为只有在运行时,才会为变量分配内存地址。

1.2 反射的另一种情形

1、A.class// A 是类名
2、new A().getClass()
3、Class.forName()

在上文中说道,过程 B → D 就是类加载,执行如上代码中任一条都可以触发此过程。

创建 Class 对象是反射的标志,而反射基于类加载。因此,这三种情况都属于反射。可实际上,只有第3种才会触发类加载。听我细细道来。。。

大家先看个图。
在这里插入图片描述
反射的最终目的是实例,可有时候只是为了获取 Class 对象。若已存在实例,则通过调用getClass()获取会更简便。

这种通过对实例进行反编译、进而创建 Class 对象的机制也属于反射,

可这种情形不会触发类加载,因为类加载只会执行一次,既然存在实例,自然已完成了类加载。

我为何会注意到“反编译不会触发类加载”这一细节?

平日看源码的时候,经常会看到这样的代码块:

static {}

这个叫做“静态代码块”,它执行于类初始化时(类加载的第三过程)。在这里会编写一些为类变量赋初始值或初始操作的代码,而往往这些代码并不容易看懂,那就需要debug。(PS:进行debug前当然需要先知道什么情况下才会执行“静态代码块”)

总结:只有Class.forName()实例化 才会触发类加载,而getClass()不会。并且,通过debug发现,A.class也同样不会触发类加载,故可判断A.class也是通过反编译进行反射。

1.3 扩展:静态内部类的类加载

大家看一个栗子。

class OuterClass {
    static class InnerClass {
        static {
            sout "csdn";
        }
    }
}

什么情况下才会打印"csdn"?据上文可知,只要进行类加载,就会执行static {}
虽然内部类属“懒加载”,但其类加载在本质上与外部类的类加载相同,即当执行Class.forName()或实例化时才会触发类加载。如下述代码:

1、Class z1 = Class.forName("OuterClass$InnerClass");
2、OuterClass.InnerClass obj1 = new OuterClass.InnerClass();

补充说明:为什么不能在类方法中实例化非静态内部类,而静态内部类可以?

因为类方法加载于类加载时,而非静态内部类属“懒加载”,在外部类调用时才加载。换言之,类加载时不会加载非静态内部类(可视为不存在),自然无法实例化。
而静态内部类同外部类一起加载(可视为“积极加载”),自然可以实例化。

PS
可能大家会疑惑,为什么我不对其他几种内部类的类加载进行说明?原因:

  1. 关于其他几种内部类的类加载我暂未研究;
  2. 只有静态内部类内才能定义static {}

具体原因可查阅博文《Java知识点锦集》的第15项。

2、反射运用(获取类成员)

在反射的使用中,直接涉及的类是 Class<T>

以下3个方法可用于获取构造方法、方法(包括成员方法、类方法)和变量(包括成员变量、类变量)。

getConstructor(xx)	// 获取构造方法,xx是构造方法形参的数据类型的class
getMethod(a, b)		// 获取方法,包括成员方法和类方法,a是方法名,b是方法形参的数据类型的class集,b位置是可变参数
getField(xx)	// 获取变量,包括成员变量和类变量,xx是变量名

笼统列举,大家看起来有点云里雾里,下面一一详述。

2.1 构造方法

待反射类:

@Data
class Reflect {
    private int x;
    private String y;
    private boolean z;
 
    private Reflect() {
    }

    Reflect(int x) {
    	this.x = x;
    }

    protected Reflect(int x, String y) {
		this.x = x;
		this.y = y;
    }

    public Reflect(int x, String y, boolean z) {
   		this.x = x;
		this.y = y;
		this.z = z;
    }
}

2.1.1 获取构造方法数组

测试示例:

class TestReflect {
    public static void main(String[] args) throws Exception {
        Class class1 = Class.forName("com.neusoft.boot.Reflect");
        // 获取公共(public)构造方法集合
        Constructor[] arr1 = class1.getConstructors();
        System.out.println("公共(public)构造方法:");
        for (Constructor c : arr1) {
            System.out.println(c);
        }
        
        // 获取所有构造方法集合
        Constructor[] arr2 = class1.getDeclaredConstructors();
        System.out.println("所有构造方法:");
		for (Constructor c : arr2) {
            c.setAccessible(true);// 强制访问
            System.out.println(c);
        }
    }
}

测试结果:
在这里插入图片描述

2.1.2 获取指定构造方法

相应获取方法:

1、getConstructor(xx)	// 获取公共(public)构造方法
2、getDeclaredConstructor(xx)	//  获取构造方法,包括:private、默认(未指定访问修饰符或类中未自定义构造方法)、protected、public

测试示例:

class TestReflect {
    public static void main(String[] args) throws Exception {
        Class class1 = Class.forName("com.neusoft.boot.Reflect");

        Constructor privateC = class1.getDeclaredConstructor(null);// 获取私有构造方法
        privateC.setAccessible(true);
        System.out.println("私有构造方法:");
        System.out.println(privateC);

        Constructor defaultC = class1.getDeclaredConstructor(int.class);// 获取访问修饰符为”默认“的构造方法
        System.out.println("访问修饰符为”默认“的构造方法:");
        System.out.println(defaultC);

        Constructor protectedC = class1.getDeclaredConstructor(int.class, String.class);// 获取访问修饰符为”protected“的构造方法
        System.out.println("访问修饰符为”protected“的构造方法:");
        System.out.println(protectedC);

        Constructor publicC1 = class1.getDeclaredConstructor(int.class, String.class, boolean.class);// 获取访问修饰符为”public“的构造方法
        System.out.println("访问修饰符为”public“的构造方法:");
        System.out.println(publicC1);

        Constructor publicC2 = class1.getConstructor(int.class, String.class, boolean.class);// 获取访问修饰符为”public“的构造方法
        System.out.println("访问修饰符为”public“的构造方法:");
        System.out.println(publicC2);
    }

测试结果:

由于构造方法名称固定,故在获取构造方法时,只需要指定相应构造方法所有形参的 Class 对象即可。

2.1.3 构造实例

测试示例:

class TestReflect {
    public static void main(String[] args) throws Exception {
        Class class1 = Class.forName("com.neusoft.boot.Reflect");

        Constructor publicC1 = class1.getDeclaredConstructor(int.class, String.class, boolean.class);// 获取访问修饰符为”public“的构造方法
        System.out.println("访问修饰符为”public“的构造方法:");
        System.out.println(publicC1);;

        Reflect d1 = (Reflect)publicC1.newInstance(10, "yc", true);// 构造方法实例化
        System.out.println("一个Reflect对象;");
        System.out.println(d1);
    }
}

测试结果:
在这里插入图片描述

2.2 方法

2.2.1 获取方法

相应获取方法:

1、getMethod(a, b)	// 获取公共(public)方法
2、getDeclaredMethod(a, b)	//  获取方法,包括:private、默认(未指定访问修饰符)、protected、public

待反射类:

class Reflect {
    private void print(String msg) {
        System.out.println("打印信息:" + msg);
    }

    public static void main(String[] args) {
        System.out.println(Arrays.toString(args));
    }
}

测试示例:

class TestReflect {
    public static void main(String[] args) throws Exception {
        Class class1 = Class.forName("com.neusoft.boot.Reflect");

        Constructor publicC = class1.getDeclaredConstructor(null);// 获取默认构造方法
        Reflect o1 = (Reflect)publicC.newInstance(null);// 实例化

        Method m1 = class1.getDeclaredMethod("print", String.class);	// 获取方法名为print,具有一个String类型参数的方法
        m1.setAccessible(true);// 强制访问
        m1.invoke(o1, "反射方法测试");
    }
}

测试结果:

在示例中,先通过反射获取默认无参构造方法(由JVM提供,因为未自定义构造方法),再调用newInstance(null)创建实例(因为构造方法无参,故无实参,为null)。

由于方法可重载,故获取方法时,需要指定方法名和所有形参的 Class 对象。
代码中的invoke()的作用是调用方法。

注意,

  1. 若方法是成员方法,则第一个参数是对象,表示调用哪个对象的成员方法;第二个参数是实参集;
    注:方法形参不一定只有一个,因此实参集中实参可能有多个。实参集的写法类似可变参数
  2. 若方法是类方法,由于类方法属于类,不属于对象,故不需要指定对象,因此第一个参数为 null;第二个参数同上。

2.2.2 一个特例:通过反射调用 main()

看下述代码:

class TestReflect {
    public static void main(String[] args) throws Exception {
        Class class1 = Class.forName("com.neusoft.boot.Reflect");

        Method m2 = class1.getMethod("main", String[].class);
        System.out.println("调用main()");
        m2.invoke(null, (Object) new String[]{"a", "b"});
    }
}

对于以此情形调用main()是否会重新启动了一个JVM,暂未深究。

注意:
若方法形参类型为数组,如上述main(),在调用invoke()时,实参必须强转为Object

2.3 变量

相应获取方法:

1、getField(xx)	// 获取公共(public)变量
2、getDeclaredField(xx)	//  获取变量,包括:private、默认(未指定访问修饰符)、protected、public

待反射类:

class Reflect {
    private static String name;
    private int age; 
}

测试示例:

class TestReflect {
    public static void main(String[] args) throws Exception {
        Class class1 = Class.forName("com.neusoft.boot.Reflect");

        Constructor publicC = class1.getDeclaredConstructor(null);// 获取默认构造方法
        Reflect o1 = (Reflect)publicC.newInstance(null);// 实例化

        Field ageField = class1.getDeclaredField("age");// 获取名为age的变量
        ageField.setAccessible(true);
        ageField.set(o1, 100);// 为对象o1的变量age赋值
        System.out.println("打印对象:");
        System.out.println(o1);

        Object value = ageField.get(o1);// 获取对象o1的变量age值
        System.out.println("对象o1的变量age的值为:");
        System.out.println(value);
    }
}

测试结果:

由于变量具有唯一性,故只需要指定变量名。

赋值和获取。

ageField.set(o1, 100);// 为对象o1的变量age赋值
Object value = ageField.get(o1);// 获取对象o1的变量age值

成员变量、类变量的赋值和获取与成员方法、类方法类似,故不赘述。

3、运用反射时的注意事项

1、若获取的类成员由非public修饰,则存在访问限制,在执行功能前,必须先调用xx.setAccessible(true),目的是设置为允许强制访问特别的:在默认情况下,私有成员不允许访问。(坦白:我忘了最后这一点的出处,所以暂且只能作为一个结论)

2、当通过newInstance()实例化时,若调用的构造方法为无参构造方法,括号内可为null或空。

3、无法通过使用子类的 Class 对象进行反射获取任何父类成员,父类同样。
其中缘由:

  1. 子类可访问父类所有成员,而并非拥有
  2. 在JVM内存空间的中,父类初始化数据存储于子类内存空间。而反射执行的位置是在方法区,自然无法获取到父类成员。

详述可查阅博文《Java知识点锦集》的第5.4项、第8项。

一种特殊情况:
当父类的成员变量或成员方法以public修饰时(没有其他修饰符),通过getField()/getMethod()可获取。

难道真的没办法获取父类成员?
当然不是。无论 Class 对象还是实例,有一点是确定的:子类可访问父类成员。那么,就可以从此处着手。
具体办法:(目前仅限于获取父类变量。至于其他成员,由于实用性不大,故暂不探讨)

  • 办法一:将父类变量作为子类方法的返回值;
  • 办法二:先获取父类的 Field 对象,调用get()时,传入子类实例。

4、通过反射无法获取抽象类或接口的方法。

5一个误区:定义方法void get(Object obj) {},调用时,实参类型可以任意,但当通过class.getMethod("get", xx)获取此方法时,xx只能是Object.class,因为每个类的 Class 对象唯一且不存在继承关系

6、获取内部类的 Class 对象,需使用特殊符号$
示例:(获取ArrayList<E>的嵌套类-迭代器类Itr的 Class 对象)

1、Class.forName("java.util.ArrayList$Itr");	√
2、Class.forName("java.util.ArrayList.Itr");	×

7一个结论:反射的本质其实就是加载类的 Class 信息、生成 Class 对象的过程。类与类之间可能存在关联,如:包含、继承或依赖等,但类的 Class 信息一定是唯一且独立的。因此,无法通过一个类的 Class 对象获取另一个类的成员。
对于在第3点中提到:“子类可以通过getField()/getMethod()获取父类成员变量和成员方法”,那是因为这2个方法的底层存在父类递归机制(从源码中获知,具体待明)。
注意:构造方法没有此性质。

4、列举两个反射在实际开发中的运用

4.1 降低代码注入

上文中阐述的各种获取类成员的方法的实参都是“写死”在程序中的,代码注入性太强。

什么是“代码注入”?
大家可能是第一次听说这个概念,比较抽象,不容易理解,我举个例:类A通过反射获取类B的变量、方法等,其中,类B的全限定名变量名方法名 等都“写死”。可现在类B的各种类信息改了。那么,你就需要去看懂类A中反射那部分代码,然后一一进行修改,是不是很耗费时间、精力。这就是“代码注入性”太强。

通过反射降低代码注入性的方法:

用配置文件封装各种实参,修改时可以统一修改,且不需要考虑代码细节。

测试示例:
待反射类:

class Reflect {
    private void print(String msg) {
        System.out.println("打印信息:" + msg);
    }
}

配置文件:

classPath=com.neusoft.boot.Reflect	// 类全限定名
methodName=print	// 方法名

测试类:

class TestReflect {
    public static void main(String[] args) throws Exception {
        Class class1 = Class.forName(getConfig("classPath"));

        Constructor publicC = class1.getDeclaredConstructor(null);// 获取默认构造方法
        Reflect o1 = (Reflect)publicC.newInstance(null);// 实例化

        Method m1 = class1.getDeclaredMethod(getConfig("methodName"), String.class);	// 获取方法名为print,具有一个String类型参数的方法
        m1.setAccessible(true);// 强制访问
        m1.invoke(o1, "反射降低代码注入性测试");// 输出结果【打印信息:反射降低代码注入性测试】
    }

    /**
     * 获取配置
     *
     * @param key
     * @return
     */
    private static String getConfig(String key) throws Exception {
        Properties p = new Properties();
        // 配置类Properties加载配置文件的方法很多,这里举个例
        String filePath = "G:\\projects-local\\java\\boot-demo\\src\\main\\resources\\Reflect-confg.properties";
        p.load(new FileReader(filePath));
        return p.getProperty(key);
    }
}

4.2 跳过泛型检查

一个大家看过无数次的例子:

class TestReflect {
    public static void main(String[] args) throws Exception {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add("4");// 编译错误
    }
}

在编译时,JVM会进行泛型检查,目的是判断所赋的值或加入的值的类型是否与类型实参相同。
反射的底层机制是类加载,不经过编译,故可以跳过泛型检查。

示例:运用反射向List<Integer>集合内添加字符串。

class TestReflect {
    public static void main(String[] args) throws Exception {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);

        Class listClass = list.getClass();
        Method addMethod = listClass.getMethod("add", Object.class);// 反射获取方法,与泛型的具体类型无关,所以是Object
        addMethod.invoke(list, 4);// 成功
        addMethod.invoke(list, "5");// 成功
        addMethod.invoke(list, "5ab");// 成功

        System.out.println(list);// 打印:【1,,2, 3, 4, 5, 5ab】
    }
}

为什么List<Integer>可以存放字符串?

关于泛型,推荐一位前辈的博文《java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一》(转发) 。
如果大家对那篇博文中的一些概念晦涩不清,可以浏览一下我写的这篇文章《关于对Java泛型的理解与简述(读后简结)》。

无论是泛型接口、泛型类,亦或者泛型方法,泛型的限制作用都在于泛型检查作用于编译阶段,例如上述的addMethod.invoke(list, "5ab"),是通过反射获取的 Method 对象,直接将字符串"5ab"加入到list中,不经过编译,故跳过了泛型检查。

5、最后

本文中的例子是为了方便大家理解、以及阐述如何通过反射获取类成员而简单举例的,不一定有实用性。大家在实际编程中可以尝试用反射去解决问题,有些情况下会简便许多。
之前,我用反射实现过“不同类之间属性值传递”(因为这两个类有几个属性相同或有某种规律,如果逐个get()/set(),代码太冗余、质量和效率都不高)。

本文完结。