【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://www.cnblogs.com/cnb-yuchen/p/17960654
出自【进步*于辰的博客】
参考笔记二,P75.3;笔记三,P15.2、P43.2、P44.2、P64.3、P69.1。
1、什么是“反射”?
关于类加载,详述可查阅博文《[Java]知识点》中的【类加载】一栏。
1.1 概述
大家先看一个图,
过程说明:
- A → B。当JVM运行,将Java源文件编译成class字节码文件。
- B → D。当如下代码执行时,JVM通过类加载器 ClassLoader 将 class 字节码文件加载进JVM方法区、生成 class 信息、进而创建 Class 对象,这个过程就是类加载。(注:只有对类的主动使用才会触发类加载,例如:反射、实例化)。
1、A.class;// A 是类名
2、new A().getClass();
3、Class.forName();
- D → E。通过调用
newInstance()
,使用 Class 对象创建实例。
总结:反射是一种通过类加载加载磁盘中的class字节码文件、创建实例的机制。
1.2 反射的另一种情形
先说一个结论:
通过对实例进行反编译、进而创建 Class 对象的机制也属于反射,
(PS:坦白说,我是通过“生成Class对象是反射的标志”来得出这个结论的,目前这个结论没有理论支持。当然,““生成Class对象是反射的标志””这个结论也没有理论支持,是我本人对反射的理解。不过,大家暂且可以相信。有不对之处,还请大家指正)
步入正题:
1、A.class;
2、new A().getClass();
3、Class.forName();
由上文可知,生成 Class 对象是反射的标志,以上这3条代码都可创建 Class 对象。所以这三种情形都是反射,反射基于类加载,那是不是都触发了类加载?
实际上,只有第3种才会触发类加载。下面我一一证明。
大家先看个图,这是通过反编译,使用实例生成 Class 对象的过程。(PS:这就是开头结论所说的“反射”)
大家看出来了吧,这就是getClass()
执行的过程,可这个过程不会触发类加载。为什么?因为类加载只会执行一次,既然存在实例,自然已完成了类加载。
结论一:
getClass()
是反射,但不会触发类加载。
反射的最终目的是实例,可有时候只是为了获取 Class 对象。若已存在实例,则通过调用getClass()
获取会更简便。
我为何要特别说明“反编译不会触发类加载”这一细节?
平日看源码的时候,经常会看到这样的代码块:
static {}
这个叫做“静态代码块”,它执行于类初始化时(类加载的第三过程)。在这里会编写一些为类变量赋初始值或初始操作的代码,而往往这些代码并不容易看懂,那就需要debug
。(PS:进行debug
前当然需要先知道什么情况下才会执行static {}
)
总结:
只有
Class.forName()
和 实例化 才会触发类加载,而getClass()
不会。并且,A.class
是反射,测试得出,A.class
也同样不会触发类加载,故可判断A.class
也是通过反编译进行反射,自然也不会执行static {}
。
PS:的确,这个结论不是很严谨。不过,我们学习,很多时候不都是“从结论看过程”嘛。
1.3 扩展
1:静态内部类的类加载。
大家看一个栗子。
class A {
static class B {
static {
sout "csdn";
}
}
}
什么情况下才会打印"csdn"
?据上文可知,只要进行类加载,就会执行static {}
。
虽然内部类属“懒加载”,但其类加载在本质上与外部类的类加载相同,即当执行Class.forName()
或实例化时就会触发类加载。也就是这样:
1、Class bClass = Class.forName("A$B");
2、B b1 = new A.B();
2:为什么不能在类方法中实例化非静态内部类,而静态内部类可以?
因为类方法加载于类加载时,而非静态内部类属“懒加载”,在外部类调用时才加载。换言之,外部类类加载时不会加载非静态内部类(可视为不存在),自然无法实例化。
而静态内部类同外部类一起加载(可视为“积极加载”),自然可以实例化。
PS:
可能大家会疑惑,为什么我不对其他几种内部类的类加载进行说明?因为:
- 对于其他几种内部类的类加载我暂未研究;
- 只有静态内部类内才能定义
static {}
。(具体说明可查阅博文《[Java]知识点》中的【static关键字】一栏)
3:在外部类已加载(如:已实例化)的情况下,静态内部类与非静态内部类的状态如何(两者都还未使用)?
先说静态内部类。静态内部类属于“积极加载”,会跟随外部类一同加载。因此,此时静态内部类已分配内存(存在引用),各个成员都为默认值。
再说非静态内部类。非静态内部类属于“懒加载”,只有当外部类使用时才开始加载。因此,此时JVM中还不存在非静态内部类的Class信息。又因为非静态内部类也是外部类的成员,外部类已加载,所有成员为默认值,故非静态内部类为null
。
注意:虽然内部类是外部类的成员,但与成员变量不同,以上说“非静态内部类为null
”是根据理论推导得出,实际无法在以上条件下测试。因此,大家可理解为“可视为非静态内部类为null
”。
2、如何使用反射?
2.1 概述
我们使用反射是为了什么?自然是获取类成员。在反射的使用中,直接涉及的类是Class。也就是说,我们是通过Class类的成员方法来获取各种类成员。
以下3个方法可分别用于获取构造方法、方法(包括成员方法、类方法)和变量(包括成员变量、类变量)。
// 获取构造方法,xx 是构造方法形参的数据类型的 Class对象
getConstructor(xx);
// 获取方法,包括成员方法和类方法,a 是方法名,b 是方法形参的数据类型的Class对象,故 b 的位置是可变参数
getMethod(a, b);
// 获取变量,包括成员变量和类变量,xx 是变量名
getField(xx);
这3个方法,为何通过这些参数,就可以定位到具体的成员?大家想想这3种成员的特点就明白了。
大家看到这里,肯定产生了两个问题:
- 有没有可以获取对应所有成员的方法?
- 以上这3个方法,可以在所有范围内(
private、protected、default、public
),获取到对应的指定成员吗?
大家点开Class类的源码,就可以看到,有一些以s
结尾的成员方法,那就是第一个问题的答案。同时,也有一些以getDeclared
开头的成员方法,如:getConstructor()
与getDeclaredConstructor()
,两者有什么区别?前者返回的是公共(public
)构造方法,后者返回的是所有构造方法。同理,获取另外两种成员的成员方法也是这样,大家自行测试一下就都明白了。
2.2 综合示例
为了便于大家阅读,我以大家最熟悉的String类为例。
(PS:我会尽量在这个示例内,简洁明了地将三种成员的获取与使用展示出来,关键的是方法的使用,大家注意形参和返回值,功能不重要)
Class z1 = Class.forName("java.lang.String");
// 获取构造方法 String(char[] value, boolean share)
Constructor c1 = z1.getDeclaredConstructor(char[].class, boolean.class);
c1.setAccessible(true);
sout c1;// java.lang.String(char[],boolean)
char[] arr1 = {'c', 's', 'd', 'n'};
String s1 = (String) c1.newInstance(arr1, true);// 获取示例
sout s1;// csdn
/**
* 大家看过String类源码的都知道,String类的底层存储结构是 private final char value[],里面存储着字符串的字符序列。
*/
// 获取变量 value
Field f1 = z1.getDeclaredField("value");
f1.setAccessible(true);
// 获取值
sout Arrays.toString((char[]) f1.get(s1));// [c, s, d, n]
// 修改 value[]
char[] arr2 = {'博', '客', '园'};
f1.set(s1, arr2);
// 再次获取值
sout Arrays.toString((char[]) f1.get(s1));// [博, 客, 园]
/**
* 获取大家比较熟悉的方法 public String substring(int beginIndex, int endIndex),
* 截取,返回 [beginIndex, endIndex) 的子字符串。
*/
Method m1 = z1.getMethod("substring", int.class, int.class);
String subS1 = (String) m1.invoke(s1, 0, 2);// 调用 substring()
sout subS1;// 博客
PS:简单举例,相信大家已经有了初步掌握,其它的就需要大家自行测试了。
下面我补充一点使用细节:
1:构造方法。
获取无参构造方法时,参数列表可以是()
或(null)
,调用newInstance()
获取实例时同样。
2:变量。
无论成员变量、类变量,都具有唯一性,故如getField()
都可以获取。获取变量值是f1.get(obj)
,obj
是实例,表示获取哪个实例的变量值。因此,如果f1
是类变量时,类变量属于类,故obj
是null
。
3:方法。
与变量同理。调用invoke()
时,第 2 个参数后采用了“可变参数”。
2.3 一个特例:通过反射调用 main()
PS:相信大家看到这里,对反射已经比较熟悉了,下面这个示例我写快点。
class E {
static class EE {
public static void main(String[] args) {
System.out.println("haha");
}
}
public static void main(String[] args) throws Exception {
Method m1 = EE.class.getMethod("main", String[].class);
m1.invoke(null, (Object) args);// 打印:haha
}
}
在调用invoke()
时,实参必须强转为Object
,且强转前类型必须是String[]
。(注:这是反射main()
时需要注意的,若是其他方法,则不需要)
3、注意
1:若获取的成员由非public
修饰,则存在访问限制,在执行功能前,必须先调用setAccessible(true)
,目的是设置为允许强制访问。
2:无法通过使用子类的 Class 对象进行反射获取任何父类成员,反之亦然。因为:
- 子类可访问父类所有成员,而并非拥有,
- 在JVM内存空间的堆中,父类初始化数据存储于子类内存空间。而反射执行的位置是在方法区,自然无法获取到父类成员。(详述可查阅博文《[Java]知识点》中的【类加载】一栏)
一种特殊情况:当父类的成员变量或成员方法由public
修饰时(没有其他修饰符),通过getField()/getMethod()
可获取。
难道真的没办法获取父类成员?
当然不是。无论 Class 对象还是实例,有一点是确定的:子类可访问父类成员。那么,就可以从此处着手。
具体办法:(目前仅限于获取父类变量。至于其他成员,由于实用性不大,故暂不探讨)
- 办法一:将父类变量作为子类方法的返回值;
- 办法二:先获取父类的 Field 对象,调用
get()
时,传入子类实例。
3:通过反射无法获取抽象类或接口的方法。
4:一个误区:定义方法void get(Object obj) {}
,调用时,实参类型可以任意,但当通过class.getMethod("get", xx)
获取此方法时,xx
只能是Object.class
,因为每个类的 Class 对象唯一且不存在继承关系。
5:获取内部类的 Class 对象,需使用特殊符号$
。
示例:(获取ArrayList类的嵌套类-迭代器类Itr
的 Class 对象)
1、Class.forName("java.util.ArrayList$Itr"); √
2、Class.forName("java.util.ArrayList.Itr"); ×
6:一个结论:
反射的本质其实就是加载 class 字节码文件、生成 Class 对象的过程。类与类之间可能存在关联,如:组合、继承或依赖等,但类的 Class 信息一定是唯一且独立的。因此,无法通过一个类的 Class 对象获取另一个类的成员。
对于在第2点中提到:“子类可以通过getField()/getMethod()
获取父类成员变量和成员方法”,那是因为这2个方法的底层存在“父类递归机制”(从源码中获知,具体暂不明)。注意:构造方法没有此性质。
4、反射运用:跳过泛型检查
一个大家看过无数次的栗子:
ArrayList<Integer> list = new ArrayList<>();
list.add(2021);
list.add("csdn");// 编译错误
在编译时,JVM会进行泛型检查,目的是判断所赋的值或加入的值的类型是否与泛型的类型实参相同。
反射的底层机制是类加载,不经过编译,故可以跳过泛型检查。
示例:使用反射向List<Integer>
集合内添加字符串。
ArrayList<Integer> list = new ArrayList<>();
list.add(2021);
// 反射获取方法,与泛型的类型实参无关,所以是Object
Method addMethod = ArrayList.class.getMethod("add", Object.class);
addMethod.invoke(list, "csdn");// 成功
sout list;// [2021, csdn]
为什么List<Integer>
可以存放字符串?
关于泛型,推荐一位前辈的博文《java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一》(转发) 。
如果大家对那篇博文中的一些概念晦涩不清,可以浏览一下我写的这篇文章《[Java]泛型》。
无论是泛型接口、泛型类,亦或者泛型方法,泛型的限制作用都在于泛型检查,作用于编译阶段,例如示例中的addMethod.invoke(list, "csdn")
,是通过反射获取的 Method 对象,直接将字符串"csdn"
加入到list
中,不经过编译,故跳过了泛型检查。
最后
class字节码文件中包含 字面量 和 符号引用。
1:什么是字面量?
字面量也称为“字面常量”,顾名思义,就是表面上看到的,它的表示(名称)就是它的值,
“字面量”是解释型语言中的常用的概念,如:a = 1
,a 是变量,1 是字面量;编译型语言中少用,对应的是“常量”,如:int a = 1
,a 是变量,1 是常量。
2:什么是符号引用?
符号引用指变量在编译时的一个地址标识,不是确切的地址,因为只有在运行时,才会为变量分配内存地址。
如:String s = "csdn"
,这个 s 写在代码中,在编译之前,就叫做“符号引用”;在编译后,称为“引用”,对应JVM内存地址。
PS:诸如这些,都是一些概念,是为各种数据所赋予的一种“称谓”。
如果大家想要更深入地了解和掌握类加载与反射,可查阅博文《Java基础之—反射(非常重要)》(转发)。
本文完结。
标签:反射,Java,成员,Class,获取,class,加载 From: https://www.cnblogs.com/cnb-yuchen/p/17960654