Java反射与注解
本章将会深入介绍 Java类的加载、连接和初始化知识,并重点介绍 java.lang.reflect 包下的接口和类,包括 Class、Method、Field、Constructor 和 Array等,这些类分别代表类、方法、成员变量、构造器和数组,Java 程序可以使用这些类动态地获取某个对象。某个类的运行时信息,并可以动态地创建 Java 对象,动态地调用Java方法,访问并修改指定对象的成员变量值。
类的加载
当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接、初始化三个步骤来对该类进行初始化。如果没有意外,JVM 将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或类初始化。
类加载指的是将类的** class 文件**读入内存,并为之创建一个 java.lang.Class 对象,也就是说,当程序中使用任何类时,系统都会为之建立一个 java.lang.Class 对象。
类的加载由类加载器完成,类加载器通常由 JVM提供,这些类加载器也是前面所有程序运行的基础,JVM 提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承 ClassLoader基类来创建自己的类加载器。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。
- 从本地文件系统加载 class 文件,这是前面绝大部分示例程序的类加载方式。
- 从JAR 包加载 class 文件,这种方式也是很常见的,前面介绍 JDBC 编程时用到的数据库驱动类 就放在 JAR 文件中,JVM 可以从 JAR 文件中直接加载该 class 文件。
- 通过网络加载class 文件。
- 把一个 Java 源文件动态编译,并执行加载。
Class文件结构
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。一个Class文件包含一下几部分:
- 魔数:每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如gif或者jpeg等在文件头中都存有魔数。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。文件格式的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过同时又不会引起混淆即可。
- 版本号:紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1(JDK 1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
- 常量池:紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,常量池中主要存放两大类常量:字面量(Literal)和符号引用(SymbolicReferences)。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 访问标志:在常量池结束之后,紧接着的两个字节代表访问标志(access_fags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。
- 类索引、父类索引、与接口索引集合:Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。
- 字段表集合:字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。描述实例变量和类变量以及实例变量的信息包括:字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。
- 方法表集合:Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。
- 属性表集合:在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。
类加载器
类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被再次载入了。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。
当JVM启动时,会形成由3个类加载器组成的初始类加载器层次结构。
- Bootstrap ClassLoader:根类加载器。它负责加载Java的核心类。
- Extension ClassLoader:扩展类加载器。它负责加载 JRE 的扩展目录(%JAVA_HOME%/jre/lib/ext)中JAR包的类。
- System ClassLoader:系统类加载器。它负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。
类加载机制
JVM的类加载机制主要有如下3种。
- 全盘负责,所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他 Class 也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
- 双亲委派,所谓父类委托,则是先让 parent(父)类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
- 缓存机制。缓存机制将会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区中。
类加载过程
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking),这7个阶段的发生顺序如下图所示。
下面将详细讲解在类加载过程中每一步中的详细过程:
1.加载
“加载”是“类加载”(Class Loading)过程的一个阶段,希望读者没有混淆这两个看起来很相似的名词。在加载阶段,虚拟机需要完成以下3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2.验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 文件格式验证,第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这一阶段可能包括下面这些验证点:
- 是否以魔术0xCAFEBABE
- 主、次版本号是否在当前虚拟机处理范围内
- 常量池的常量中是否有不被支持的类型
- 指向常量的各种索引值是否有执行不存在的常量或者不符合类型的常量
- CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
- Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
- 元数据验证,第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,这个阶段可能包括的验证点如下:
- 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。
- 这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
- 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。
- 字节码验证,第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完校验后,这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件,例如:
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似
- 这样的情况:在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中。
- 保证跳转指令不会跳转到方法体以外的字节码指令上。
- 保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。
- 符号引用验证,最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生,符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要校验下列内容:
- 符号引用中通过字符串描述的全限定名是否能找到对应的类。
- 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
- 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。
3.准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的概念需要强调一下,首先,这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
4.解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。直接引用则是指:可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。
5.初始化
类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。
通过反射获取类信息
获取Class对象
每个类被加载之后,系统就会为该类生成一个对应的Class对象,通过该Class对象就可以访问到JVM中的这个类。在Java程序中获得Class对象通常有如下3种方式。
- 使用Class类的forName(String clazzName)静态方法。该方法需要传入字符串参数,该字符串参数的值是某个类的全限定类名(必须添加完整包名)。
- 调用某个类的class属性来获取该类对应的Class对象。例如,Person.class将会返回Person类对应的Class对象。
- 调用某个对象的getClass()方法。该方法是java.lang.Object类中的一个方法,所以所有的Java对象都可以调用该方法,该方法将会返回该对象所属类对应的Class对象。
下面,通过示例来演示获取Class对象
1 | package cn.bytecollege; |
获取构造器
Class类提供了大量的实例方法来获取该Class对象所对应类的详细信息,下面4个方法用于获取Class对应类所包含的构造器。
下面4个方法用于获取Class对应类所包含的构造器。
- Connstructor getConstructor(Class<?>… parameterTypes):返回此Class对象对应类的指定public构造器。
- Constructor<?>[]getConstructors():返回此Class对象对应类的所有public构造器。
- Constructor
getDeclaredConstructor(Class<?>…parameterTypes):返回此Class对象对应类的指定构造器,与构造器的访问权限无关。 - Constructor<?>[]getDeclaredConstructors():返回此Class对象对应类的所有构造器,与构造器的访问权限无关。
下面通过示例演示获取构造器:
1 | package cn.bytecollege; |
1 | package cn.bytecollege; |
通过反射可以获取类中定义的构造方法,获取了构造方法就可以利用反射获取的构造方法来创建对象。
通过反射来生成对象有如下两种方式。
- 使用Class对象的newInstance()方法来创建该Class对象对应类的实例,这种方式要求该 Class 对象的对应类有默认构造器,而执行 newInstance()方法时实际上是利用默认构造器来创建该类的实例。
- 先使用 Class 对象获取指定的 Constructor 对象,再调用 Constructor 对象的newInstance()方法来创建该Class对象对应类的实例。通过这种方式可以选择使用指定的构造器来创建实例。
下面通过示例来演示利用反射创建对象:
1 | package cn.bytecollege; |
获取类包含的方法
下面4个方法用于获取Class对应类所包含的方法。
- Method getMethod(String name,Class<?>…parameterTypes):返回此Class对象对应类的指定public方法。
- Method[]getMethods():返回此Class对象所表示的类的所有public方法。
- Method getDeclaredMethod(String name,Class<?>…parameterTypes):返回此Class对象对应类的指定方法,与方法的访问权限无关。
- Method[] getDeclaredMethods():返回此Class对象对应类的全部方法,与方法的访问权限无关。
1 | package cn.bytecollege; |
运行后的结果是:
每个Method对象对应一个方法,获得Method对象后,程序就可通过该Method来调用它对应的方法。在Method里包含一个invoke()方法,该方法的签名如下。
- Object invoke(Object obj,Object…args):该方法中的obj是执行该方法的主调,后面的args是执行该方法时传入该方法的实参。
1 | package cn.bytecollege; |
获取类包含的Field
- Field getField(String name):返回此Class对象对应类的指定public Field。
- Field[]getFields():返回此Class对象对应类的所有public Field。
- Field getDeclaredField(String name):返回此Class对象对应类的指定Field,与Field的访问权限无关。
- Field[] getDeclaredFields():返回此Class对象对应类的全部 Field,与Field的访问权限无关。
1 | package cn.bytecollege; |
1 | package cn.bytecollege; |
通过Class对象的getFields()或getField()方法可以获取该类所包括的全部Field或指定Field。Field提供了如下两组方法来读取或设置Field值。
- getXxx(Object obj):获取obj对象该Field的属性值。此处的Xxx对应8个基本类型,如果该属性的类型是引用类型,则取消get后面的Xxx。
- setXxx(Object obj,Xxx val):将obj对象的该Field设置成val值。此处的Xxx对应8个基本类型,如果该属性的类型是引用类型,则取消set后面的Xxx。
反射操作数组
在java.lang.reflect包下还提供了一个Array类,Array对象可以代表所有的数组。程序可以通过使用Array来动态地创建数组,操作数组元素等。
Array提供了如下几类方法。
- static Object newInstance(Class<?>componentType,int…length):创建一个具有指定的元素类型、指定维度的新数组。
- static xxx getXxx(Object array,int index):返回array数组中第index个元素。其中xxx是各种基本数据类型,如果数组元素是引用类型,则该方法变为get(Object array,int index)。
- static void setXxx(Object array,int index,xxx val):将array数组中第index个元素的值设为val。其中xxx是各种基本数据类型,如果数组元素是引用类型,则该方法变成set(Object array,int index,Object val)。
下面程序示范了如何使用Array来生成数组,为指定数组元素赋值,并获取指定数组元素的方式:
1 | package cn.bytecollege; |
注解
Java 5开始,Java增加了元数据(MetaData)的支持,也就是Annotation(注解),注解可以理解为代码里的特殊标识,这些标识可以在编译、类加载、运行时被读取,并进行相应的处理。通过Annotation,开发者可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。
Annotation是一个特殊的接口,程序可以通过反射来获取指定程序元素的Annotation对象,然后通过Annotation对象来取得注释里的元数据。
基本Annotation
Java提供的4个基本Annotation的用法——使用Annotation时要在其前面增加@符号,并把该Annotation当成一个修饰符使用,用于修饰它支持的程序元素。
4个基本的Annotation如下:
- @Override:用来指定方法重写的,它可以强制一个子类必须覆盖父类的方法。
- @Deprecated:用于表示某个程序元素(类、方法等)已过时,当其他程序使用已过时的类、方法时,编译器将会给出警告。
- @SuppressWarnings:指示被该Annotation修饰的程序元素(以及该程序元素中的所有子元素)取消显示指定的编译器警告。
- @SafeVarargs:是Java 7专门为抑制“堆污染”警告提供的(了解)。
元注解
JDK除了在java.lang下提供了4个基本的Annotation之外,还在java.lang.annotation包下提供了4个Meta Annotation(元Annotation),这4个元Annotation都用于修饰其他的Annotation定义。可以理解为在注解中使用的注解。
@Retention
@Retention只能用于修饰一个Annotation定义,用于指定被修饰的Annotation可以保留多长时间,@Retention包含一个RetentionPolicy类型的value成员变量,所以使用@Retention时必须为该value成员变量指定值。
value成员变量的值只能是如下3个。
- RetentionPolicy.CLASS:编译器将把Annotation记录在class文件中。当运行Java程序时,JVM不再保留Annotation。这是默认值。
- RetentionPolicy.RUNTIME:编译器将把Annotation记录在class文件中。当运行Java程序时,JVM也会保留Annotation,程序可以通过反射获取该Annotation信息。
- RetentionPolicy.SOURCE:Annotation只保留在源代码中,编译器直接丢弃这种Annotation。
如果需要通过反射获取注释信息,就需要使用value属性值为RetentionPolicy.RUNTIME的@Retention。使用@Retention元数据Annotation可采用如下代码为value指定值。
1 | package cn.bytecollege; |
如果Annotation里只有一个value成员变量,使用该Annotation时可以直接在Annotation后的括号里指定value成员变量的值,无须使用name=value的形式。查看元注解@Rentention源码可以发现@Rentention注解中只有一个value属性:
1 |
|
因此在上例中的代码可以进行简写:
1 | package cn.bytecollege; |
@Target
@Target也只能修饰一个Annotation定义,它用于指定被修饰的Annotation能用于修饰哪些程序单元。@Target元Annotation也包含一个名为value的成员变量,该成员变量的值只能是如下几个。
- ElementType.ANNOTATION_TYPE:指定该策略的Annotation只能修饰Annotation。
- ElementType.CONSTRUCTOR:指定该策略的Annotation只能修饰构造器。
- ElementType.FIELD:指定该策略的Annotation只能修饰成员变量。
- ElementType.LOCAL_VARIABLE:指定该策略的Annotation只能修饰局部变量。
- ElementType.METHOD:指定该策略的Annotation只能修饰方法定义。
- ElementType.PACKAGE:指定该策略的Annotation只能修饰包定义。
- ElementType.PARAMETER:指定该策略的Annotation可以修饰参数。
- ElementType.TYPE:指定该策略的Annotation可以修饰类、接口(包括注释类型)或枚举定义。
与使用@Retention类似的是,使用@Target也可以直接在括号里指定value值,而无须使用name=value的形式
1 | package cn.bytecollege; |
@Documented
@Documented用于指定被该元Annotation修饰的Annotation类将被javadoc工具提取成文档,如果定义Annotation类时使用了@Documented修饰,则所有使用该Annotation修饰的程序元素的API文档中将会包含该Annotation说明。
1 | package cn.bytecollege; |
@Inherited
@Inherited元Annotation指定被它修饰的Annotation将具有继承性——如果某个类使用了@A Annotation(定义该Annotation时使用了@Inherited修饰)修饰,则其子类将自动被@A修饰。
1 | package cn.bytecollege; |
自定义注解
定义新的Annotation类型使用@interface关键字(在原有的interface关键字前增加@符号)定义一个新的Annotation类型与定义一个接口非常像,如下代码可定义一个简单的Annotation类型。
1 | public Test{ |
定义了该Annotation之后,因为没有指定@Rentention,因此就可以在程序的任何地方使用该Annotation。在默认情况下,Annotation可用于修饰任何程序元素,包括类、接口、方法等。
Annotation不仅可以是这种简单的Annotation,还可以带成员变量,Annotation的成员变量在Annotation定义中以无形参的方法形式来声明,其方法名和返回值定义了该成员变量的名字和类型。如下代码可以定义一个有成员变量的Annotation。
1 | public MyAnno{ |
一旦在Annotation里定义了成员变量之后,使用该Annotation时就应该为该Annotation的成员变量指定值,如下代码所示。
1 | package cn.bytecollege; |
也可以在定义Annotation的成员变量时为其指定初始值(默认值),指定成员变量的初始值可使用default关键字。如下代码定义了@MyAnno Annotation,该Annotation里包含了两个成员变量:name和age,这两个成员变量使用default指定了初始值。
1 | package cn.bytecollege; |
如果为Annotation的成员变量指定了默认值,使用该Annotation时则可以不为这些成员变量指定值,而是直接使用默认值。
获取Annotation的信息
当开发者使用Annotation修饰了类、方法、Field等成员之后,这些Annotation不会自己生效,必须由开发者提供相应的工具来提取并处理Annotation信息。Java使用Annotation接口来代表程序元素前面的注释,该接口是所有Annotation类型的父接口。Java 5在java.lang.reflect包下新增了AnnotatedElement接口,该接口代表程序中可以接受注释的程序元素。该接口主要有如下几个实现类。
- Class:类定义。
- Constructor:构造器定义。
- Field:类的成员变量定义。
- Method:类的方法定义。
- Package:类的包定义。
AnnotatedElement接口是所有程序元素(如Class、Method、Constructor等)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象(如Class、Method、Constructor等)之后,程序就可以调用该对象的如下3个方法来访问Annotation信息。
- getAnnotation(Class
annotationClass):返回该程序元素上存在的指定类型的注释,如果该类型的注释不存在,则返回null。 - Annotation[]getAnnotations():返回该程序元素上存在的所有注释。
- boolean isAnnotationPresent(Class< ? extendsAnnotation>annotationClass):判断该程序元素上是否存在指定类型的注释,如果存在则返回true,否则返回false。
获取注解
在下面的示例中,结合自定义注解,获取Class上的注解
1 | package cn.bytecollege.anno; |