一个反射问住一大堆人
点击蓝色字免费订阅,每天收到这样的好信息
单单是问反射有什么用,其实最常用的就两个:
-
根据类名创建实例(类名可以从配置文件读取,不用new,达到解耦)
-
用Method.invoke执行方法
但是这些其实不难理解,难的是反射本身。如果有兴趣可以往下看:
由于反射本身确实抽象(说是Java中最抽象的概念也不为过),所以我当初写作时也用了大量的比喻。但是比喻有时会让答案偏离得更远。前阵子看了些讲设计模式的文章,把比喻都用坏了。有时理解比喻,竟然要比理解设计模式本身还费劲...那就南辕北辙了。所以,这一次,能不用比喻就尽量不用,争取用最实在的代码去解释。
主要内容:
-
JVM是如何构建一个实例的
-
.class文件
-
类加载器
-
Class类
-
反射API
JVM是如何构建一个实例的
下文我会使用的名词及其对应关系
-
内存:即JVM内存,栈、堆、方法区啥的都是JVM内存,只是人为划分
-
.class文件:就是所谓的字节码文件,这里称.class文件,直观些
假设main方法中有以下代码:
Person p = new Person();
很多初学者会以为整个创建对象的过程是下面这样的
javac Person.java java Person
不能说错,但是粗糙了一点。
稍微细致一点的过程可以是下面这样的
通过new创建实例和反射创建实例,都绕不开Class对象。
.class文件
有人用编辑器打开.class文件看过吗?
比如我现在写一个类
用vim命令打开.class文件,以16进制显示就是下面这副鬼样子:
在计算机中,任何东西底层保存的形式都是0101代码。
.java源码是给人类读的,而.class字节码是给计算机读的。根据不同的解读规则,可以产生不同的意思。就好比“这周日你有空吗”,合适的断句很重要。
同样的,JVM对.class文件也有一套自己的读取规则,不需要我们操心。总之,0101代码在它眼里的样子,和我们眼中的英文源码是一样的。
类加载器
在最开始复习对象创建过程时,我们了解到.class文件是由类加载器加载的。关于类加载器,如果掰开讲,是有很多门道的,可以看看
@请叫我程序猿大人
写的好怕怕的类加载器。但是核心方法只有loadClass(),告诉它需要加载的类名,它会帮你加载:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查是否已经加载该类
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果尚未加载,则遵循父优先的等级加载机制(所谓双亲委派机制)
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 模板方法模式:如果还是没有加载成功,调用findClass()
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
// 子类应该重写该方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
加载.class文件大致可以分为3个步骤:
-
检查是否已经加载,有就直接返回,避免重复加载
-
当前缓存中确实没有该类,那么遵循父优先加载机制,加载.class文件
-
上面两步都失败了,调用findClass()方法加载
需要注意的是,ClassLoader类本身是抽象类,而抽象类是无法通过new创建对象的。所以它的findClass()方法写的很随意,直接抛了异常,反正你无法通过ClassLoader对象调用。也就是说,父类ClassLoader中的findClass()方法根本不会去加载.class文件。
正确的做法是,子类重写覆盖findClass(),在里面写自定义的加载逻辑。比如:
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
try {
/*自己另外写一个getClassData()
通过IO流从指定位置读取xxx.class文件得到字节数组*/
byte[] datas = getClassData(name);
if(datas == null) {
throw new ClassNotFoundException("类没有找到:" + name);
}
//调用类加载器本身的defineClass()方法,由字节码得到Class对象
return defineClass(name, datas, 0, datas.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("类找不到:" + name);
}
}
defineClass()是ClassLoader定义的方法,目的是根据.class文件的字节数组byte[] b造出一个对应的Class对象。我们无法得知具体是如何实现的,因为最终它会调用一个native方法:
反正,目前我们关于类加载只需知道以下信息:
Class类
现在,.class文件被类加载器加载到内存中,并且JVM根据其字节数组创建了对应的Class对象。所以,我们来研究一下Class对象。
Class对象是Class类的实例,我们将在这一小节一步步分析Class类的结构。
但是,在看源码之前,我想问问聪明的各位,如果你是JDK源码设计者,你会如何设计Class类?
假设现在有个BaseDto类
上面类至少包括以下信息(按顺序):
-
权限修饰符
-
类名
-
参数化类型(泛型信息)
-
接口
-
注解
-
字段(重点)
-
构造器(重点)
-
方法(重点)
最终这些信息在.class文件中都会以0101表示:
整个.class文件最终都成为字节数组byte[] b,里面的构造器、方法等各个“组件”,其实也是字节。
所以,我猜Class类的字段至少是这样的:
好了,看一下源码是不是如我所料:
字段、方法、构造器对象
注解数据
泛型信息
等等。
而且,针对字段、方法、构造器,因为信息量太大了,JDK还单独写了三个类,比如Method类:
也就是说,Class类准备了很多字段用来表示一个.class文件的信息,对于字段、方法、构造器等,为了更详细地描述这些重要信息,还写了三个类,每个类里面都有很详细的对应。
也就是说,原本UserController类中所有信息,都被“解构”后保存在Class类、Method类等的字段中。
大概了解完Class类的字段后,我们看看Class类的方法。
-
构造器
可以发现,Class类的构造器是私有的,我们无法手动new一个Class对象,只能由JVM创建。JVM在构造Class对象时,需要传入一个类加载器,然后才有我们上面分析的一连串加载、创建过程。
-
Class.forName()方法
反正还是类加载器去搞呗。
-
newInstance()
也就是说,newInstance()底层就是调用无参构造对象的newInstance()。
所以,本质上Class对象要想创建实例,其实都是通过构造器对象。如果没有空参构造对象,就无法使用clazz.newInstance(),必须要获取其他有参的构造对象然后调用构造对象的newInstance()。
反射API
没啥好说的,在日常开发中反射最终目的主要两个:
-
创建实例
-
反射调用方法
创建实例的难点在于,很多人不知道clazz.newInstance()底层还是调用Contructor对象的newInstance()。所以,要想调用clazz.newInstance(),必须保证编写类的时候有个无参构造。
反射调用方法的难点,有两个,初学者可能会不理解。
再此之前,先来理清楚Class、Field、Method、Constructor四个对象的关系:
Field、Method、Constructor对象内部有对字段、方法、构造器更详细的描述:
OK,理清关系后我们继续来看看反射调用方法时的两个难点。
-
难点一:为什么根据Class对象获取Method时,需要传入方法名+参数的Class类型
为什么要传name和ParameterType?
因为.class文件中有多个方法,比如
所以必须传入name,以方法名区分哪个方法,得到对应的Method。
那参数parameterTypes为什么要用Class类型,我想和调用方法时一样直接传变量名不行吗,比如userName, age。
答案是:我们无法根据变量名区分方法
User getUser(String userName, int age); User getUser(String mingzi, int nianling);
这不叫重载,这就是同一个方法。只能根据参数类型。
我知道,你还会问:变量名不行,那我能不能传String, int。
不好意思,这些都是基本类型和引用类型,类型不能用来传递。我们能传递的要么值,要么对象(引用)。而String.class, int.class是对象,且是Class对象。
实际上,调用Class对象的getMethod()方法时,内部会循环遍历所有Method,然后根据方法名和参数类型匹配唯一的Method返回。
循环遍历所有Method,根据name和parameterType匹配
难点二:调用method.invoke(obj, args);时为什么要传入一个目标对象?
上面分析过,.class文件通过IO被加载到内存后,JDK创造了至少四个对象:Class、Field、Method、Constructor,这些对象其实都是0101010的抽象表示。
以Method对象为例,它到底是什么,怎么来的?我们上面已经分析过,Method对象有好多字段,比如name(方法名),returnType(返回值类型)等。也就是说我们在.java文件中写的方法,被“解构”以后存入了Method对象中。所以对象本身是一个方法的映射,一个方法对应一个Method对象。
我在专栏的另一篇文章中讲过,对象的本质就是用来存储数据的。而方法作为一种行为描述,是所有对象共有的,不属于某个对象独有。比如现有两个Person实例
Person p1 = new Person(); Person p2 = new Person();
对象 p1保存了"hst"和18,p2保存了"cxy"和20。但是不管是p1还是p2,都会有changeUser(),但是每个对象里面写一份太浪费。既然是共性行为,可以抽取出来,放在方法区共用。
但这又产生了一个棘手的问题,方法是共用的,JVM如何保证p1调用changeUser()时,changeUser()不会跑去把p2的数据改掉呢?
所以JVM设置了一种隐性机制,每次对象调用方法时,都会隐性传递当前调用该方法的对象参数,方法可以根据这个对象参数知道当前调用本方法的是哪个对象!
同样的,在反射调用方法时,本质还是希望方法处理数据,所以必须告诉它执行哪个对象的数据。
所以,把Method理解为方法执行指令吧,它更像是一个方法执行器,必须告诉它要执行的对象(数据)。
当然,如果是invoke一个静态方法,不需要传入具体的对象。因为静态方法并不能处理对象中保存的数据。
打油诗
我不在乎我的作品文章是被现在的人读还是由子孙后代来读。既然上帝花了六千年来等一位观察者,我可以花上一个世纪来等待读者。
往期推荐
本文分享自微信公众号 - Java小白学心理(gh_9a909fa2fb55)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
干货 | 滴滴 数据分析原来是这样做的!
↑ 点击上方 “凹凸数据”关注 + 星标 ~ 每天更新,干货&福利不断 hi,我是 Rilke Yang 这是一篇我关于滴滴的数据实战,之前首发在和鲸,这次投稿到凹凸数据,希望能够帮助到大家~ 原文链接:https://www.kesci.com/home/project/5f06b0193af6a6002d0fa357 随着企业日常经营活动的进行,企业内部必然产生了各式各样的数据,如何利用这些数据得出有益的见解,并支持我们下一步的产品迭代以及领导决策就显得尤为重要。 A/B测试是互联网企业常用的一种基于数据的产品迭代方法,它的主要思想是在控制其他条件不变的前提下对不同(或同一、同质)样本设计不同实验水平(方案),并根据最终的数据变现来判断自变量对因变量的影响;A/B测试的理论基础主要源于数理统计中的假设检验部分,此部分统计学知识读者可自行探索。 长话短说,本次实战用到的数据集分为两个Excel文件,其中test.xlsx为滴滴出行某次A/B测试结果数据,city.xlsx为某城市运营数据。 数据说明 test.xlsx city.xlsx date:日期 date:日期 gr...
- 下一篇
操作系统和并发的爱恨纠葛
点击蓝色“Java建设者”关注我哟 加个“星标”,及时阅读最新技术文章 这是Java建设者的第 110 篇原创文章 我一直没有急于写并发的原因是我参不透操作系统,如今,我已经把操作系统刷了一遍,这次试着写一些并发,看看能不能写清楚,卑微小编在线求鼓励...... 我打算采取操作系统和并发同时结合讲起来的方式。 并发历史 在计算机最早期的时候,没有操作系统,执行程序只需要一个过程,那就是从头到尾依次执行。任何资源都会为这个程序服务,这必然就会存在 浪费资源 的情况。 ❝ 这里说的浪费资源指的是资源空闲,没有充分使用的情况。 ❞ 操作系统为我们的程序带来了 并发性,操作系统使我们的程序同时运行多个程序,一个程序就是一个进程,也就相当于同时运行了多个进程。 操作系统是一个并发系统,并发性是操作系统非常重要的特征,操作系统具有同时处理和调度多个程序的能力,比如多个 I/O 设备同时在输入输出;设备 I/O 和 CPU 计算同时进行;内存中同时有多个系统和用户程序被启动交替、穿插地执行。操作系统在协调和分配进程的同时,操作系统也会为不同进程分配不同的资源。 操作系统实现多个程序同时运行解...
相关文章
文章评论
共有0条评论来说两句吧...