Java 反射学习笔记
Class 类
JVM中保存类的Class
在理解反射这个概念之前,我们要先理解一下 Class,Class 是一个 Java 自己带的类,注意这里的 Class 首字母是大写的,说明它是一个类的名字,而不是 类 的英文翻译,就像我们自己定义的一个类一样:
1 | public class Xorex { |
这叫作 Class 的类(这个名字容易误解成这个叫类的类,但实际上 Class 只是它的名字),也有自己的定义,虽然有点长:
1 | public final class Class<T> implements java.io.Serializable, |
那么这个叫作 Class 类有什么用呢?答案是用来保存其他的类。
在 JVM 运行的时候,它一边运行,一边会把自己需要执行的类放入自己的内存中,叫作动态加载 (不会一次性全部加载,而是需要的时候再去找),比如一个程序我们需要 Xorex
和 Tempest
两个类并在程序中创建 N 个它们的实例。那么 JVM 运行的时候,会在第一次实例化的时候,查看自己的内存中有没有这个类的信息,如果内存中没有这个类,那么就会把这个类加载到自己的内存中,然后根据内存中的这个类的信息,创建一个实例(开辟一块内存空间存放这个实例的数据。),内存中有了这个类的信息之后,第二次创建实例就简单多了,只需要再开辟一块内存空间存放这个类的数据就算实例化了(实例是类的数据)。
那么我们放在内存中的类,是以什么形式的保存的呢?没错,就是用 Class 类的实例来保存,每一个保存在内存中的类,都是 Class 类的一个实例!这个类的所有信息都被保存在 Class 类的实例中。
反射的概念
这里我们就可以引入反射的概念了,反射 Reflection ,就是利用一些能确定某个类的信息,映射出这个类,然后反射出整个类的所有结构。这个确定某个类的信息需要有确定性,比如某个类的完整名称(方法三),某个类的实例(方法二),或者某个类的 Class 包含的所有信息(方法一),只要有这些确定性信息就能映射到一个确定的类身上,然后反射出来整个类的所有信息。
既然我们可以通过 Class 获得一个类的所有信息,那么我们就可以不通过 new 运算符,来创建一个实例了。你可能想问,为什么要不通过 new 运算符来创建实例呢?
因为在框架编写中,并不知道使用这个框架的人要传入什么类来完成某个功能,但是我们仍然需要实例化使用者传入的类,完成框架需要执行的任务。这里就需要在不知道一个类的情况下实例化一个类,因而 new 运算符就失效了。但是 Class 的出现可以让我们先将一个类转化为 Class 类型,然后利用 Class 实例化这个类,最后对其进行各种操作。
将一个类转化为 Class 的方法有三种:
- 使用一些类的静态变量
Class str=String.class;
- 使用一些类的实例提供的
getClass()
方法Class str="Xorex".getClass();
- 使用一个类的完整类名来得到
Class str=Class.forName("java.lang.String");
一个类一个Class
我们用上面的方法,能获取一个类的 Class 实例,这个实例包含着这个类的所有信息。回想上面 JVM 执行的过程,就可以发现每个类在 JVM 中只有一个 Class 实例(因为实例是根据类的构成创建的,类是唯一的,数据也自然是唯一的),那么我们无论通过什么方法获得某个类的 Class 实例,它们都应该是引用了同一个实例,所以就可以使用 ==
来验证我们获得的 Class 实例是否为同一个实例,如果是,那么两种获得 Class 实例的唯一性信息所映射出来的类是同一个类。
比如:
1 | Class One="Xorex".getClass(); |
其实这个特性是很理所当然的,对于一个类的信息,只保存一次,实例化的时候重复使用,节省内存。
用类的Class实例来获得类的实例
当我们反射出来一个 Class 实例之后,我们相当于掌握了一个完整的类,那么我们自然能够将建这个类的实例,方法也很简单:
1 | Class str=String.class; |
这里 (String)str.newInstance()
就等价与 new String()
只不过这个创建实例的过程是由 Class 类的 newInstance()
方法实现的。利用这个方法创建实例是有很大的局限性的,在使用 new String()
的时候,会直接调用 String 的构造方法,在加上重载机制,可以在创建实例的时候给构造方法传入参数来初始化。但是 newInstance()
方法不可能对于一个未知的类的构造方法进行重载,所以就导致了使用反射机制创建的实例没有办法往对应类的构造方法里面传入参数。
访问字段
获取字段
在 java.lang.reflect
里面由一个类叫作 Field ,专门用来存储字段的信息,Class 类中存储类字段的数据类型就是 Field ,当然我们可以通过这个东西来存储我们从 Class 实例中得到的字段信息。
获得字段的方法有这些:
getField(String name)
返回 Field 数据,为名字为 name 的字段(包括父类)。getDeclaredField(String name)
作用同上,但不包括父类的字段。getFields()
返回 Field[] 数据,为该 Class 实例中所有的字段(包括父类)。getDeclaredFields()
同上,但不包括父类字段。
前两个在使用的时候,需要处理可能抛出的异常 NoSuchFieldException
。
得到了一个字段 Field,而类 Field 里面包含了一些方法可以查看这个字段的详情信息,比如 getName()
和 getType()
获取字段的名字和类型。
获取/修改 字段值
Field 类中有一个方法为 get(Object)
,将该类的实例作为参数传入可以得到这个字段在实例中的值,同样可以通过方法 set(Object1,Object2)
,来修改这个字段在实例 Object1 中的值为 object2,代码如下:
1 | import java.lang.reflect.Field; |
上面代码通过 Tempest
的实例 Xorex
映射出来 Tempest
类,并反射出来对应的 Class
实例,然后使用 Class
类中的 getField()
方法获得了类型为 Field
的 Tempest
类的字段 Blessing
。最后使用 Field 类自带的 get()
/ set()
方法 获得/修改 了 Tempest
类对应实例 Xorex
的字段 Blessing
。
上面的话可能有点绕,但这就是整个调用的逻辑原理。
访问方法
获取方法
同样的,就像字段拥有自己的类 Field,方法也有属于自己的类 Method:
Method getMethod(name, Class...)
:获取某个public
的Method
(包括父类)Method getDeclaredMethod(name, Class...)
:获取当前类的某个Method
(不包括父类)Method[] getMethods()
:获取所有public
的Method
(包括父类)Method[] getDeclaredMethods()
:获取当前类的所有Method
(不包括父类)
上面的参数里面有一条是 Class...
,这是因为有着重载的存在,对于一个名字的方法可能有多种签名,所以为了区分不同的方法需要把完整的方法签名传入进去,而 Class...
就是需要传入签名中的形参列表,只需要按照对应顺序传入对应数据类型的 Class 实例即可。如果对应的方法的形参为 int x,String y
,那么就需要将 Class...
写为 int.class,String.class
即可。
1 | import java.lang.reflect.Field; |
输出的就是一个 Method 类型数据,public void Tempest.GetAns(int,int)
。
还可以利用的 Method 类的一些方法来输出实例的固定信息:
getName()
:返回方法名称,例如:"getScore"
;getReturnType()
:返回方法返回值类型,也是一个Class实例,例如:String.class
;getParameterTypes()
:返回方法的参数类型,是一个Class数组,例如:{String.class, int.class}
;getModifiers()
:返回方法的修饰符,它是一个int
,不同的bit表示不同的含义。
调用方法
- 调用普通方法
调用方法使用 Method 类的 invoke(Object,MethodParameter...)
方法,其中 Object 参数传入一个方法所在类的实例,MethodParameter...
传入对应方法需要的参数。需要注意的是,这个 Method 实例在获得的时候就已经确定了是具体的某个方法了,所以参数方法要严格按照对应的方法签名传进去,不要妄想在 Method 里面玩方法重载。
比如上面代码获得的 Method 就可以这样调用:
1 | Tempest.class.getMethod("GetAns",int.class,int.class).invoke(Xorex,1,2); |
- 调用静态方法
调用静态方法的话,方式和前面都是一样的,唯一的不同的是,最后使用 invoke()
方法的时候就不必须传入一个实例了,可以传入一个 null 来补位,比如把上面的方法 GetAns()
修改成静态方法之后,就可以不单独搞一个实例作为参数传进入了(当然你想传入实例的话也可以,没什么影响),直接用 null 补位:
1 | Tempest.class.getMethod("GetAns",int.class,int.class).invoke(null,1,2); |
- 调用多态方法
当我们用反射出来的父类 Class 得到的一个父类的方法 Method 之后,传入一个子类的实例调用此 Method ,会发现这个 Method 实际执行的方法是子类的方法而不是父类的。
说明 Method 没有保存方法的代码,而只是保留了方法签名,还是利用引用的关系进行方法调用。所以调用一个实现了多态的方法的时候,是遵循多态原则的(实际运行的是子类重写过的方法)
具体内部的原理虽然能强行推出来一个还算合理的解释,但是无法验证真伪,这些东西牵扯到的知识面比较广,和 JVM 以及这些类的底层实现代码有关,暂且放一放,以后会好好研究一下它们到底是如何运作起来的。
暂且记住:使用 反射调用方法 时,仍然 遵循多态原则 :即总是调用实际类型的覆写方法。
调用构造方法
在前面我们利用反射出来的 Class 来创建对应类的实例的时候,Class 的 newInstance()
拥有着无法调用有参数的构造方法的问题,那么 Reflect 就提供了一个 Constructor 类,专门用来解决这个问题。
首先导入 Constructor 类之后,需要使用方法 getConstructor(Parameter...)
从 Class 里面获得构造方法的信息,因为构造方法的名字和类名相同,所以就不需要方法名作为参数,按照去掉方法名的 getMethod()
一样,直接填充对应构造方法的实参即可。
得到 Constructor 的实例之后,就可以利用里面的方法 newInstance()
来构造一个实例,不过这次,可以往里面填对应构造方法的实参了,算是解决了 Class 自己的 newInstance()
的一个小缺陷。
下面是代码演示:
1 | public class test { |
我们利用 Constructor 获得了 Integer 类中的构造方法 Integer(int)
的信息,然后用它的实例的 newInstance(int)
方法成功调用 Integer 类中的构造方法 Integer(int)
,并实例化出来一个 Integer 对象。
获取继承关系
获取父类
使用 Class 类中的 getSuperClass()
可以获取这个 Class 实例表示的类的父类,返回的是父类的 Class 实例。我们就可以利用它来看一些类的继承关系:
1 | Class Xorex=NullPointerException.class; |
然后就会一路打印继承关系,到了 Object 类,因为没有父类,所以返回的是 null,结束循环。
1 | class java.lang.NullPointerException |
获取实现接口
这里是使用 getInterfaces()
返回一个 Class 的数组,遍历就能得到实现的所有接口。
1 | Class Xorex=Integer.class; |
输出 Integer 类实现的所有接口(不包括父类实现的接口):
1 | interface java.lang.Comparable |
判断向上转型
判断一个实例的类是不是另外一个类的子类的时候,可以使用 instanceof 操作符,如果是,那么这个实例就可向上转型为另外一个类的实例。那么两个 Class 如何判断是否有继承关系呢?
可以使用 Class 类的方法 Class1.isAssignableFrom(Class2)
来确定 Class1 是否继承了 Class2 ,返回一个布尔值。
代理
静态代理
注意!!!这里有误,将代理类的代理对象给搞错了,代理类的代理对象是调用者。原本调用者去访问目标类,加入代理类之后,是代理类代替调用者去访问目标类。
举个例子:我想点份外卖,但是手机没电了,于是我让同学用他手机帮我点外卖。在这个过程中,其实就是我同学(代理对象)帮我(被代理的对象)代理了点外卖(被代理的行为),在这个过程中,同学可以完全控制点外卖的店铺、使用的APP,甚至把外卖直接吃了都行(对行为的完全控制)。
所以下面的很多逻辑都是错误的,请看最新的文章:设计模式:结构模型笔记 里面对两种代理模式的解释进行了重写。
当我们调用一个第三方的类完成一些任务的时候,发现这个第三方的类没有办法满足我们的需求,需要添加一些功能,但是我们又不能修改人家第三方类的代码,在调用库的类里面添加功能则会打乱我们的业务实现逻辑。这个时候,我们就可以再新建一个类,作为代理类,在这个类里面添加新业务实现的代码,然后用第三方的类完成补充。最后我们再通过对这个代理类的调用实现整个业务需求。
这样我们就通过代理操作,新建了一个类对第三方类进行 “修改” 以满足业务需求,同时原有的代码结构不变,不过是从直接操作第三方类变成了直接操作代理类。
这就是静态代理: 代理类 = 原类 + 增强代码
我们直接对代理类的操作即可,代理类会完成对原类的完善。以后修改代码只需要修改代理类,原调用地方是不需要修改代码的。
我们来看一段静态代理的代码:
1 | public class test { |
动态代理
前面说类静态代理,说是静态代理是因为这些过程在运行的时候都是不变的,编译生成 .class
文件是在 JVM 运行之前完成的。但是动态代理的代理类,是在 JVM 运行中生成的 .class
的。
动态代理有什么好处吗,为什么在 JVM 运行中生成有什么用啊?
当然有用,它最大的用处就是在运行中生成。在静态代理中,如果我们需要对大量的原类进行编写增强代码相似的代理类,重复的工作就太多了。于是我们想要在程序运行的时候,根据实际所对应的原类能自动生成需要的代理类,我们只用写一次代理类模板,就能直接代理所有的原类,那就太方便了。想到动态代理可以在程序运行中生成代理类,这不就是我们想要的嘛,写一个代理类模板,运行的时候依次生成所有原类的代理类,这样以后修改增强代码只需要在代理类模板修改即可。
看下图,这里静态代理和动态代理最大的区别就是多了一个中间处理方法 invoke() ,这个invoke() 里面就是用来写增强代码的地方,里面对原类的各种调用利用反射来完成。只需要将原类传进代理生成器,就能利用反射生成一个原类对应的代理类,最后只要操作这个生成的代理类即可。修改代码只修改 invoke()
为了能生成代理类,就需要有模板 InvocationHandler.invoke()
,这个是我们自己通过重写实现的,然后需要一个代理类生成器:Proxy.newProxyInstance()
,最后,只需要将模板和数据塞入代理类生成器,就能量产代理类了。
下面就是代码的具体实现上面的需求:
1 | import java.lang.reflect.InvocationHandler; |
首先写一个代理类模板 ,来规定生成的代理类都有哪些对应的功能,这就是 InvocationHandler
接口的 invoke()
需要实现功能。而我们上面代码定义这个 invoke()
为多输出了一段字符串,然后调用被代理实例的某个方法。然后又定义了启动代理类生成器 Proxy.newProxyInstance()
的方法 bind()
,用来将被代理实例和模板通过代理类生成器绑定,从而生成被代理实例需要的代理类。
对于 InvocationHandler
接口需要被实现的 invoke()
方法为:
1 | Object invoke(Object proxy, Method method, Object[] args) throws Throwable |
proxy 为被代理的实例
method 为需要调用实例的方法
args 为调用的时候接受的参数
上面三个参数的如何传递并不需要关心,这些 Proxy.newProxyInstance()
对自动生成填写合适参数的代理类。
然后就是 Proxy.newProxyInstance()
的定义了:
1 | public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException |
- loader 需要传入被代理实例的类加载器,使用
Instance.getClass().getClassLoader()
获得名字为 Instance 的被代理实例的类加载器,共给生成的代理类使用。 - ingerfaces 按照被代理类实现的接口来对应生成代理类,使用
Instance.getClass().getInterfaces()
就可以获得名字为 Instance 的被代理实例实现的接口了。 - h 一个
InvocationHandler
接口实现的类,需要实现其invoke()
方法,里面为实现需求的所有代码,在被调用的代理类的方法就是通过invoke()
作为模板生成的。
这样实现的好处:将数据和功能分离了,一些需要被增加的类变成了数据。我们只要关心如何实现增加的功能,不需要关心谁需要增加功能,里面的 “谁” 就变成了数据,只要在参数里面传入它,就能自动增加它的功能。
而实现这一切的核心就是反射机制让我们不用关心具体的对象是谁也可以操作它。