【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
www.cnblogs.com/cnb-yuchen/...出自【进步*于辰的博客】
参考笔记一,P74.6;笔记二,P74.2、P75.3;笔记三,P15.2、P43.2、P44.2/3。
目录
-
- [1.1 概述](#1.1 概述 "#11-%E6%A6%82%E8%BF%B0")
- [1.2 反射的另一种情形](#1.2 反射的另一种情形 "#12-%E5%8F%8D%E5%B0%84%E7%9A%84%E5%8F%A6%E4%B8%80%E7%A7%8D%E6%83%85%E5%BD%A2")
- [1.3 扩展:静态内部类的类加载](#1.3 扩展:静态内部类的类加载 "#13-%E6%89%A9%E5%B1%95%E9%9D%99%E6%80%81%E5%86%85%E9%83%A8%E7%B1%BB%E7%9A%84%E7%B1%BB%E5%8A%A0%E8%BD%BD")
-
-
[2.1 构造方法](#2.1 构造方法 "#21-%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95")
- [2.1.1 获取构造方法数组](#2.1.1 获取构造方法数组 "#211-%E8%8E%B7%E5%8F%96%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95%E6%95%B0%E7%BB%84")
- [2.1.2 获取指定构造方法](#2.1.2 获取指定构造方法 "#212-%E8%8E%B7%E5%8F%96%E6%8C%87%E5%AE%9A%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95")
- [2.1.3 构造实例](#2.1.3 构造实例 "#213-%E6%9E%84%E9%80%A0%E5%AE%9E%E4%BE%8B")
-
[2.2 方法](#2.2 方法 "#22-%E6%96%B9%E6%B3%95")
- [2.2.1 获取方法](#2.2.1 获取方法 "#221-%E8%8E%B7%E5%8F%96%E6%96%B9%E6%B3%95")
- [2.2.2 一个特例:通过反射调用
main()
](#2.2.2 一个特例:通过反射调用 main() "#222-%E4%B8%80%E4%B8%AA%E7%89%B9%E4%BE%8B%E9%80%9A%E8%BF%87%E5%8F%8D%E5%B0%84%E8%B0%83%E7%94%A8-main")
-
[2.3 变量](#2.3 变量 "#23-%E5%8F%98%E9%87%8F")
-
-
- [4.1 降低代码注入](#4.1 降低代码注入 "#41-%E9%99%8D%E4%BD%8E%E4%BB%A3%E7%A0%81%E6%B3%A8%E5%85%A5")
- [4.2 跳过泛型检查](#4.2 跳过泛型检查 "#42-%E8%B7%B3%E8%BF%87%E6%B3%9B%E5%9E%8B%E6%A3%80%E6%9F%A5")
1、什么是"反射"?
关于类加载,详述可查阅博文《Java知识点锦集》的第5项。
1.1 概述
大家先看一个图,
过程说明:
- A → B。当JVM运行,将 java 源文件编译成 class 字节码文件。
- B → D。执行如下代码,通过类加载器 ClassLoader 将 class 字节码文件加载进JVM方法区 、生成 class 信息、进而创建 Class 对象,这个过程就是"类加载"。(注:只有对类的主动使用才会触发类加载,例如:反射、实例化)。
java
1、A.class
2、new A().getClass()
3、Class.forName()
- D → E。通过调用
newInstance()
,使用 Class 对象创建实例。
结论:反射是一种通过类加载加载JVM方法区中的 class 信息、创建实例的机制。
补充一点:
class字节码文件(图中B)中包含字面量和符号引用。字面量指为变量所赋的值;符号引用指变量在编译时的一个地址标识 ,不是确切的地址,因为只有在运行时,才会为变量分配内存地址。
1.2 反射的另一种情形
java
1、A.class// A 是类名
2、new A().getClass()
3、Class.forName()
在上文中说道,过程 B → D 就是类加载,执行如上代码中任一条都可以触发此过程。
创建 Class 对象是反射的标志,而反射基于类加载。因此,这三种情况都属于反射。可实际上,只有第3种才会触发类加载。听我细细道来。。。
大家先看个图。
反射的最终目的是实例,可有时候只是为了获取 Class 对象。若已存在实例,则通过调用getClass()
获取会更简便。
这种通过对实例进行反编译、进而创建 Class 对象的机制也属于反射,
可这种情形不会触发类加载,因为类加载只会执行一次,既然存在实例,自然已完成了类加载。
我为何会注意到"反编译不会触发类加载"这一细节?
平日看源码的时候,经常会看到这样的代码块:
java
static {}
这个叫做"静态代码块",它执行于类初始化时(类加载的第三过程)。在这里会编写一些为类变量赋初始值或初始操作的代码,而往往这些代码并不容易看懂,那就需要debug
。(PS :进行debug
前当然需要先知道什么情况下才会执行"静态代码块")
总结 :只有Class.forName()
和 实例化 才会触发类加载,而getClass()
不会。并且,通过debug
发现,A.class
也同样不会触发类加载,故可判断A.class
也是通过反编译进行反射。
1.3 扩展:静态内部类的类加载
大家看一个栗子。
java
class OuterClass {
static class InnerClass {
static {
sout "csdn";
}
}
}
什么情况下才会打印"csdn"
?据上文可知,只要进行类加载,就会执行static {}
。
虽然内部类属"懒加载",但其类加载在本质上与外部类的类加载相同,即当执行Class.forName()
或实例化时才会触发类加载。如下述代码:
java
1、Class z1 = Class.forName("OuterClass$InnerClass");
2、OuterClass.InnerClass obj1 = new OuterClass.InnerClass();
补充说明:为什么不能在类方法中实例化非静态内部类,而静态内部类可以?
因为类方法加载于类加载时,而非静态内部类属"懒加载",在外部类调用时才加载。换言之,类加载时不会加载非静态内部类(可视为不存在),自然无法实例化。
而静态内部类同外部类一起加载(可视为"积极加载"),自然可以实例化。
PS :
可能大家会疑惑,为什么我不对其他几种内部类的类加载进行说明?原因:
- 关于其他几种内部类的类加载我暂未研究;
- 只有静态内部类内才能定义
static {}
。
具体原因可查阅博文《Java知识点锦集》的第15项。
2、反射运用(获取类成员)
在反射的使用中,直接涉及的类是 Class
<T>
。以下3个方法可用于获取构造方法、方法(包括成员方法、类方法)和变量(包括成员变量、类变量)。
java
getConstructor(xx) // 获取构造方法,xx是构造方法形参的数据类型的class
getMethod(a, b) // 获取方法,包括成员方法和类方法,a是方法名,b是方法形参的数据类型的class集,b位置是可变参数
getField(xx) // 获取变量,包括成员变量和类变量,xx是变量名
笼统列举,大家看起来有点云里雾里,下面一一详述。
2.1 构造方法
待反射类:
java
@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 获取构造方法数组
测试示例:
java
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 获取指定构造方法
相应获取方法:
java
1、getConstructor(xx) // 获取公共(public)构造方法
2、getDeclaredConstructor(xx) // 获取构造方法,包括:private、默认(未指定访问修饰符或类中未自定义构造方法)、protected、public
测试示例:
java
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 构造实例
测试示例:
java
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 获取方法
相应获取方法:
java
1、getMethod(a, b) // 获取公共(public)方法
2、getDeclaredMethod(a, b) // 获取方法,包括:private、默认(未指定访问修饰符)、protected、public
待反射类:
java
class Reflect {
private void print(String msg) {
System.out.println("打印信息:" + msg);
}
public static void main(String[] args) {
System.out.println(Arrays.toString(args));
}
}
测试示例:
java
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()
的作用是调用方法。
注意,
- 若方法是成员方法,则第一个参数是对象,表示调用哪个对象的成员方法;第二个参数是实参集;
注:方法形参不一定只有一个,因此实参集中实参可能有多个。实参集的写法类似可变参数。 - 若方法是类方法,由于类方法属于类,不属于对象,故不需要指定对象,因此第一个参数为
null
;第二个参数同上。
2.2.2 一个特例:通过反射调用 main()
看下述代码:
java
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 变量
相应获取方法:
java
1、getField(xx) // 获取公共(public)变量
2、getDeclaredField(xx) // 获取变量,包括:private、默认(未指定访问修饰符)、protected、public
待反射类:
java
class Reflect {
private static String name;
private int age;
}
测试示例:
java
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);
}
}
测试结果:
由于变量具有唯一性,故只需要指定变量名。
赋值和获取。
java
ageField.set(o1, 100);// 为对象o1的变量age赋值
Object value = ageField.get(o1);// 获取对象o1的变量age值
成员变量、类变量的赋值和获取与成员方法、类方法类似,故不赘述。
3、运用反射时的注意事项
1 、若获取的类成员由非public
修饰,则存在访问限制 ,在执行功能前,必须先调用xx.setAccessible(true)
,目的是设置为允许强制访问 。特别的:在默认情况下,私有成员不允许访问。(坦白:我忘了最后这一点的出处,所以暂且只能作为一个结论)
2 、当通过newInstance()
实例化时,若调用的构造方法为无参构造方法,括号内可为null
或空。
3 、无法通过使用子类的 Class 对象进行反射获取任何父类成员,父类同样。
其中缘由:
- 子类可访问父类所有成员,而并非拥有;
- 在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 对象)
java
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中反射那部分代码,然后一一进行修改,是不是很耗费时间、精力。这就是"代码注入性"太强。
通过反射降低代码注入性的方法:
用配置文件封装各种实参,修改时可以统一修改,且不需要考虑代码细节。
测试示例:
待反射类:
java
class Reflect {
private void print(String msg) {
System.out.println("打印信息:" + msg);
}
}
配置文件:
java
classPath=com.neusoft.boot.Reflect // 类全限定名
methodName=print // 方法名
测试类:
java
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 跳过泛型检查
一个大家看过无数次的例子:
java
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>
集合内添加字符串。
java
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()
,代码太冗余、质量和效率都不高)。
本文完结。