第5章 - Java反射
作者:vwFisher
时间:2019-09-04
GitHub代码:https://github.com/vwFisher/JavaBasicGuide
目录
- 1 Class类与Java反射机制
1 Class类与Java反射机制
(basic.reflect.ReflectDemo)
通过Java反射机制,可以在程序中访问已经装载到 JVM 中的 Java 对象的描述,实现访问、监测、修改描述 Java 对象本身信息的功能。所有 Java 类均继承 Object 类,在 Object 类中定义了 getClass() 方法,该方法返回一个类型为 Class 的对象
在通过 getFields() 和 getMethods() 方法一次获取权限为 public 的成员变量和方法时,还包括从超类中继承的变量和方法,而 getDeclaredFields() 和 getDeclaredMethods() 方法只获得本类中定义的所有成员变量和方法
方法定义 | 说明 |
---|---|
Package getPackage() | 获取该类的存放路径 |
String getName() | 获取该类的名称 |
Class<? super T> getSuperclass() | 获取该类继承的类 |
Class[] getInterface() | 获取该类实现的所有接口 |
构造方法相关 | |
Constructor[] getConstructors() | 获取所有权限为public的构造方法 |
Constrctor getConstructor(Class<?>... parameterTypes) | 获取权限为public的指定构造方法 |
Constructor[] getDeclaredConstructors() | 获取所有构造方法,按声明顺序返回 |
Constrctor getDeclaredConstructor(Class<?>... parameterTypes) | 取指定构造方法 |
方法相关 | |
Method[] getMethods() | 获取所有权限为public的方法 |
Method getMethod(String name, Class<?>... parameterTypes) | 获取权限为public的指定方法 |
Method[] getDeclaredMethods() | 获取所有方法,按声明顺序返回 |
Method getDeclaredMethod(String name, Class<?>... parameterTypes) | 获取指定方法 |
成员变量相关 | |
Field[] getFields() | 获取所有权限为public的成员变量 |
Field getField(String name) | 获取权限为public的指定成员变量 |
Field[] getDeclaredFields() | 获取所有成员变量,按声明顺序返回 |
Field getDeclaredField(String name) | 获取指定成员变量 |
内部类相关 | |
Class[] getClasses() | |
Class[] getDeclaredClasses() | 获取所有权限为public的内部类 |
内部类的声明类相关 | 获取所有内部类 |
Class getDeclaringClass() | 如果该类为内部类则返回它的成员类,否则返回null |
1.1 访问构造方法
通过下列方法访问构造方法时,将返回 Constructor 类型的对象或数组,每个 Constructor 对象代表一个构造函数,利用 Constructor 对象可以操纵相应的构造方法
- getConstructors()
- getConstructors(Class<?>... parameterType)
- getDeclaredConstructors()
- getDeclaredConstructor(Class<?>... parameterType)
如果访问指定的构造方法,需要根据该构造方法的入口参数的类型来访问。例如访问一个入口参数类型依次为String和int型的构造方法,可以通过
objectClass.getDeclaredConstructor(String.class, int.class);
objectClass.getDeclaredConstructor(new Class[]{String.class, int.class});
Constructor 类中的常用方法
方法 | 说明 |
---|---|
boolean isVarArgs() | 查看该构造方法是否允许带有可变数量的参数 |
Class<?>[] getParameterTypes() | 按照声明顺序以Class数组的形式获得该构造方法的各个参数的类型 |
Class<?>[] getExceptionTypes() | 以Class数组的形式获得该构造方法的各个参数的类型 |
T newInstance(Object... initargs) | 通过该构造方法利用指定参数创建一个该类的对象,如果未设置参数则采用默认无参数的构造方法 |
void setAccessible(boolean flag) | 如果该构造方法权限为private,默认不允许通过反射利用 newInstance(Object...initargs) 方法创建对象。如果先执行该方法,并将入口参数设为true,则允许创建 |
int getModifiers() | 获得可以解析出该构造方法所采用修饰符的正数 |
通过 java.lang.reflect.Modifier 类可以解析出 getModifiers() 方法的返回值所表示的修饰符信息,在该类中提供了用来解析的静态方法,既可以查看是否被指定的修饰符修饰,还可以以字符串的形式获得所有修饰符。
Modifier类中的常用静态方法
静态方法 | 说明 |
---|---|
boolean isPublic(int mod) | 查看是否被public修饰符修饰 |
boolean isProtected(int mod) | 查看是否被protected修饰符修饰 |
boolean isPrivate(int mod) | 查看是否被private修饰符修饰 |
boolean isStatic(int mod) | 查看是否被static修饰符修饰 |
boolean isFinal(int mod) | 查看是否被final修饰符修饰 |
String toString(int mod) | 以字符串的形式返回所有修饰符 |
1.2 访问成员变量
通过下列方法访问成员变量时,将返回Field类型的对象或数组,每个Field对象代表一个成员变量,利用Field对象可以操纵相应的成员变量
- getFields()
- getField(String name)
- getDeclaredFields()
- getDeclaredField(String name)
如果是访问指定的成员变量,可以通过该成员变量的名称来访问。例如访问一个名称为birthday的成员变量:
object.getDeclaredField("birthday")
Field类中的常用方法
方法 | 说明 |
---|---|
String getName() | 获取该成员变量的名称 |
Class<?> getType() | 获取表示该成员变量类型的Class对象 |
Object get(Object obj) | 获取指定对象obj中该成员变量的值, 返回值为Object型 注意:有对应其他类型的get方法 |
void set(Object obj, Object value) | 将指定对象obj中该成员变量的值设置为value 注意:有对应其他类型的set方法 |
boolean isAccessible() | 是否可以访问该属性 |
void setAccessible(boolean flag) | 如果该构造方法的权限为private, 默认不允许通过反射来获取值 |
int getModifiers() | 获得可以解析出该成员变量所采用修饰符的正数 |
1.3 访问方法
通过下列方法访问方法时,将返回Method类型的对象或数组,每个Method对象代表一个方法,利用Method对象可以操作相应的方法
- getMethods()
- getMethod(String name, Class<?>... parameterTypes)
- getDeclaredMethods()
- getDeclaredMethod(String name, Class<?>... parameterTypes)
如果是访问指定的方法,需要根据该方法的名称和入口参数的类型来访问,例如访问一个名称为 print,入口参数类型依次为 String和int型的方法,可以通过下面两种方式实现
objectClass.getDeclaredMethod("print", String.class, int.class)
objectClass.getDeclaredMethod("print", new Class[]{String.class, int.class})
方法 | 说明 |
---|---|
String getName() | 获得该方法的名称 |
Class<?>[] getParameterTypes() | 按照声明顺序以Class数组的形式获得该方法的各个参数的类型 |
Class<?> getReturnType() | 以Class对象的形式获得该方法的返回值的类型 |
Class<?>[] getExceptionTypes() | 以Class数组的形式获得该方法可能抛出的异常类型 |
Object invoke(Object obj, Object... args) | 利用指定参数args执行指定对象obj中的该方法 |
boolean isVarArgs() | 查看构造方法是否允许带有可变数量的参数 |
int getModifiers() | 获得可以解析出该方法所采用修饰符的整数 |
1.4 注解(Annotation)的使用
(示例: basic.demo14.annotation)
14.4.1 什么是注解(Annotation)
注解,可以看作是对一个 类/方法 的一个扩展的模版,每个 类/方法 按照注解类中的规则,来为 类/方法 注解不同的参数,在用到的地方可以得到不同的 类/方法 中注解的各种参数与值。
可以把注解理解为代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。
JDK5.0 以后引入,java增加了对元数据(描述数据属性的信息)的支持。其实说白就是代码里的特殊标志,这些标志可以在编译,类加载,运行时被读取,并执行相应的处理,以便于其他工具补充信息或者进行部署。
1.4.2 metadata(元数据)
元数据从metadata一词译来,就是 关于数据的数据 的意思。
元数据的功能作用有很多,比如:你可能用过Javadoc的注释自动生成文档。这就是元数据功能的一种。总的来说,元数据可以用来创建文档,跟踪代码的依赖性,执行编译时格式检查,代替已有的配置文件。如果要对于元数据的作用进行分类,目前还没有明确的定义,不过我们可以根据它所起的作用,大致可分为三类:
- 编写文档:通过代码里标识的元数据生成文档
- 代码分析:通过代码里标识的元数据对代码进行分析
- 编译检查:通过代码里标识的元数据让编译器能实现基本的编译检查
在Java中元数据以标签的形式存在于Java代码中,元数据标签的存在并不影响程序代码的编译和执行,它只是被用来生成其它的文件或针在运行时知道被运行代码的描述信息。综上所述:
- 元数据以标签的形式存在于Java代码中
- 元数据描述的信息是类型安全的,即元数据内部的字段都是有明确类型的
- 元数据需要编译器之外的工具额外的处理用来生成其它的程序部件
- 元数据可以只存在于Java源代码级别,也可以存在于编译之后的Class文件内部
1.4.3 注解的分类:
根据注解参数的个数,我们可以将注解分为三类:
- 标记注解:一个没有成员定义的Annotation类型被称为标记注解。这种Annotation类型仅使用自身的存在与否来为我们提供信息。比如后面的系统注解@Override
- 单值注解
- 完整注解
根据注解使用方法和用途,我们可以将Annotation分为三类:
- JDK内置系统注解
- 元注解
- 自定义注解
1.4.4 系统内置标准注解
注解的语法比较简单,除了 @
符号的使用外,他基本与 Java 固有的语法一致,JavaSE 中内置三个标准注解,定义在 java.lang 中
- @Override:用于修饰此方法覆盖了父类的方法
- @Deprecated:用于修饰已经过时的方法
- @SuppressWarnnings: 用于通知java编译器禁止特定的编译警告
下面我们依次看看三个内置标准注解的作用和使用场景
1.4.4.1 @Override(限定重写父类方法)
@Override是一个标记注解类型,它被用作标注方法。
它说明了被标注的方法重载了父类的方法,起到了断言的作用。如果我们使用了这种 Annotation 在一个没有覆盖父类方法的方法时,java编译器将以一个编译错误来警示。这个annotaton常常在我们试图覆盖父类方法而确又写错了方法名时发挥威力
1.4.4.2 @Deprecated,标记已过时:
同样Deprecated也是一个标记注解。当一个类型或者类型成员使用 @Deprecated 修饰的话,编译器将不鼓励使用这个被标注的程序元素。而且这种修饰具有一定的延续性
如果我们在代码中通过继承或者覆盖的方式使用了这个过时的类型或者成员,虽然继承或者覆盖后的类型或者成员并不是被声明为 @Deprecated,但编译器仍然要报警
注意,@Deprecated 这个 annotation 类型和 javadoc 中的 @deprecated 这个 tag 是有区别的
前者是java编译器识别的,而后者是被 javadoc 工具所识别用来生成文档(包含程序成员为什么已经过时、它应当如何被禁止或者替代的描述)
在 java5.0,java编译器仍然象其从前版本那样寻找 @deprecated 这个javadoc tag,并使用它们产生警告信息。但是这种状况将在后续版本中改变,新版本应该使用 @Deprecated 来修饰过时的方法而不是 @deprecated javadoc tag
1.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 注解的常见参数值的简单说明:
- deprecation:使用了不赞成使用的类或方法时的警告
- unchecked:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics)来指定集合保存的类型;
- fallthrough:当 Switch 程序块直接通往下一种情况而没有 Break 时的警告;
- path:在类路径、源文件路径等中有不存在的路径时的警告;
- serial:当在可序列化的类上缺少 serialVersionUID 定义时的警告;
- finally:任何 finally 子句不能正常完成时的警告;
- all:关于以上所有情况的警告
1.4.5 元注解
元注解的作用就是负责注解其他注解。Java5.0定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。Java5.0定义的元注解:
- @Target
- @Retention
- @Documented
- @Inherited
这些类型和它们所支持的类在java.lang.annotation包中可以找到。下面我们看一下每个元注解的作用和相应分参数的使用说明
1.4.5.1 @Target
@Target说明了Annotation所修饰的对象范围: Annotation可被用于packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数). 在Annotation类型的声明中使用了target可更加明晰其修饰的目标
作用: 用于描述注解的使用范围(即: 被描述的注解可以用在什么地方)
取值(ElementType)有:
- CONSTRUCTOR: 用于描述构造器
- FIELD: 用于描述域
- LOCAL_VARIABLE: 用于描述局部变量
- METHOD: 用于描述方法
- PACKAGE: 用于描述包
- PARAMETER: 用于描述参数
- TYPE: 用于描述类、接口(包括注解类型) 或enum声明
1.4.5.2 @Retention:
@Retention定义了该Annotation被保留的时间长短: 某些Annotation仅出现在源代码中, 而被编译器丢弃; 而另一些却被编译在class文件中; 编译在class文件中的Annotation可能会被虚拟机忽略, 而另一些在class被装载时将被读取(请注意并不影响class的执行, 因为Annotation与class在使用上是被分离的). 使用这个meta-Annotation可以对 Annotation的"生命周期"限制.
作用: 表示需要在什么级别保存该注释信息, 用于描述注解的生命周期(即: 被描述的注解在什么范围内有效)
取值(RetentionPoicy)有:
- SOURCE: 在源文件中有效(即源文件保留)
- CLASS: 在class文件中有效(即class保留)
- RUNTIME: 在运行时有效(即运行时保留,注解处理器可以通过反射, 获取到该注解的属性值, 从而去做一些运行时的逻辑处理)
Retention meta-annotation类型有唯一的value作为成员, 它的取值来自java.lang.annotation.RetentionPolicy的枚举类型值.
1.4.5.3 @Documented:
@Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API, 因此可以被例如javadoc此类的工具文档化. Documented是一个标记注解, 没有成员
1.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类型被发现, 或者到达类继承结构的顶层
1.4.6 自定义注解:
使用@interface自定义注解时, 自动继承了java.lang.annotation.Annotation接口, 由编译程序自动完成其他细节. 在定义注解时, 不能继承其他的注解或接口. @interface用来声明一个注解, 其中的每一个方法实际上是声明了一个配置参数. 方法的名称就是参数的名称, 返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum). 可以通过default来声明参数的默认值
定义注解格式:
public @interface 注解名 {
String value() default "";
public int id();
}
注解参数的可支持数据类型:
- 所有基本数据类型(int,float,boolean,byte,double,char,long,short)
- String类型
- Class类型
- enum类型
- Annotation类型
- 以上所有类型的数组
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作为默认值是一种常用的做法. 这个约束使得处理器很难表现一个元素的存在或缺失的状态, 因为每个注解的声明中, 所有元素都存在, 并且都具有相应的值, 为了绕开这个约束, 我们只能定义一些特殊的值, 例如空字符串或者负数, 一次表示某个元素不存在
1.4.7 注解处理器
使用注解的过程中, 很重要的一部分就是创建于使用注解处理器. Java SE5扩展了反射机制的API, 以帮助程序员快速的构造自定义注解处理器
注解处理器类库(java.lang.reflect.AnnotatedElement)
Java使用Annotation接口来代表程序元素前面的注解, 该接口是所有Annotation类型的父接口. 除此之外, Java在java.lang.reflect包下新增了AnnotatedElement接口, 该接口代表程序中可以接受注解的程序元素, 该接口主要有如下几个实现类:
- Class:类定义
- Constructor:构造器定义
- Field:类的成员变量定义
- Method:类的方法定义
- 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框架, 来综合应用和进一步加深对注解的各个知识点的理解和运用
1.5 获取Class对象的三种方式
JAVA反射机制是在运行状态中, 对于任意一个类(class文件) , 都能够知道这个类的所有属性和方法, 并能够调用它的任意一个方法和属性
动态获取类中的信息, 就是JAVA的反射, 可以理解为对类的解剖
/**
* 方式一
* Object类中的getClass()方法的,想要用这种方式, 必须要明确具体的类, 并创建对象
*/
public static void getClassObject1() {
System.out.println("\n==========方式一==========");
ReflectBean bean1 = new ReflectBean();
Class clazz1 = bean1.getClass();
ReflectBean bean2 = new ReflectBean();
Class clazz2 = bean2.getClass();
System.out.println(clazz1 == clazz2);
}
/**
* 方式二
* 任何数据类型都具备一个静态的属性.class来获取其对应的Class对象。相对简单, 但是还是要明确用到类中的静态成员.
*/
public static void getClassObject2() {
System.out.println("\n==========方式二==========");
Class clazz1 = ReflectBean.class;
Class clazz2 = ReflectBean.class;
System.out.println(clazz1 == clazz2);
}
/**
* 方式三(推荐使用)
* 只要通过给定的类的 字符串名称就可以获取该类, 更为扩展,可是用Class类中的方法完成
* 该方法就是forName。这种方式只要有名称即可, 更为方便, 扩展性更强
*/
public static void getClassObject3() throws ClassNotFoundException {
System.out.println("\n==========方式三==========");
String className = "basic.demo14.ReflectBean";
Class clazz = Class.forName(className);
System.out.println(clazz);
}
1.6 new和Class对比
/**
* 实例化对象的方式
* @throws ClassNotFoundException
*/
public static void NewDemo() throws Exception {
System.out.println("\n==========new与Class的对比==========");
/**
* new的方式
* 先根据被new的类的名称找寻该类的字节码文件, 并加载进内存, 并创建该字节码文件对象, 并接着创建该字节文件的对应的Person对象
*/
basic.demo14.ReflectBean bean1 = new basic.demo14.ReflectBean();
/**
* Class的方式(反射获取对象的方式)
* 找寻该名称类文件, 并加载进内存, 并产生Class对象.
*/
Class clazz = 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方法进行对象的初始化.
*/
Constructor constructor =clazz.getConstructor(int.class, String.class);
Object obj = constructor.newInstance(1, "Constructor构造");
}