我理解的反射

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

我之前对反射的理解未必精确却代表了自己当时所能理解的限度...每写完一篇文章,我都能感受到原本一个个孤立的知识由点到线,由线到面,最终拔地而起变成一个个立体的概念,让我有机会站在更高的地方去环视周围这个广袤无垠的程序世界。

这感觉,会上瘾。

由于反射本身确实抽象(说是Java中最抽象的概念也不为过),所以我当初写作时也用了大量的比喻。但是比喻有时会让答案偏离得更远。前阵子看了些讲设计模式的文章,把比喻都用坏了。有时理解比喻,竟然要比理解设计模式本身还费劲...那就南辕北辙了。所以,这一次能不用比喻就尽量不用,争取用最实在的代码去解释。


JVM是如何构建一个实例的

本文涉及的名词及其对应关系

  • 内存:即JVM内存,栈、堆、方法区啥的都是JVM内存(不考虑JDK1.8元空间的区别)
  • .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文件是由类加载器加载的。由于本文是介绍反射的,所以关于类加载器就不做太细节的介绍,ClassLoader类负责加载类,它的核心方法是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个步骤:

  1. 检查是否已经加载,有就直接返回,避免重复加载
  2. 当前缓存中确实没有该类,那么遵循父优先加载机制(双亲委派机制),加载.class文件
  3. 上面两步都失败了,调用findClass()方法加载

需要注意的是,ClassLoader类本身是抽象类,而抽象类是无法通过new创建对象的,所以它并没有实现最核心的findClass()方法,直接抛了异常。此处又是应用了模板方法模式,具体的findClass()方法丢给子类实现。

所以正确的做法是,子类重写覆盖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类的结构。

但是,在看源码之前,我想问问聪明的各位,如果你是JDK源码设计者,你会如何设计Class类?

假设现在有个BaseDto类:

上面类至少包括以下信息(按顺序):

  • 权限修饰符
  • 类名
  • 参数化类型(泛型信息)
  • 接口
  • 注解
  • 字段(重点)
  • 构造器(重点)
  • 方法(重点)

最终这些信息在.class文件中都会以0101表示:

整个.class文件最终都成为字节数组byte[] b,里面的构造器、方法等各个“组件”,其实也是字节。

既然Class要描述.class文件,我猜Class类的字段至少是这样的:

好了,看一下源码是不是如我所料。

还真是!

字段、方法、构造器对象:

注解数据

泛型信息

等等。

而且,针对字段、方法、构造器,因为这三个部分信息量太大了,JDK单独写了三个类:Field、Method、Constructor。我们挑Method类看一下:

也就是说,Class类确实按我们所猜想的设置了很多字段来描述.class文件的结构,比如类名、注解、实现的接口等,但对于字段、方法、构造器等结构,由于细节其实非常多,每一个单独拎出来都能扯很久,为了更详细地描述这些重要信息,JDK单独写了三个类Field、Method、Constructor,每个类里面都有更加详细的对应关系。

而Class类持有这三个对象数组的引用即可(因为一个类可能有多个方法,所以Class要用Method[] methods保存)。

最终,原本UserController类中所有信息,都被“解构”后保存在Class类中。其中,字段、方法、构造器又用Field、Method等对象单独表示。

大概了解完Class类的字段后,我们来看看Class类的方法。

  • 构造器

可以发现,Class类的构造器是私有的,我们无法手动new一个Class对象,只能由JVM创建。JVM在构造Class对象时,需要传入一个类加载器,然后才有我们上面分析的一连串加载、创建过程。

  • Class.forName()方法

反正还是类加载器去搞呗。

  • newInstance()

也就是说,newInstance()底层就是调用无参构造对象的newInstance()。

所以,本质上Class对象要想创建实例,其实都是通过构造器对象。如果没有空参构造对象,就无法使用clazz.newInstance(),必须要获取其他有参的构造对象然后调用对应的Constructor#newInstance(xxx)。

反射API

没啥好说的,在日常开发中反射最终目的主要两个:

  • 创建实例
  • 反射调用方法

创建实例

创建实例的难点在于,很多人不知道clazz.newInstance()底层还是调用Contructor对象的newInstance(),所以,要想调用clazz.newInstance(),必须保证编写类的时候有个无参构造,否则就要先获取对应的Constructor。

反射调用方法

讲完反射如何创建实例,我们再聊聊第二点:反射调用方法。难点有两个,初学者可能会不理解。

先来理清楚Class、Field、Method、Constructor四个对象的关系:

上面讲过了,Field、Method、Constructor对象内部有对字段、方法、构造器更详细的描述:

OK,理清关系后我们正式来研究如何利用反射调用方法。

  • 难点一:为什么根据Class对象获取Method时,需要传入方法名+参数的Class类型?

也就是Class#getMethod()为什么需要methodName和ParameterType?

这是因为.class文件中通常有不止一个方法,比如:

所以必须传入name,以明确本次需要调用的方法,得到该方法对应的Method对象。

OK,明白了。但是参数parameterTypes为什么要用Class类型,像调用普通方法那样直接传变量名不行吗,比如getUserByName(username)。

答案是:JVM判定方法重载的依据是参数列表,包括参数类型及参数个数,但不包括变量名:

User getUser(String userName, int age); User getUser(String mingzi, int nianling);

仅仅是变量名不同不叫重载,本质是同一个方法。

你可能还不死心,可能会追问:变量名不行,那我能不能传String, int。

不好意思,这些都是基本类型和引用类型,类型不能用来传递(你见过方法传参传一个String、Int的吗)。我们能传递的要么值,要么对象(引用)。而String.class, int.class是对象,且是Class对象。

从方法设计的角度,方法对应Method,方法的参数对应String.class、int.class才是对等的。

实际上,调用Class对象的getMethod()方法时,内部会循环遍历所有Method,然后根据方法名methodName和参数类型parameterType匹配唯一的Method返回:

  • 难点二:调用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设置了一种隐性机制,每次对象调用方法时,都会隐性传递当前调用该方法的对象参数,方法可以根据这个对象参数知道当前调用本方法的是哪个对象!

同样的,在反射调用方法时,本质还是希望方法处理数据,所以必须告诉它去处理哪个对象的数据。

大家仔细看,就会更加理解什么是射:

  • 黑色细线是正向方法调用
  • 绿色粗线是反射方法调用

重新理解反射

最好把Class/Method/Field等理解为方法执行指令,它更像是一个方法执行器,必须告诉它要执行的对象(数据)。当然,如果是invoke一个静态方法,不需要传入具体的对象,因为静态方法是属于类的。

大家有没有看过成龙的《醉拳》,酒鬼师傅为了教成龙功夫,用两根竹竿拴住他的双手,操作他做出各种招式。Method也是如此,原本是对象自己调用方法,现在则是反射调用方法。但前提是,必须要有对象。

这里的成龙就是Object对象,师傅就是Class/Method/Field,竹竿就是反射工具,整个行为就是反射!这些动作,成龙本人当然可以做(正向自发),但现在则是由师傅引导他做(反向被动)。

至于很多人会疑惑的为啥反射需要我们传入Object,有了Object不就可以自己调用了吗?这纯属学习学糊涂了...

没有成龙这个具体的人,师傅的着力点就没了,就是对空气瞎比划,恐怖不恐怖啊。一个招式,肯定要有具体的发力者的。

至于有了Object为啥不直接调用Object?这是因为你看的角度不对。现在你要做的是,在还不知道是哪位徒弟、甚至没有徒弟的时候,师傅就要先准备好这一套训练方法。后面如果有人要学醉拳,直接套上就可以了。也就是说,反射是一种面向未来的通用处理方式,未来虽未来,但我的代码已经存在!

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/166952.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

提取图像文本的 5 大 Python 库

引言 光学字符识别是一个古老但依然具有挑战性的问题&#xff0c;涉及从非结构化数据中&#xff08;包括图像和PDF文档&#xff09;检测和识别文本。它在银行、电子商务和社交媒体内容管理等领域具有广泛的应用。 但与数据科学中的每个主题一样&#xff0c;尝试学习如何解决OC…

光谱融合——Deep Spatio-spectral Attention Convolutional Neural Networks

Hyperspectral Image Super-Resolution via Deep Spatiospectral Attention Convolutional Neural Networks 简介 论文链接 对于高分辨率多光谱&#xff08;高分辨率意味空间信息更好&#xff09;图像&#xff0c;采用spatial attention机制进行空间信息的保留&#xff0c;对…

Vue+Swiper实现轮播图效果

效果展示 实现了自带切换按钮在图片外部实现了自定义的切换按钮 背景 在项目中使用到了轮播图&#xff0c;实现点击上一张下一张时实现循环显示&#xff0c;同时预览两个图片&#xff0c;并加以文字对图片的说明。 设计 使用 Swiper 插件&#xff0c;可以实现当前这个需求。…

[Kettle] 公式

公式是用来计算数据流中数据的表达式 公式可以是"AB"这样的简单计算&#xff0c;也可以是类似"if/then"复杂业务逻辑判断的表达式 数据源 2019年11月月考成绩(Kettle数据集16).xlshttps://download.csdn.net/download/Hudas/88553816?spm1001.2014.300…

二阶段提交

二阶段提交 二阶段提交&#xff08;英语&#xff1a;Two-phase Commit&#xff09;是指&#xff0c;为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm)。 二阶段过程 在两阶段提交过程中&#xff0c;主要分为了两种角色协调者&…

webpack快速上手之搭建cesium三维地球环境

前言 &#x1f4eb; 大家好&#xff0c;我是南木元元&#xff0c;热衷分享有趣实用的文章&#xff0c;希望大家多多支持&#xff0c;一起进步&#xff01; &#x1f345; 个人主页&#xff1a;南木元元 目录 Cesium介绍 初始化一个Cesium地球 ​编辑 Webpack的使用 Webpac…

鼠标点击网页任何地方都显示光标闪烁输入状态

出现这种情况的原因 因为大概是用户无意中打开了浏览器无障碍模式中的使用文本光标浏览网页的功能。 对于Chrome浏览器而言就是这样的&#xff1a; 直接按F7关闭这个模式

谈谈系统性能调优中都需要考虑哪些因素

一、 什么是性能调优&#xff1f; 这个系统好慢、网站又打不开了&#xff0c;太卡了&#xff0c;又没响应了&#xff0c;相信大家都遇到过用户的这种抱怨&#xff0c;此时&#xff0c;说明我们的应用系统出现了性能问题&#xff0c;那么怎么办呢&#xff0c;首先想到的应该是优…

CNCC 2023 | 大模型全面革新推荐系统!产学界多位大咖精彩献言

随着人工智能领域的不断突破&#xff0c;大模型的潮流已然席卷而来。大模型一跃成为时代的新宠&#xff0c;展现出强大的通用性和泛化能力&#xff0c;为 AI 技术的应用进一步打开了想象空间。与此同时&#xff0c;推荐系统作为大规模机器学习算法应用较为成熟的方向之一&#…

腾讯混元模型

最近腾讯的混元大模型内测,我有幸申请到了一个名额。 身为一个程序员,我想大家最关注的一定是该模型的代码和类代码能力,因为这直接关系到这个模型能帮我们解决多少问题,节约多少时间,提高多少效率。 对此,我针对工作中可能会用到的几个点进行了详细的体验。 先说结论:腾讯混元…

工业基础类IFC—材质和纹理

在我们的 IFC技术交流群&#xff08;788206534&#xff09;里&#xff0c;经常会有人提问“如何学习 IFC文档或者其开发”的问题。对于这个问题&#xff0c;我一直没有机会做一个完整的回答&#xff0c;这次我认真回忆了自己关于IFC的学习经历&#xff0c;在此与大家分享。一是…

利用API连接抖音外卖与电商平台和营销系统,实现无代码开发的集成

利用API连接抖音外卖和电商平台 抖音来客&#xff0c;作为抖音生活服务商家经营平台&#xff0c;为商家提供一站式经营服务&#xff0c;其中包括入驻、上品、经营和履约等功能。商家可以通过API调用&#xff0c;实现电商平台和抖音外卖的连接&#xff0c;从而优化运营效率。例…

Java SE——*API API帮助文档

1. API概述 Java中的API可以理解为一本编程字典或者工具包&#xff0c;它包含了许多预定义的类、接口和方法&#xff0c;可以帮助我们更方便地开发Java程序 想象一下&#xff0c;当你要做一个饭菜的时候&#xff0c;你需要食谱和厨具。食谱告诉你需要哪些食材和步骤&#xff…

解决 VS2022 关于 c++17 报错: C2131 表达式必须含有常量值

使用 VS2022 编译 ORB-SLAM3 加载Vocabulary 二进制ORBvoc.bin 时&#xff0c;在 DBOW2 里修改 TemplatedVocabulary.h 代码显示这样的错误&#xff1a; 编译器错误 C2131 表达式的计算结果不是常数 定位到我的代码中&#xff1a; char buf [size_node] ; 原因 &#xff1a; …

基于GATK流程化进行SNP calling

在进行变异检测时&#xff0c;以群体基因组重测序数据为例&#xff0c;涉及到的个体基本都是上百个&#xff0c;而其中大多数流程均是重复的步骤。 本文将基于GATK进行SNP calling的流程写入循环&#xff0c;便于批量分析。 1 涉及变量 1.工作目录work_dir/ 2.参考基因组ref…

SMART PLC数值积分器功能块(矩形+梯形积分法完整源代码)

PLC的数值积分器算法也可以参考下面文章链接: PLC算法系列之数值积分器(Integrator)-CSDN博客文章浏览阅读1.5k次,点赞3次,收藏3次。数值积分和微分在工程上的重要意义不用多说,闭环控制的PID控制器就是积分和微分信号的应用。流量累加也会用到。有关积分运算在流量累加上…

助力安全生产--韩施电气为您提供电动机保护及电机故障解决方

上海韩施电气自成立于2008年&#xff0c;是一家专门从事销售电气自动化设备、电力设备、机电设备的综合型贸易公司&#xff0c;公司自成立以来一直专注于EOCR产品的推广销售和技术服务&#xff0c;成为韩国施耐德EOCR在国内的总代理&#xff0c;并授予代理证书&#xff0c;我们…

uni-app:前端实现心跳机制(全局)+局部页面控制心跳暂停和重新心跳

一、App.vue全局中写入心跳 在data中定义变量heartbeatTimer&#xff0c;便于暂停心跳使用在onLaunch中引用开始心跳的方法startHeartbeat()写入开始心跳方法写入暂停心跳方法写入请求后端刷心跳机制 定义变量 // 在全局设置的心跳机制中添加一个变量来保存定时器的标识 data(…

云计算行业敲门砖—证书盘点

未来10年&#xff0c;都会是云计算技术不断发展变革的时代&#xff0c;这其中会产生非常多的就业机会。有数据统计&#xff0c;未来五年&#xff0c;云计算行业人才缺口达150万&#xff0c;选对了行业&#xff0c;你就成功了一半。 云计算可以考的证书还是很多的&#xff0c;很…

React中StrictMode严格模式,导致开发环境,接口会请求两次或多次( useEffect 请求多次)

问题描述&#xff1a; 我在用 create-react-app时&#xff0c;开发环境&#xff0c;一进页面接口会请求两次或多次。 我在首页 useEffect里 请求一个接口&#xff0c;整个页面就在这里请求这一次接口。但 实际上请求了两次。我检查了代码&#xff0c;确定只调用了一次&#xf…