第5章- Java反射
14 Class类与Java反射机制
(示例: basic.reflect.ReflectDemo)
通过Java反射机制,可以在程序中访问已经装载到JVM中的Java对象的描述,实现访问、监测、修改描述Java对象本身信息的功能。所有Java类均继承Object类, 在Object类中定义了getClass()方法, 该方法返回一个类型为Class的对象
在通过getFields()和getMethods()方法一次获取权限为public的成员变量和方法时, 还包括从超类中继承的变量和方法,而getDeclaredFields()和getDeclaredMethods()方法只获得本类中定义的所有成员变量和方法
组成部分 | 访问方法 | 返回值类型 | 说明 |
包路径 | getPackage() | Package对象 | 获取该类的存放路径 |
类名称 | getName() | String对象 | 获取该类的名称 |
继承类 | getSuperclass() | Class对象 | 获取该类继承的类 |
实现接口 | getInterface() | Class型数组 | 获取该类实现的所有接口 |
构造方法 | getConstructors() | Constructor型数组 | 获取所有权限为public的构造方法 |
getConstructor(Class<?>... parameterTypes) | Constrctor对象 | 获取权限为public的指定构造方法 | |
getDeclaredConstructors() | Constructor型数组 | 获取所有构造方法,按声明顺序返回 | |
getDeclaredConstructor (Class<?>... parameterTypes) | Constrctor对象 | 获取指定构造方法 | |
方法 | getMethods() | Method型数组 | 获取所有权限为public的方法 |
getMethod(String name, Class<?>... parameterTypes) | Method对象 | 获取权限为public的指定方法 | |
getDeclaredMethods() | Method型数组 | 获取所有方法, 按声明顺序返回 | |
getDeclaredMethod(String name, Class<?>... parameterTypes) | Method对象 | 获取指定方法 | |
成员变量 | getFields() | Field型数组 | 获取所有权限为public的成员变量 |
getField(String name) | Field对象 | 获取权限为public的指定成员变量 | |
getDeclaredFields() | Field型数组 | 获取所有成员变量,按声明顺序返回 | |
getDeclaredField(String name) | Field对象 | 获取指定成员变量 | |
内部类 | getClasses() | Class型数组 | 获取所有权限为public的内部类 |
getDeclaredClasses() | Class型数组 | 获取所有内部类 | |
内部类的声明类 | getDeclaringClass() | Class对象 | 如果该类为内部类则返回它的成员类,否则返回null |
14.1 访问构造方法
通过下列方法访问构造方法时, 将返回Constructor类型的对象或数组, 每个Constructor对象代表一个构造函数, 利用Constructor对象可以操纵相应的构造方法
1).getConstructors()
2).getConstructors(Class<?>... parameterType)
3).getDeclaredConstructors()
4).getDeclaredConstructor(Class<?>... parameterType)
如果访问指定的构造方法, 需要根据该构造方法的入口参数的类型来访问. 例如访问一个入口参数类型依次为String和int型的构造方法, 可以通过
1).objectClass.getDeclaredConstructor(String.class, int.class);
2).objectClass.getDeclaredConstructor(new Class[]{String.class, int.class});
Constructor类中的常用方法
方法 | 说明 |
isVarArgs() | 查看该构造方法是否允许带有可变数量的参数, 允许返回true, 否则返回false |
getParameterTypes() | 按照声明顺序以Class数组的形式获得该构造方法的各个参数的类型 |
getExceptionTypes() | 以Class数组的形式获得该构造方法的各个参数的类型 |
newInstance(Object... initargs) | 通过该构造方法利用指定参数创建一个该类的对象, 如果未设置参数则表示采用默认无参数的构造方法 |
setAccessible(boolean flag) | 如果该构造方法权限为private, 默认不允许通过反射利用newInstance(Object...initargs)方法创建对象. 如果先执行该方法, 并将入口参数设为true, 则允许创建 |
getModifiers() | 获得可以解析出该构造方法所采用修饰符的正数 |
通过java.lang.reflect.Modifier类可以解析出getModifiers()方法的返回值所表示的修饰符信息, 在该类中提供了用来解析的静态方法, 既可以查看是否被指定的修饰符修饰, 还可以以字符串的形式获得所有修饰符.
Modifier类中的常用静态方法
静态方法 | 说明 |
isPublic(int mod) | 查看是否被public修饰符修饰, 如果是返回true, 否则返回false |
isProtected(int mod) | 查看是否被protected修饰符修饰, 如果是返回true,否则返回false |
isPrivate(int mod) | 查看是否被private修饰符修饰, 如果是返回true,否则返回false |
isStatic(int mod) | 查看是否被static修饰符修饰, 如果是返回true, 否则返回false |
isFinal(int mod) | 查看是否被final修饰符修饰, 如果是返回true, 否则返回false |
toString(int mod) | 以字符串的形式返回所有修饰符 |
14.2 访问成员变量
通过下列方法访问成员变量时, 将返回Field类型的对象或数组, 每个Field对象代表一个成员变量, 利用Field对象可以操纵相应的成员变量
1).getFields()
2).getField(String name)
3).getDeclaredFields()
4).getDeclaredField(String name)
如果是访问指定的成员变量, 可以通过该成员变量的名称来访问.例如访问一个名称为birthday的成员变量:
object.getDeclaredField("birthday")
Field类中的常用方法
方法 | 说明 |
getName() | 获取该成员变量的名称 |
getType() | 获取表示该成员变量类型的Class对象 |
get(Object obj) | 获取指定对象obj中该成员变量的值, 返回值为Object型 |
set(Object obj, Object value) | 将指定对象obj中该成员变量的值设置为value |
getInt(Object obj) | 获取指定对象obj中类型为int的该成员变量的值 |
setInt(Object obj, int i) | 将指定对象obj中类型为int的该成员变量的值设置为i |
getFloat(Object obj) | 获取指定对象obj中类型为float的该成员变量的值 |
setFloat(Object obj, float f) | 将指定对象obj中类型为float的该成员变量的值设置为f |
getBoolean(Object obj) | 获取指定对象obj中类型为boolean的该成员变量的值 |
setBoolean(Object obj, boolean z) | 将指定对象obj中类型为boolean的该成员变量的值设置为z |
setAccessible(boolean flag) | 如果该构造方法的权限为private, 默认不允许通过反射利用newInstance(Object... initargs)方法创建对象, 如果先执行该方法, 并将入口参数设为true,则允许创建 |
getModifiers() | 获得可以解析出该成员变量所采用修饰符的正数 |
14.3 访问方法
通过下列方法访问方法时, 将返回Method类型的对象或数组, 每个Method对象代表一个方法, 利用Method对象可以操作相应的方法.
1).getMethods()
2).getMethod(String name, Class<?>... parameterTypes)
3).getDeclaredMethods()
4).getDeclaredMethod(String name, Class<?>... parameterTypes)
如果是访问指定的方法, 需要根据该方法的名称和入口参数的类型来访问, 例如访问一个名称为pring, 入口参数类型依次为String和int型的方法, 可以通过下面两种方式实现
1).objectClass.getDeclaredMethod("pring", String.class, int.class)
2).objectClass.getDeclaredMethod("pring", new Class[]{String.class, int.class})
方法 | 说明 |
getName() | 获得该方法的名称 |
getParameterTypes() | 按照声明顺序以Class数组的形式获得该方法的各个参数的类型 |
getReturnType() | 以Class对象的形式获得该方法的返回值的类型 |
getExceptionTypes() | 以Class数组的形式获得该方法可能抛出的异常类型 |
invoke(Object obj, Object... args) | 利用指定参数args执行指定对象obj中的该方法, 返回值为Object型 |
isVarArgs() | 查看构造方法是否允许带有可变数量的参数, 如果允许返回true, 否则返回false |
getModifiers() | 获得可以解析出该方法所采用修饰符的整数 |
14.4 注解(Annotation)的使用
(示例: basic.demo14.annotation)
14.4.1 什么是注解(Annotation)
Annotation(注解)就是Java提供了一种元程序中的元素关联任何信息和着任何元数据(metadata)的途径和方法. Annotion(注解)是一个接口, 程序可以通过反射来获取指定程序元素的Annotion对象, 然后通过Annotion对象来获取注解里面的元数据
Annotation(注解)是JDK5.0及以后版本引入的. 它可以用于创建文档, 跟踪代码中的依赖性, 甚至执行基本编译时检查. 从某些方面看, annotation就像修饰符一样被使用, 并应用于包、类型、构造方法、方法、成员变量、参数、本地变量的声明中. 这些信息被存储在Annotation的"name=value"结构对中.
Annotation的成员在Annotation类型中以无参数的方法的形式被声明. 其方法名和返回值定义了该成员的名字和类型. 在此有一个特定的默认语法: 允许声明任何Annotation成员的默认值: 一个Annotation可以将name=value对作为没有定义默认值的Annotation成员的值, 当然也可以使用name=value对来覆盖其它成员默认值. 这一点有些近似类的继承特性, 父类的构造函数可以作为子类的默认构造函数, 但是也可以被子类覆盖
Annotation能被用来为某个程序元素(类、方法、成员变量等)关联任何的信息. 需要注意的是, 这里存在着一个基本的规则: Annotation不能影响程序代码的执行, 无论增加、删除Annotation, 代码都始终如一的执行. 另外, 尽管一些annotation通过java的反射api方法在运行时被访问, 而java语言解释器在工作时忽略了这些annotation. 正是由于java虚拟机忽略了Annotation, 导致了annotation类型在代码中是"不起作用"的; 只有通过某种配套的工具才会对annotation类型中的信息进行访问和处理. 本文中将涵盖标准的Annotation和meta-annotation类型, 陪伴这些annotation类型的工具是java编译器(当然要以某种特殊的方式处理它们)
14.4.2 metadata(元数据)
元数据从metadata一词译来, 就是"关于数据的数据"的意思.
元数据的功能作用有很多, 比如: 你可能用过Javadoc的注释自动生成文档. 这就是元数据功能的一种. 总的来说, 元数据可以用来创建文档, 跟踪代码的依赖性, 执行编译时格式检查, 代替已有的配置文件. 如果要对于元数据的作用进行分类, 目前还没有明确的定义, 不过我们可以根据它所起的作用, 大致可分为三类:
1). 编写文档:通过代码里标识的元数据生成文档
2). 代码分析:通过代码里标识的元数据对代码进行分析
3). 编译检查:通过代码里标识的元数据让编译器能实现基本的编译检查
在Java中元数据以标签的形式存在于Java代码中, 元数据标签的存在并不影响程序代码的编译和执行, 它只是被用来生成其它的文件或针在运行时知道被运行代码的描述信息.
综上所述:
1).元数据以标签的形式存在于Java代码中
2).元数据描述的信息是类型安全的, 即元数据内部的字段都是有明确类型的
3).元数据需要编译器之外的工具额外的处理用来生成其它的程序部件
4).元数据可以只存在于Java源代码级别, 也可以存在于编译之后的Class文件内部
14.4.3 注解的分类:
根据注解参数的个数, 我们可以将注解分为三类:
1).标记注解: 一个没有成员定义的Annotation类型被称为标记注解. 这种Annotation类型仅使用自身的存在与否来为我们提供信息. 比如后面的系统注解@Override
2).单值注解
3).完整注解
根据注解使用方法和用途,我们可以将Annotation分为三类:
1).JDK内置系统注解
2).元注解
3).自定义注解
14.4.4 系统内置标准注解
注解的语法比较简单, 除了@符号的使用外, 他基本与Java固有的语法一致, JavaSE中内置三个标准注解, 定义在java.lang中:
1).@Override:用于修饰此方法覆盖了父类的方法;
2).@Deprecated:用于修饰已经过时的方法;
3).@SuppressWarnnings: 用于通知java编译器禁止特定的编译警告
下面我们依次看看三个内置标准注解的作用和使用场景
14.4.4.1 @Override(限定重写父类方法)
@Override是一个标记注解类型, 它被用作标注方法. 它说明了被标注的方法重载了父类的方法, 起到了断言的作用. 如果我们使用了这种Annotation在一个没有覆盖父类方法的方法时, java编译器将以一个编译错误来警示. 这个annotaton常常在我们试图覆盖父类方法而确又写错了方法名时发挥威力
14.4.4.2 @Deprecated,标记已过时:
同样Deprecated也是一个标记注解. 当一个类型或者类型成员使用@Deprecated修饰的话, 编译器将不鼓励使用这个被标注的程序元素. 而且这种修饰具有一定的"延续性"
如果我们在代码中通过继承或者覆盖的方式使用了这个过时的类型或者成员, 虽然继承或者覆盖后的类型或者成员并不是被声明为@Deprecated, 但编译器仍然要报警
值得注意, @Deprecated这个annotation类型和javadoc中的@deprecated这个tag是有区别的: 前者是java编译器识别的, 而后者是被javadoc工具所识别用来生成文档(包含程序成员为什么已经过时、它应当如何被禁止或者替代的描述)
在java5.0, java编译器仍然象其从前版本那样寻找@deprecated这个javadoc tag, 并使用它们产生警告信息. 但是这种状况将在后续版本中改变, 我们应在现在就开始使用@Deprecated来修饰过时的方法而不是@deprecated javadoc tag
14.4.4.3 SuppressWarnnings(抑制编译器警告)
@SuppressWarnings被用于有选择的关闭编译器对类、方法、成员变量、变量初始化的警告. 在java5.0, sun提供的javac编译器为我们提供了-Xlint选项来使编译器对合法的程序代码提出警告, 此种警告从某种程度上代表了程序错误. 例如当我们使用一个generic collection类而又没有提供它的类型时, 编译器将提示出"unchecked warning"的警告. 通常当这种情况发生时, 我们就需要查找引起警告的代码. 如果它真的表示错误, 我们就需要纠正它. 例如如果警告信息表明我们代码中的switch语句没有覆盖所有可能的case, 那么我们就应增加一个默认的case来避免这种警告
有时我们无法避免这种警告, 例如, 我们使用必须和非generic的旧代码交互的generic collection类时, 我们不能避免这个unchecked warning. 此时@SuppressWarning就要派上用场了, 在调用的方法前增加@SuppressWarnings修饰, 告诉编译器停止对此方法的警告
SuppressWarning不是一个标记注解. 它有一个类型为String[]的成员, 这个成员的值为被禁止的警告名. 对于javac编译器来讲, 被-Xlint选项有效的警告名也同样对@SuppressWarings有效, 同时编译器忽略掉无法识别的警告名
SuppressWarnings注解的常见参数值的简单说明:
1).deprecation:使用了不赞成使用的类或方法时的警告
2).unchecked:执行了未检查的转换时的警告, 例如当使用集合时没有用泛型(Generics)来指定集合保存的类型;
3).fallthrough:当Switch 程序块直接通往下一种情况而没有Break 时的警告;
4).path:在类路径、源文件路径等中有不存在的路径时的警告;
5).serial:当在可序列化的类上缺少serialVersionUID 定义时的警告;
6).finally:任何finally 子句不能正常完成时的警告;
7).all:关于以上所有情况的警告
14.4.5 元注解
元注解的作用就是负责注解其他注解。Java5.0定义了4个标准的meta-annotation类型,它们被用来提供对其它annotation类型作说明。Java5.0定义的元注解:
1).@Target 2).@Retention 3).@Documented 4).@Inherited
这些类型和它们所支持的类在java.lang.annotation包中可以找到。下面我们看一下每个元注解的作用和相应分参数的使用说明
14.4.5.1 @Target
@Target说明了Annotation所修饰的对象范围: Annotation可被用于packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数). 在Annotation类型的声明中使用了target可更加明晰其修饰的目标
作用: 用于描述注解的使用范围(即: 被描述的注解可以用在什么地方)
取值(ElementType)有:
1).CONSTRUCTOR: 用于描述构造器
2).FIELD: 用于描述域
3).LOCAL_VARIABLE: 用于描述局部变量
4).METHOD: 用于描述方法
5).PACKAGE: 用于描述包
6).PARAMETER: 用于描述参数
7).TYPE: 用于描述类、接口(包括注解类型) 或enum声明
14.4.5.2 @Retention:
@Retention定义了该Annotation被保留的时间长短: 某些Annotation仅出现在源代码中, 而被编译器丢弃; 而另一些却被编译在class文件中; 编译在class文件中的Annotation可能会被虚拟机忽略, 而另一些在class被装载时将被读取(请注意并不影响class的执行, 因为Annotation与class在使用上是被分离的). 使用这个meta-Annotation可以对Annotation的"生命周期"限制.
作用: 表示需要在什么级别保存该注释信息, 用于描述注解的生命周期(即: 被描述的注解在什么范围内有效)
取值(RetentionPoicy)有:
1).SOURCE: 在源文件中有效(即源文件保留)
2).CLASS: 在class文件中有效(即class保留)
3).RUNTIME: 在运行时有效(即运行时保留,注解处理器可以通过反射, 获取到该注解的属性值, 从而去做一些运行时的逻辑处理)
Retention meta-annotation类型有唯一的value作为成员, 它的取值来自java.lang.annotation.RetentionPolicy的枚举类型值.
14.4.5.3 @Documented:
@Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API, 因此可以被例如javadoc此类的工具文档化. Documented是一个标记注解, 没有成员
14.4.5.4 @Inherited:
@Inherited 元注解是一个标记注解, @Inherited阐述了某个被标注的类型是被继承的. 如果一个使用了@Inherited修饰的annotation类型被用于一个class, 则这个annotation将被用于该class的子类.
注意: @Inherited annotation类型是被标注过的class的子类所继承. 类并不从它所实现的接口继承annotation, 方法并不从它所重载的方法继承annotation
当@Inherited annotation类型标注的annotation的Retention是RetentionPolicy.RUNTIME,则反射API增强了这种继承性. 如果我们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时, 反射代码检查将展开工作: 检查class和其父类, 直到发现指定的annotation类型被发现, 或者到达类继承结构的顶层
14.4.6 自定义注解:
使用@interface自定义注解时, 自动继承了java.lang.annotation.Annotation接口, 由编译程序自动完成其他细节. 在定义注解时, 不能继承其他的注解或接口. @interface用来声明一个注解, 其中的每一个方法实际上是声明了一个配置参数. 方法的名称就是参数的名称, 返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum). 可以通过default来声明参数的默认值
定义注解格式:
public @interface 注解名{
String value() default "";
public int id();
}
注解参数的可支持数据类型:
1).所有基本数据类型(int,float,boolean,byte,double,char,long,short)
2).String类型
3).Class类型
4).enum类型
5).Annotation类型
6).以上所有类型的数组
Annotation类型里面的参数该怎么设定:
第一. 只能用public或默认(default)这两个访问权修饰. 例如, [default | public] String value();
第二. 参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和String,Enum,Class,annotations等数据类型, 以及这一些类型的数组. 例如, String value(); 这里的参数成员就为String;
第三. 如果只有一个参数成员, 最好把参数名称设为"value", 后加小括号.
注解元素的默认值:
注解元素必须有确定的值, 要么在定义注解的默认值中指定, 要么在使用注解时指定, 非基本类型的注解元素的值不可为null. 因此, 使用空字符串或0作为默认值是一种常用的做法. 这个约束使得处理器很难表现一个元素的存在或缺失的状态, 因为每个注解的声明中, 所有元素都存在, 并且都具有相应的值, 为了绕开这个约束, 我们只能定义一些特殊的值, 例如空字符串或者负数, 一次表示某个元素不存在
14.4.7 注解处理器
如果没有用来读取注解的方法和工作, 那么注解也就不会比注释更有用处了.
使用注解的过程中, 很重要的一部分就是创建于使用注解处理器. Java SE5扩展了反射机制的API, 以帮助程序员快速的构造自定义注解处理器
注解处理器类库(java.lang.reflect.AnnotatedElement):
Java使用Annotation接口来代表程序元素前面的注解, 该接口是所有Annotation类型的父接口. 除此之外, Java在java.lang.reflect包下新增了AnnotatedElement接口, 该接口代表程序中可以接受注解的程序元素, 该接口主要有如下几个实现类:
1).Class:类定义
2).Constructor:构造器定义
3).Field:类的成员变量定义
4).Method:类的方法定义
5).Package:类的包定义
java.lang.reflect包下主要包含一些实现反射功能的工具类, 实际上, java.lang.reflect 包所有提供的反射API扩充了读取运行时Annotation信息的能力. 当一个Annotation类型被定义为运行时的Annotation后, 该注解才能是运行时可见, 当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取
AnnotatedElement接口是所有程序元素(Class、Method和Constructor)的父接口, 所以程序通过反射获取了某个类的AnnotatedElement对象之后, 程序就可以调用该对象的如下四个个方法来访问Annotation信息:
方法1: <T extends Annotation> T getAnnotation(Class<T> annotationClass): 返回改程序元素上存在的、指定类型的注解, 如果该类型注解不存在, 则返回null
方法2: Annotation[] getAnnotations():返回该程序元素上存在的所有注解.
方法3: boolean is AnnotationPresent(Class<?extends Annotation> annotationClass): 判断该程序元素上是否包含指定类型的注解, 存在则返回true, 否则返回false.
方法4: Annotation[] getDeclaredAnnotations():返回直接存在于此元素上的所有注释. 与此接口中的其他方法不同, 该方法将忽略继承的注释.(如果没有注释直接存在于此元素上, 则返回长度为零的一个数组).该方法的调用者可以随意修改返回的数组; 这不会对其他调用者返回的数组产生任何影响
Java注解的基础知识点(见下面导图)基本都过了一遍, 下一篇我们通过设计一个基于注解的简单的ORM框架, 来综合应用和进一步加深对注解的各个知识点的理解和运用
14.5 获取Class对象的三种方式
JAVA反射机制是在运行状态中, 对于任意一个类(class文件) , 都能够知道这个类的所有属性和方法, 并能够调用它的任意一个方法和属性
动态获取类中的信息, 就是JAVA的反射, 可以理解为对类的解剖
/**
* 方式一
* Object类中的getClass()方法的,想要用这种方式, 必须要明确具体的类, 并创建对象
*/
publicstaticvoidgetClassObject1() {
System.out.println("\n==========方式一==========");
ReflectBean bean1= newReflectBean();
Classclazz1= bean1.getClass();
ReflectBean bean2= newReflectBean();
Classclazz2= bean2.getClass();
System.out.println(clazz1== clazz2);
}
/**
* 方式二
* 任何数据类型都具备一个静态的属性.class来获取其对应的Class对象。相对简单, 但是还是要明确用到类中的静态成员.
*/
publicstaticvoidgetClassObject2() {
System.out.println("\n==========方式二==========");
Classclazz1= ReflectBean.class;
Classclazz2= ReflectBean.class;
System.out.println(clazz1== clazz2);
}
/**
* 方式三(推荐使用)
* 只要通过给定的类的字符串名称就可以获取该类, 更为扩展,可是用Class类中的方法完成
* 该方法就是forName。这种方式只要有名称即可, 更为方便, 扩展性更强
*/
publicstaticvoidgetClassObject3() throwsClassNotFoundException {
System.out.println("\n==========方式三==========");
String className= "basic.demo14.ReflectBean";
Classclazz= Class.forName(className);
System.out.println(clazz);
}
14.6 new和Class对比
/**
* 实例化对象的方式
* @throwsClassNotFoundException
*/
publicstaticvoidNewDemo() throwsException {
System.out.println("\n==========new与Class的对比==========");
/**
* new的方式
* 先根据被new的类的名称找寻该类的字节码文件, 并加载进内存, 并创建该字节码文件对象, 并接着创建该字节文件的对应的Person对象
*/
basic.demo14.ReflectBeanbean1= newbasic.demo14.ReflectBean();
/**
* Class的方式(反射获取对象的方式)
* 找寻该名称类文件, 并加载进内存, 并产生Class对象.
*/
Classclazz= Class.forName("basic.demo14.ReflectBean");
Object obj1= clazz.newInstance();
ReflectBean bean2= (ReflectBean) obj1;
bean2.setNum(1);
bean2.setName("Class的方式");
bean2.show();
System.out.println();
/**
* 如果没有默认构造函数, 则先获取该构造函数
* 获取到了指定的构造函数对象
* 通过该构造器对象的newInstance方法进行对象的初始化.
*/
Constructorconstructor=clazz.getConstructor(int.class, String.class);
Object obj= constructor.newInstance(1,"Constructor构造");
}