| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> Java知识库 -> 高薪程序员&面试题精讲系列24之你熟悉反射吗? -> 正文阅读 |
|
[Java知识库]高薪程序员&面试题精讲系列24之你熟悉反射吗? |
一. 面试题及剖析1. 今日面试题今天 壹哥 带各位来复习一个非常牛逼的技术--反射!虽然反射在我们开发时用的很多,但关于反射的面试题,出现的频率倒并不算很多,一般就是问问我们对反射是否了解,如下:
2. 题目剖析刚才 壹哥 跟各位说,反射是一个很牛逼的技术,其实一听名字就感觉和别人不一样对吧,而反射自身的能力也对得起它的名字,确实很厉害! 那你可能会问,反射这么牛逼,我怎么没听过?我怎么不会用?如果真是这样的话,那只能说明你的Java还没学到位。 比如Java中的各种框架,无论是著名的还是不著名的,几乎每种框架,包括Java本身的内部实现中,反射的身影都随处可见。甚至可以说反射就是框架设计的灵魂,没有反射,很多框架都无法被设计出来! 举个栗子,假如面试官问你是否熟悉Spring框架的底层源码,你告诉面试官自己对Spring源码非常熟悉,结果你连反射这个概念都不知道,我相信很多面试官都会怀疑你到底有没有看过Spring的源码! 而且反射这种技术,简直就是“位面之子”,几乎就没有他不能干的。甚至类中私有的变量,一般情况下我们是拿不到的,但是反射中随意的一个getDeclaredField()方法就能拿到私有属性;一般的接口不能实例化,利用反射可以轻易生成一个生成代理Class对象,通过反射就能拿到所有想拿的信息。 也就是说,在反射面前,一个类几乎就是透明的存在,毫无秘密可以,Java中把这种可以“看透 class”的能力称为内省,这种能力在框架开发中尤为重要。 另外,虽然反射很牛逼,但是使用起来其实并不难,它的API并不是很多,而且使用起来一般也有固定的模式。但是对于初学者而言,反射最难得的地方不在于怎么用,而是不理解为什么要有反射,以及到底什么时候用反射,其实也就是不理解反射的作用。 所以面试官有时候就会通过我们对反射的掌握情况,来判断我们对Java面向对象的深入理解。 二. 反射简介既然反射这么牛逼,我们还是赶紧来看看吧,都有点迫不及待了。 1. 概念首先我们来回顾一下反射的概念,在给面试官介绍反射的时候,肯定是先给人家说一下什么是反射。我们来看看百度百科上对反射的介绍:
根据上面的的描述信息,壹哥 给各位提取出了几个关键信息,如下:
我之所以说反射牛逼,就是因为反射与我们正常创建对象的方式不一样。至于怎么不一样的,不要着急,我们一点点往下看。 2. 作用其实根据上面对反射概念的描述,我们也能提取出反射的作用来。 反射的主要功能有:
一句话,通过反射机制,我们可以利用class字节码反向创建出该字节码对应的类对象,并且可以反向调用该对象的各种属性和方法,包括私有的属性与方法。使用反射的这些特性,我们就可以获得与Java类进行动态交互的能力,在开发时实现类的动态加载,也可以实现自定义注解等功能。 等等!有的同学在这里开始提问了,难道反射也可以创建对象? 是的!反射的一个比较核心的功能真的就是用来创建对象的! 但是Java中创建对象不都是通过new的方式来创建的吗?我们都知道,想要对象,就靠new! 其实呢,Java中创建对象的方式有两种,即“正常”的new方式,和“反常”的反射方式。两者区别如下:
我们可以这么理解,反射就好比是"逆向工程",比如你摆我面前一架战斗机,我就能给你进行反向破解,创造出一架新的战斗机,甚至还可以推陈出新搞出更先进的战斗机,是不是有点“我兔”的风格。当然这个比喻并不是很恰当,实际上反射的功能更强大。我们一点点往下看吧。 3. 意义那么反射存在的意义到底有哪些呢?
正是反射有以上的特征,所以它能动态地编译和创建对象,极大的激发了编程语言的灵活性,强化了多态的特性,进一步提升了面向对象编程的抽象能力,因而受到编程界的青睐。 4. 缺点虽然反射机制带来了极大的灵活性及方便性,但反射也有缺点。反射机制的功能非常强大,但不能滥用,能不使用反射时,尽量不要使用,原因如下:
三. 反射API在上面的章节中,壹哥 带各位复习了反射的各种理论概念,接下来我再带各位复习一下反射的常用API有哪些,以及如何进行代码实现。 1. 获取字节码的3种方式实现反射操作的前提,就是先得获取Class字节码,在Java中一共给我们提供了3种获取字节码的方式。 通过
通过
通过
以上3种方式,每种方式都有自己的使用场景,彼此之间其实并无优劣。 2. 反射的组成反射最终也必须有类参与,Java中负责反射的类都位于java.lang.reflect包里,反射的实现一般有下面几个类来组成:
其中AccessibleObject 是另外三个类的基类,如下图所示: 我们在进行反射操作时,主要也就是操作这几个类。接下来 壹哥 就带各位细细的看看这些API如何操作。 3. 创建类对象通过反射创建类对象主要有两种方式:
3.1 通过 Class 对象的 newInstance() 方法创建对象 首先我们可以用上面提到的3种获取字节码方式之一,得到一个Class字节码,然后调用newInstance()方法创建一个Teacher类对象。
3.2 通过 Constructor 对象的 newInstance() 方法创建对象 另外我们知道,一个类中的构造方法可能会有好几个,所以我们在通过 Constructor 对象创建类对象时,可以选择某个特定的构造方法,而通过 Class 对象则只能使用默认的无参数构造方法。下面的代码就调用了一个有参数的构造方法进行了类对象的初始化。 4. 获取类的公开方法利用
该方法会返回一个包含clazz类中公开的 Method对象的数组,这些Method对象代表了此 Class字节码对象所表示的类或接口(包括那些由该类或接口声明的,以及从超类和超接口继承的类或接口)中的公共成员方法。 使用示例代码:
5. 获取指定名称和参数类型的公开方法
该方法会返回一个包含clazz类中公开的 Method对象的数组,这些Method对象代表了此 Class字节码对象所表示的类或接口(包括那些由该类或接口声明的,以及从超类和超接口继承的类或接口)中的公共成员方法。 使用示例代码:
注: invoke()方法释义:
6. 获取类的所有方法上面的getMethods()方法,只能返回类中公开的方法,不能返回私有的等方法,我们可以使用如下方法来获取私有方法。
该方法会返回一个 Method 对象数组,这个对象数组代表了此 Class 对象所表示的类或接口中声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括从父类中继承的方法。 使用示例代码:
7. 获取指定名称和参数类型的所有方法同样的,我们也可以获取类中指定名称和参数类型的所有方法。
该方法会返回一个 Method 对象,该对象代表了此 Class 对象所表示的类或接口中指定名称和参数类型的所有方法。当我们去获取类中的方法、类构造器、属性时,如果要获取私有的方法或私有的构造器、私有的属性,则必须使用带有 declared 关键字的方法。 使用示例代码:
注意: 如果我们反射执行类中的私有方法,私有方法被执行之前,需要通过setAccessible(true)方法设置访问权限,或者被称为“暴力反射”,该设置会允许通过反射来访问类的私有变量。这个设置是针对私有变量而言的,public和protected等都不需要。如下所示:
8. 获取公开的成员变量我们可以获取类中的方法,当然也可以获取类中的属性(成员变量)。我们通过 Class 对象的 getFields() 方法可以获取 Class 类的属性,但无法获取私有属性。
返回一个包含某些 Field 对象的数组,这个数组代表了这个 Class 对象所表示的类或接口中所有可访问的公共字段。如果该 Class 对象表示一个类,则此方法会返回该类及其所有超类中的公共字段。如果该 Class 对象表示一个接口,则此方法会返回该接口及其所有超接口中的公共字段。 另外我们还可以使用getField(String name)方法来获取指定名称的公共属性。
返回一个 Field 对象,它反映此 Class 对象所表示的类或接口的指定公共成员字段。 使用示例代码:
9. 获取所有的成员变量上面的方法用来获取类中公开的属性,但是不能获取私有的属性,我们可以使用 Class 对象的 getDeclaredFields() 方法,获取包括私有属性在内的所有属性。
返回 一个 Field 对象数组,这个数组代表了这个 Class 对象所表示的类或接口中所声明的所有字段。包括公共、保护、默认(包)访问和私有字段,但不包括继承的字段。
示例代码:
10. 获取构造器除了可以获取类中的属性、方法之外,我们还可以获取类中的构造方法,如下:
构造器和上面的方法、属性使用过程大同小异,这里就不再一一展示了。 但需要注意的是,当返回Constructor数组时,返回类型是Constructor<?>[],而不是Constructor<T>[]!因为此方法返回之后,该数组可能会被修改以保存不同类的 Constructor 对象。 11. 获取接口信息另外,我们也可以利用Class字节码来获取类的接口信息的,方法如下:
因为一个类可以实现多个接口,所以getInterfaces()方法返回的是Class[]数组。 注意: getInterfaces()方法只会返回指定类实现的接口,不会返父类实现的接口。 12. 获取泛型信息很多人认为Java类在编译后,会把泛型信息给擦除掉,所以在运行时是无法获取到泛型信息的。但在某些情况下,我们还是可以通过反射在运行时获取到泛型信息的。比如我们可以先获取到java.lang.reflect.Method对象,然后就有可能获取到某个方法的泛型返回信息。
上面这段代码会打印如下结果:
13. 实现动态代理我们在学习Spring AOP面向切面编程的时候,会涉及到动态代理的概念,所谓的动态代理就是指把运行时创建接口的动态实现称为动态代理。而动态代理也需要用到反射在运行时创建接口的动态实现来实现,java.lang.reflect.Proxy类就提供了创建动态实现的功能。 动态代理可以用于许多不同的目的,例如数据库连接和事务管理、用于单元测试的动态模拟对象以及其他类似aop的方法拦截等。 13.1 创建代理 调用java.lang.reflect.Proxy类的newProxyInstance()方法就可以创建动态代理,newProxyInstance()方法有三个参数:
13.2 示例代码
运行上面代码后,proxy变量中就包含了MyInterface接口的动态实现。 13.3 InvocationHandler 对代理的所有调用都将由到实现了InvocationHandler接口的handler类对象来处理,所以必须将InvocationHandler的实现传递给Proxy.newProxyInstance()方法。对动态代理的所有方法调用都转发到实现接口的InvocationHandler对象。 InvocationHandler代码:
13.4 实现InvocationHandler接口的类
上面代码中会生成一个MyInterface的接口对象proxy,通过proxy对象调用的方法都会由MyInvocationHandler类的invoke()方法处理。 下面详细介绍invoke()方法的三个参数:
四. 反射原理(重点)1. Java开发步骤我们进行Java开发时,一般要经历3个开发步骤,如下图所示: 根据上图可知,Java的开发步骤如下:
或者,我们也可以用下图把这个开发步骤再次展示: 2. Java类加载过程根据上面的内容可知,当我们编写完一个 Java 项目之后,所有的 Java 文件都会被编译成一个个的 .class 字节码文件。根据虚拟机的工作原理,一般情况下,这些.class字节码最终被执行需要经历如下过程:
所以 class 字节码文件 被编译出来之后,就需要被 ClassLoader类加载器 加载到JVM虚拟机中才能运行。类加载器 ClassLoader 加载class文件时,会把类里的一些数值常量、方法、类信息等加载到内存中,称之为类的元数据,另外还有这个类的父类、接口、构造函数、方法、属性等原始信息也会被加载进内存。最终 JVM虚拟机会根据这些信息,在内存中自动产生一个的 Class 类对象,这个对象会被保存在.class文件里。如下图所示: 3. 加载.class文件具体过程根据上面的小节可知,我们的Java类需要被类加载器加载进内存才可以工作,类加载器加载某个类的过程大致如下图所示: 我这里把这个加载过程总结如下:
这样,我们的 .class字节码文件 就被加载到了JVM中,并被转换成了对应的Class类对象,但此时反射还没有介入工作。 4. Class类对象的创建步骤上面的小节中,我提到了生成Class类对象,接下来我们再来看看生成Class类对象的具体步骤。 根据上面章节可知,类加载器首先会检查这个类的Class对象是否已被加载过,如果尚未加载,默认的类加载器就会根据类名查找到对应的.class文件,然后得到对应的Class类对象。只有得到了Class对象的引用,才能在运行时使用类型信息。其实创建Class类对象的过程分为以下3个步骤:
所以当我们编写了与反射相关的代码后,就生成了对应的反射相关.class字节码,最后再生成对应的Class类对象。在这个Class类对象里维护着该类的所有Method,Field,Constructor的cache,这份cache也可以被称作根对象。所以反射的原理之一其实就是动态的生成这个Class类对象,并将其加载到JVM中运行。 在Java中,Class类与java.lang.reflect类库一起对反射进行了支持,该类库包含Field、Method和Constructor类。这些Field、Method、Constructor等类对象会由JVM在启动时创建,用以表示未知类里对应的成员。 接着我们就可以使用Contructor来创建新的对象,用get()和set()方法获取和修改类中与Field对象关联的字段,用invoke()方法调用与Method对象关联的方法。 另外,我们还可以调用getFields()、getMethods()和getConstructors()等许多便利的方法,来返回表示字段、方法、以及构造器对象的数组。 这些对象信息都是在运行时被完全确定下来的,而在编译时不需要知道关于类的任何事情。 5. Method对象的创建过程至此,我们就得到了一个包含很多类信息的Class类对象了。接下来我们再看看利用反射执行某个方法的执行原理。 由上面可知,Class类对象是在加载类时由JVM构造的,JVM为每个类都管理着一个独一无二的Class对象,这份Class对象里维护着该类的所有Method,Field,Constructor的cache,这份cache也可以被称作根对象。 每次getMethod()方法获取到的Method对象,都会持有对根对象的引用,因为一些重量级的Method的成员变量(主要是MethodAccessor),我们不希望每次创建Method对象时都要重新初始化。于是所有代表同一个方法的Method对象都共享着根对象的MethodAccessor,每一次创建都会调用根对象的copy方法复制一份。创建Method对象的过程如下图所示: 这样我们就创建了另一个对象---Method对象,接下来就可以利用该对象执行某个方法了。 6. invoke()方法执行过程上面我们已经生成了Method对象,然后就可以调用Method.invoke()方法了。调用Method.invoke()方法后,会直接去调MethodAccessor.invoke。
注: 是否采用inflation和15这个数字都可以在jvm参数中调整。 7. inflation机制的由来那么为什么要有inflation机制,以及为什么用15次作为是否调用native()方法和Java代码的分水岭呢? 其实之所以要有inflation这个机制,是因为创建MethodAccessorImpl对象时,第1次和第16次调用是最耗时的(初始化NativeMethodAccessorImpl和字节码拼装MethodAccessorImpl)。毕竟初始化是不可避免的,而native方式的初始化会更快,因此前几次的调用会采用native方法。 但随着调用次数的增加,每次反射都使用JNI跨越native边界会对优化有阻碍作用,相对来说使用拼装出的字节码可以直接以Java调用的形式实现反射,发挥了JIT优化的作用,避免了JNI为了维护OopMap(HotSpot用来实现准确式GC的数据结构) 而进行封装/解封装的性能损耗。 因此在已经创建了MethodAccessor的情况下,使用Java版本的实现会比native版本更快。所以当调用次数到达一定次数(15次)后,会切换成Java实现的版本,来优化未来可能的更频繁的反射调用。 五. 结语至此,壹哥 就带大家把反射相关的知识点复习回顾了一遍,我们在给面试官介绍反射时,要从反射的概念、作用、存在意义、使用方法开始介绍。当然如果你能把反射的底层执行原理,甚至反射的底层源码都能够回答的很清楚,我相信,但是一个反射,就足以让面试官对你另眼相看了。 不知本文对反射的介绍,有没有加深你之前的印象呢?可以在评论区留言讨论一下,说说你对反射的理解吧。 |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/24 3:27:51- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |