相关文章:
Java Relfect
Java RelfectUtils
反射一:基本类周边信息获取
反射二:泛型相关周边信息获取
反射三:类内部信息获取
JAVA 反射机制是在运行状态中,对于任何一个类,都能够知道这个类的所有属性和方法;对于任何一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为 JAVA 语言的反射机制。
一、引入
在开始反射之前,我们先看看 JVM 是如何将我们写的类对应的 java 文件加载到内存中的。
1、类的生命周期
这部分我们先讲讲 JVM 的加载机制。写一个最简单的 Main 函数,来看看这个函数的是如何被执行的,代码如下:
这段代码很简单,我们定义了一个 Animal 的类,在 main() 函数中,我们首先定义了一个 Animal 实例,然后调用了该实例的 setName() 方法。
大家都知道,在拿到一个 java 源文件后,如果要经过源码编译,要经过两个阶段:编译(javac.exe)、运行(java.exe)。
- 编译
|
|
在执行后在同一目录下生成 Test.class 和 Animal 类对应的文件Test$Animal.class(由于我们的 Animal 类是 Main 中的内部类,所以用 $ 表示)。
- 运行
|
|
在这一阶段,又分为三个小阶段:装载,链接,初始化。
- 装载
类的装载是通过类加载器完成的,加载器将 .class 文件的二进制文件装入 JVM 的方法区,并且在堆区创建描述这个类的 java.lang.Class 对象,用来封装数据。 但是同一个类只会被类装载器装载一次! - 链接
链接就是把二进制数据组装为可以运行的状态。链接分为校验、准备、解析这三个阶段。校验一般用来确认此二进制文件是否适合当前的 JVM(版本),准备就是为静态成员分配内存空间,并设置默认值。解析指的是转换常量池中的代码作为直接引用的过程,直到所有的符号引用都可以被运行程序使用(建立完整的对应关系)。 - 初始化
初始化就是对类中的变量进行初始化值。完成之后,类型也就完成了初始化,初始化之后类的对象就可以正常使用了,直到一个对象不再使用之后,将被垃圾回收,释放空间。
当没有任何引用指向 Class 对象时就会被卸载,结束类的生命周期。如果再次用到就再重新开始装载、链接和初始化的过程。
2、获取类类型
2.1、泛型隐藏填充类型
泛型隐藏填充类型默认填充为无界通配符?在上面,我们讲了,类只会被装载一次,利用装载的类可以实例化出各种不同的对象。而反射就是通过获取装载的类来做出各种操作的。装载的类,我们称为类类型,利用装载的类产生的实例,我们称为类实例。下面我们就看看,如何利用代码获取类类型的:
运行结果如下:
从结果中可以看出 class1 和 class2 是完全一样的,那构造他们时的方法一和方法二有什么区别呢?
可以看到这两个方法,右边全部都是 Animal.class,而左边却有些不同。方法一中,是直接生成了一个 Class 的实例。而在方法二中,则生成的是一个 Class 的泛型,并且使用的是无界通配符来填充的。我们都知道,Class 类是一个泛型,而泛型的正规写法就应该是
而方法一,只是把泛型的填充给省略了.在泛型中,如果把泛型的填充给省略掉,那就会默认填充为无界通配符”?”。所以方法一的真实写法是这样的:
所以这两种写法是意义是完全相同的。如果我们不用通配符,也就只能这样写:
2.2、获取类类型的方法
上面我们通过 Class<?> class1 = Animal.class; 即直接使用类名的 Class 对象来获取类类型,这只是其中一个方法,下面这四种方法都可以获得对应的类类型:
方法一:通过类实例的 getClass() 方法得到类类型;方法二:直接通过类的 class 对象得到;方法三和方法四中是通过类名得到,这两点要非常注意,这里的 className 一定要从包名具体到类名,唯一定位到一个类才行,不然就会报 ClassNotFound 错误。
在上面我们提到过,类只会被加载一次,所以 a、b、c、d 都是相等的,因为他们都是指向同一个对象,如果用等号操作符来判断的话:
result 的值为 true。
下面我们针对方法三和方法四举个粟子来看下:有一个单独的 Animal 类:
测试方法:
结果如下:
从上面的用法中,可以看出,我们要使用 Class.forName() 或者 getClassLoader().loadClass(),其中的类名必须是从包名到类名的完整路径。
从这里看来 Class.forName() 和 getClassLoader().loadClass() 是相同的,其实他们是有区别的。平时,我们不建议使用 getClassLoader().loadClass() 的方法来加载类类型。有关Class.forName() 和 getClassLoader().loadClass() 的具体区别,会在本篇末尾讲述。
二、基本类类型周边信息获取
我们知道类分为基本类和泛型类,这篇我们只讲基本类类型的周边信息获取,有关泛型类的周边信息获取,我们会放到下一篇中。
这部分主要讲述类类型周边信息获取方法,包括类名,包名,超类和继承接口。
1、类名、包名获取
相关的有三个函数:
函数使用如下:
结果如下:
从结果中很清晰的看到,class.getName() 获取的是类名包含完整路径。调用 Class.forName() 就是用的这个值。class.getSimpleName() 得到的是仅仅是一个类名。而 class.getPackage() 得到的是该类对应的 Package 对象。通过 package.getName() 能获得该类所对应的包名。
2、获取超类 Class 对象
获取 superClass 的类对象,涉及到两个函数:
getSuperclass() 用来获取普通函数,而 getGenericSuperclass() 是用来获取泛型类型的父类而设计的,有关 getGenericSuperclass() 的知识我们后面会讲,这里先看看 getSuperclass() 的用法。
我们仍然利用前面讲到的 Animal 类,然后在其上派生一个 AnimalImpl 子类:
然后使用:
结果如下:
在这里,我们使用了 Class.forName(“com.xxt.xtest.AnimalImpl”); 找到 AnimalImpl 的类类型对象,然后调用 class2.getSuperclass() 找到它的父类 Class 对象。很明显,它的父类是 Animal 类。由于我们这里得到了父类的 Class 对象 parentClass,所以可以对它使用 Class 的一切函数。所以调用 parentClass.getName() 就可以获得父类的名称了。
3、直接继承的接口的 Class 对象
这里要先声明一个观点:Class 类,不同于定义类的 class 标识,Class 类是一个泛型。类对象是由 Class 对象来表示,而接口对象同样也是用 Class 对象来表示。所以同样是 Class 对象,它可能表示的类对象,也可能表示的是接口对象。获取接口对象的函数如下:
与获取 superClass 对象一样,这里同样有两个函数来获取接口对象,有关 getGenericInterfaces() 获取泛型接口的方法,我们下篇再讲,这里先讲讲获取普通接口的方法 getInterfaces()。
getInterfaces() 将获取指定类直接继承的接口列表,注意是直接继承。如果不是直接继承,那将是获取不到的。举个例子,以上面的 Animal 为例:
我们先声明一个接口,让 Animal 类来继承:
然后是 Animal 类继承接口:
为了测试不是直接继承的接口是无法获取的问题,我们再从 Animal 派生一个子类 AnimalImpl:
我们再整理一下思路,Animal 类直接继承了 IAnimal,而 AnimalImpl 仅仅派生自 Animal,它的 IAnimal 接口不是直接继承的,而是从它的父类 Aniaml 那带过来的。
然后我们分别看看 Animal 类和 AnimalImpl 类的的获取接口的结果,完整的代码如下:
结果如下:
从结果可以看出,这里找到了 Animal 类所继承的接口值;但 AnimalImpl 获取到的接口列表为空。所以这也证明了 getInterfaces() 只能获取类直接继承的接口列表。
4、获取某个类类型的所有接口
如果我想传进去一下类类型,然后要得到它所有继承的接口列表要怎么办?不管它是不是直接继承来的都要列出来。
那只有靠递规了,我们需要递规它的父类直接继承的接口、父类的父类直接继承的接口以此类推,最终到 Object 类的时候就找到所有继承的接口了。
在开始递规获取所有接口之前,我们先构造下代码。由于我们要获取所有接口,为了效果更好些,我们在 Animal 和 AnimalImpl 基础上,多加几个继承的接口:
所以如果我们获取 AnimalImpl 类的接口列表,得到的应该是三个:自已直接继承的 Serializable、从父类 Animal 那继承的 IAnimal 和 Serializable。获取类类型所有接口列表的方法:
测试代码:
先看看执行结果:
这段代码最关键的地方在于 getAllInterface(Class<?> clazz);代码分为两部分,第一部分是获得自己的接口列表和父类的列表:通过 Class<?>[] interSelf = clazz.getInterfaces(); 获得自已直接继承的接口列表。然后,通过 Class<?> superClazz = clazz.getSuperclass(); 获取父类的 Class 类型,然后调用 getAllInterface(superClazz) 获得父类的所有接口列表。那么,把它们两个合并,就是所有的接口列表了。
合并逻辑:对 interParent 和 interSelf 判空,如果两个列表都是空,那直接返回空;如果有一个是空,另一个不是空,则返回那个不是空的列表;如果两个都不是空,则将他们合并,然后返回合并后的列表。
5、获取类的访问修饰符
由于我们在定义类时,比如下面的内部类:
在类名,前面的那一坨 public static final,就是类的访问修饰符,是定义这个类在的访问区域和访问限定的。这部分就讲讲如何获取类的这部分访问修饰符,以上面的内部类 InnerClass 为例:
结果如下:
首先,在这部分代码中,我们又换了一种类加载方式,使用的是 ClassLoader。然后我们单独来看看这句:
通过 clazz.getModifiers() 得到一个整型变量,由于访问修饰符有很多,所以这些修饰符被打包成一个 int,对应的二进制中,每个修饰符是一个标志位,可以被置位或清零。另外 Java 开发人员单独提供了一个类来提取这个整型变量中各标识位的函数,这个类就是 Modifier。Modifier 中主要有以下几个方法:
首先是 toString 函数:这个函数的作用就是根据传进来的整型,获取其中的标识位来判断具有哪个修饰符,然后将所有修饰符拼接起来输出。比如我们的例子中输出的就是:public static final。
其它的就是一些 isXXXX(int moifiers) 的判断指定标识位的函数了。在例子中,我们使用了 Modifier.isFinal(int modifiers) 来判断是不是具有 final 修饰符,返回结果为 true。
6、获取接口的访问修饰符
从上面获取类的访问修饰符时,接口、类、函数都是通过 Modifier 类判断访问修饰符的,又因为类和接口类型全部都是用 Class 对象来标识,所以接口和类的获取访问修饰符的方式完全相同,下面就举一个简单的例子:
测试代码:
如果我们要直接获取一个接口的对象,同样,也是通过开头所讲的那四种获取Class对象的方式。因为我们现在知道 Class 对象,不光代表类也可以代表接口。有关 Modifier 的使用与第五部分获取类的修饰符是一样的。
7、Class.forName 与 ClassLoader.loadClass 的区别
我们通过源码来看看他们的区别。先看 Class.forName:
从源中可以看到 Class.forName(String className) 最终调用的是 forName(String className, boolean initializeBoolean, ClassLoader classLoader) 函数。
其中:
- className:类名。
- initializeBoolean:表示是否需要初始化;如果设为 true,表示在加载以后,还会进入链接阶段。
- classLoader:ClassLoader 加载器。
我们知道源文件在编译后,在运行时,分为三个阶段:加载、链接和初始化。这里的 initializeBoolean 就是定义是否进行链接和初始化。而 Class.forName 默认是设置的为 true。所以利用 Class.forName() 得到的类类型,除了加载进来以外,还进行了链接和初始化操作。
下面再来看看 ClassLoader.loadClass()
loadClass(String className) 最终是调用递规函数 loadClass(String className, boolean resolve) 来将类加载出来。通过代码也可以看出来 ClassLoader 的 loadClass(String className) 只是将类加载出来,并没有链接与初始化的步骤。
最后,我们总结一下,Class.forName(String className) 不仅会将类加载进来,而且会对其进行初始化,而 ClassLoader.loadClass(String ClassName) 则只是将类加载进来,而没有对类进行初始化。一般来讲,他们两个是通用的,但如果你加载类依赖初始化值的话,那 ClassLoader.loadClass(String ClassName) 将不再适用。
举例来说:在 JDBC 编程中,常看到这样的用法。Class.forName(“com.mysql.jdbc.Driver”); 如果换成了 getClass().getClassLoader().loadClass(“com.mysql.jdbc.Driver”); 就不行。
为什么呢?打开 com.mysql.jdbc.Driver 的源代码看看:
原来,Driver 在 static 块中会注册自己到 java.sql.DriverManager。而 static 块就是在 Class 的初始化中被执行。所以这个地方就只能用Class.forName(className)。
这篇文章所涉及到的几个函数: