【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://blog.csdn.net/m0_69908381/article/details/129627321
出自【进步*于辰的博客】
参考笔记一,P74.6;笔记二,P74.2、P75.3;笔记三,P15.2、P43.2。
关于类加载,详述可查阅博文【【java知识点】锦集】的第5项。
1、A.class
2、new A().getClass()
3、Class.forName()
newInstance()
,使用 Class 对象 创建实例。结论: \color{red}{结论:} 结论:反射是一种通过类加载加载JVM方法区中的 class 信息、创建实例的机制。
补充一点: 补充一点: 补充一点:
class字节码文件(图中B)中包含 字面量 \color{green}{字面量} 字面量和 符号引用 \color{green}{符号引用} 符号引用。字面量指为变量所赋的值;符号引用指变量在编译时的一个地址标识,不是确切的地址,因为只有在运行时,才会为变量分配内存地址。
1、A.class
2、new A().getClass()
3、Class.forName()
在上文中说道,过程 B → D 就是类加载,执行如上代码中任一条都可以触发此过程。
创建 Class 对象是反射的标志,而反射基于类加载。因此,这3种情况都属于反射。可实际上,只有第3种才会触发类加载。为什么?
大家先看个图。
反射的最终目的是实例,可有时候只是为了获取 Class 对象。若已存在实例,则通过调用getClass()
获取会更简便。这种通过对实例进行反编译、进而创建 Class 对象的过程也属于反射,如上图所示。
可这种情形不会触发类加载,因为类加载只会执行一次,既然存在实例,自然已完成了类加载。
为什么要阐明此情形“不会触发类加载”这一细节? \color{grey}{为什么要阐明此情形“不会触发类加载”这一细节?} 为什么要阐明此情形“不会触发类加载”这一细节?
其实这也算是一个基础知识吧,只是我们可能没留意。
平日看源码的时候,经常会看到这样的代码块:
static {}
这个叫做“静态代码块”,它执行于类初始化时(类加载的第三过程)。在这里会编写一些为类变量赋初始值或初始操作的代码,而往往这些代码并不容易看懂,那就需要debug
。
综上所述 \color{red}{综上所述} 综上所述,只有Class.forName()
和 实例化 才会触发类加载,而getClass()
不会。并且,通过debug
发现,A.class
也同样不会触发类加载,故可判断A.class
也是通过反编译进行反射。
大家看一个栗子。
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();
题外话 \color{brown}{题外话} 题外话:
可能大家会疑惑,为什么我不对其他几种内部类的类加载进行说明?原因:
static {}
。在反射的使用中,直接涉及的类是 Class
。
以下3个方法可用于获取构造方法、方法(包括成员方法、类方法)和变量(包括成员变量、类变量)。
getConstructor(xx) // 获取构造方法,xx是构造方法形参的数据类型的class
getMethod(a, b) // 获取方法,包括成员方法和类方法,a是方法名,b是方法形参的数据类型的class集,b位置是可变参数
getField(xx) // 获取变量,包括成员变量和类变量,xx是变量名
笼统列举,大家看起来有点云里雾里,下面一一详述。
待反射类:
@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;
}
}
测试示例:
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);
}
}
}
相应获取方法:
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 对象即可。
测试示例:
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);
}
}
相应获取方法:
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()
的作用是调用方法。
注意, \color{red}{注意,} 注意,
null
;第二个参数同上。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,暂未深究。
注意: \color{red}{注意:} 注意:
若方法形参类型为数组,如上述main()
,在调用invoke()
时,实参必须强转为Object
。
相应获取方法:
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值
成员变量、类变量的赋值和获取与成员方法、类方法类似,故不赘述。
1、若获取的类成员由非public
修饰,则存在 访问限制 \color{red}{访问限制} 访问限制,在执行功能前,必须先调用xx.setAccessible(true)
,目的是设置为允许强制访问。 特别的: \color{red}{特别的:} 特别的:在默认情况下,私有成员不允许访问。(坦白:我忘了最后这一点的出处,所以暂且只能作为一个结论)
2、当通过newInstance()
实例化时,若调用的构造方法为无参构造方法,括号内可为null
或空。
3、无法通过使用子类的 Class 对象进行反射获取任何父类成员,父类同样。
其中缘由:
详述可查阅博文【【java知识点】锦集】的第5.4项、第8项。
一种特殊情况 \color{brown}{一种特殊情况} 一种特殊情况:
当父类的成员变量或成员方法以public
修饰时(没有其他修饰符),通过getField()/getMethod()
可获取。
难道真的没办法获取父类成员? \color{grey}{难道真的没办法获取父类成员?} 难道真的没办法获取父类成员?
当然不是。无论 Class 对象还是实例,有一点是确定的:子类可访问父类成员。那么,就可以从此处着手。
具体办法:(目前仅限于获取父类变量。至于其他成员,由于实用性不大,故暂不探讨)
get()
时,传入子类实例。4、通过反射无法获取抽象类或接口的方法。
5、 一个误区 \color{green}{一个误区} 一个误区:定义方法void get(Object obj) {}
,调用时,实参类型可以任意,但当通过class.getMethod("get", xx)
获取此方法时,xx
只能是Object.class
,因为每个类的 Class 对象唯一且不存在继承关系。
6、获取内部类的 Class 对象,需使用特殊符号$
。
示例:(获取ArrayList
类的嵌套类-迭代器类Itr
的 Class 对象)
1、Class.forName("java.util.ArrayList$Itr"); √
2、Class.forName("java.util.ArrayList.Itr"); ×
7、 一个结论 \color{red}{一个结论} 一个结论:反射的本质其实就是加载类的 Class 信息、生成 Class 对象的过程。类与类之间可能存在关联,如:包含、继承或依赖等,但类的 Class 信息一定是唯一且独立的。因此,无法通过一个类的 Class 对象获取另一个类的成员。
对于在第3点中提到:“子类可以通过getField()/getMethod()
获取父类成员变量和成员方法”,那是因为这2个方法的底层存在 父类递归机制 \color{DoderBlue}{父类递归机制} 父类递归机制(从源码中获知,具体待明)。
注意: \color{Cornislk}{注意:} 注意:构造方法没有此性质。
上文中阐述的各种获取类成员的方法的实参都是“写死”在程序中的,代码注入性太强。
什么是“代码注入”? \color{grey}{什么是“代码注入”?} 什么是“代码注入”?
大家可能是第一次听说这个概念,比较抽象,不容易理解,我举个例:类A通过反射获取类B的变量、方法等,其中,类B的全限定名、变量名、方法名 等都“写死”。可现在类B的各种类信息改了。那么,你就需要去看懂类A中反射那部分代码,然后一一进行修改,是不是很耗费时间、精力。这就是“代码注入性”太强。
通过反射降低代码注入性的方法: \color{green}{通过反射降低代码注入性的方法:} 通过反射降低代码注入性的方法:
用配置文件封装各种实参,修改时可以统一修改,且不需要考虑代码细节。
测试示例:
待反射类:
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);
}
}
一个大家看过无数次的例子:
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会进行泛型检查,目的是判断所赋的值或加入的值的类型是否与 类型实参 \color{brown}{类型实参} 类型实参相同。
反射的底层机制是 类加载 \color{red}{类加载} 类加载,不经过编译,故可以跳过泛型检查。
示例:运用反射向List
集合内添加字符串。
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】
}
}
为什么 \color{grey}{为什么} 为什么List
可以存放字符串? \color{grey}{可以存放字符串?} 可以存放字符串?
关于泛型,详述可查阅一位前辈的博文【java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一】(转发) 。
如果大家对那篇博文中的一些概念晦涩不清,可以浏览一下我写的这篇文章【关于对【泛型_java】的理解与简述(读后简结)】。
无论是泛型接口、泛型类,亦或者泛型方法,泛型的限制作用都在于 泛型检查 \color{red}{泛型检查} 泛型检查,作用于编译阶段,例如上述的addMethod.invoke(list, "5ab")
,是通过反射获取的 Method 对象,直接将字符串"5ab"
加入到list
中,不经过编译,故跳过了泛型检查。
本文中的例子是为了方便大家理解、以及阐述如何通过反射获取类成员而简单举例的,不一定有实用性。大家在实际编程中可以尝试用反射去解决问题,有些情况下会简便许多。
之前,我用反射实现过“ 不同类之间属性值传递 \color{brown}{不同类之间属性值传递} 不同类之间属性值传递”(因为这两个类有几个属性相同或有某种规律,如果逐个get()/set()
,代码太冗余、质量和效率都不高)。
本文完结。