反射
1. 释义
Java 语言的反射机制指的是一种动态获取类与对象的信息以及操作类与对象的属性和方法的功能。即,在程序运行过程中,对于任意一个类或对象,都能够知道这个类的所有属性和方法,并且对它们进行操作或调用。
反射会把 Java 类中的各种成分映射成一个个的 Java 对象。例如,一个类的包、构造方法、字段、方法等等信息,利用反射技术就可以对这个类进行解剖,把这些个组成部分映射成一个个对象。
要想解剖一个类,必须先获取到该类的字节码文件对象,即对应的Class
类型的对象。
反射相关的接口和类都存在于java.lang.reflect
包下,常用到以下类型:
接口/类 | 含义 |
---|---|
Class | 类 |
Method | 方法 |
Field | 字段 |
Constructor | 构造器 |
Array | 数组 |
Type
也是java.lang.reflect
包下的一个接口,其代表所有类型的公共高级接口,Class
是Type
接口的实现类。Type
包括原始类型、参数化类型、数组类型、类型变量、基本类型等。
2. Class类
程序需要在运行时发现对象和类的真实信息时,可以有两种做法:
- 若在编译时和运行时都完全知道类型的具体信息,此时可以先使用
instanceof
运算符进行判断,再利用强制类型转换将其转换成其运行时类型的变量即可; - 当编译时根本无法预知该对象和类可能属于哪些类,程序只能依靠运行时信息来发现该对象和类的真实信息时,必须使用反射。
2.1 Class类的继承图

2.2 获取一个类/接口的Class对象
在 Java 程序中获得 Class
对象通常有三种方式:
使用
Class
类的static forName(String className)
静态方法:Class<?> aClass = Class.forName("java.util.Random"); System.out.println(aClass); // class java.util.Random
调用任意类型(甚至
void
关键字)的class
属性来获取该类对应的Class
对象;Class<Random> cl1 = Random.class; Class<Integer> cl2 = int.class; Class<Double[]> cl3 = Double[].class; Class<Void> cl4 = void.class;
调用某个对象的
getClass()
方法:Test tmp = new Test(); Class<Test> tmpClass = tmp.getClass();
请注意,一个 Class
对象实际上表示的是一个类型,其可能是类,也可能不是类。例如,int
不是类,但 int.class
是一个 Class
类型的对象。
JVM为每个类型管理一个唯一的Class
对象,因此可以直接利用 ==
运算符实现两个类对象的比较。
2.3 Class对象的方法
Class
类提供了大量的实例方法来获取该 Class
对象所对应类的详细信息,其大致包含如下方法,其中每个方法都可能包含多个重载的版本,具体应该查阅 API 文档来进行掌握。
对于只能在源代码上保留的注解,使用运行时获得的
Class
对象当然无法访问到该注解对象。
获取类成分信息
构造器
方法 作用 Constructor<T> getConstructor(Class<?>… parameterTypes)
返回此 Class
对象对应类的、带指定形参列表的public
构造器Constructor<?>[] getConstructors()
返回此 Class
对象对应类的所有public
构造器Constructor<T> getDeclaredConstructor(Class<?>… parameterTypes)
返回此 Class
对象对应类的、带指定形参列表的构造器,与构造器的访问权限无关Constructor<?>[] getDeclaredConstructors()
返回此 Class
对象对应类的所有构造器,与构造器的访问权限无关方法
方法 作用 Method getMethod(String name, Class<?>… parameterTypes)
返回本类或其父类中声明的带指定形参列表的 public
方法,注意不包含protected
和private
Method[] getMethods()
返回本类及其父类的所有 public
方法,注意不包含protected
和private
Method getDeclaredMethod(String name, Class<?>… parameterTypes)
返回本类中声明的带指定形参列表的方法,注意不包括父类、不在乎访问权限 Method[] getDeclaredMethods()
返回本类中声明的全部方法,注意不包括父类、不在乎访问权限 字段
方法 作用 Field getField(String name)
Field[] getFields()
Field getDeclaredField(String name)
Field[] getDeclaredFields()
注解
方法 作用 <A extends Annotation> A getAnnotation(Class<A> annotationClass)
<A extends Annotation> A getDeclaredAnnotation(Class<A> annotationClass)
Annotation[] getAnnotations()
Annotation[] getDeclaredAnnotations()
<A extends Annotation> A[] getAnnotationsByType(Class<A> annotationClass)
<A extends Annotation> A[] getDeclaredAnnotationsByType(Class<A> annotationClass)
内部类/外部类
方法 作用 Class<?>[] getDeclaredClasses()
返回类的所有内部类 Class<?> getDeclaringClass()
返回类所在的外部类 所继承的父类/所实现的接口
方法 作用 Class<?>[] getInterfaces()
返回类所实现的全部接口 Class<? super T> getSuperClass()
返回类的父类的 Class
对象该类的修饰符、所在包、类包等基本信息
方法 作用 int getModifiers()
Package getPackage()
String getName()
String getSimpleName()
鉴于历史原因,
getName
方法在应用于数组类型的时候会返回有些奇怪的名字:Double[].class.getName(); // 返回"[Ljava.lang.Double;" int[].class.getName(); // 返回"[I"
判断该类是否为接口、枚举、注解类型、数组等
方法 作用 boolean isAnnotation()
判断是否是注解类型 boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
判断此 Class
对象是否使用了注解进行修饰boolean isAnonymousClass()
判断是否是一个匿名类 boolean isArray()
判断是否是一个数组类 boolean isEnum()
判断是否是一个枚举类 boolean isInterface()
判断是否是一个接口 boolean isInstance(Object obj)
判断 obj
是否是此Class
对象的实例
其他方法
static Class<?> forName(String className)
T newInstance()
:通过反射来创建新的实例,但只能调用该类的public
的无参构造方法isAssignableFrom()
:判断一个向上转型是否可以实现URL getResource(String name)
:查找与类位于同一位置的资源文件,返回一个用来加载资源的URL
。如果找不到资源,则返回null
。InputStream getResourceAsStream(String name)
:同上,返回输入流。
3. 类的成分:Constructor, Method, Field
Class
对象可以获得类里的构造器(Constructor
)、方法(Method
)、字段(Field
),程序可以通过Constructor
对象来调用对应的构造器创建实例,通过Method
对象来执行对应的方法,通过Field
对象直接访问并修改对象的字段值。这样一来,Java程序可以通过反射获取的这些对象得到类的相应成分的信息,同时可通过反射来生成并操作对象。
通过反射创建对象时性能要稍低一些,实际上,只有当程序需要动态创建某个类的对象时才会考虑反射,通常在开发通用性比较广的框架、基础平台时可能会大量使用反射。
Constructor, Method, Field
的类图:
Constructor | Method | Field |
---|---|---|
![]() | ![]() | ![]() |
由上可见,Constructor, Method, Field
都实现了java.lang.reflect.Member
接口,同时有一个共同的父类AccessibleObject
。在AccessibleObject
基类中,有一个setAccessible(boolean flag)
方法,可用于将原本被private
修饰的私有成员的权限放开(protected
的可以直接访问,不需要放开),使得子类Constructor, Method, Field
等可以获得所有相应类成员的访问权限。
此外,Constructor
和Method
非常类似,不同之处仅在于Constructor
是一个构造方法,并且调用结果总是返回类实例。
由上可知,反射可以突破private
限制,那类的封装还有什么意义?
- 正常情况下,我们总是通过
p.name
来访问Person
的name
字段,编译器会根据public
、protected
和private
决定是否允许访问字段,这样就达到了数据封装的目的。 - 而反射是一种非常规的用法,使用反射,首先代码非常繁琐,其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。
- 此外,
setAccessible(true)
可能会失败。如果JVM运行期存在SecurityManager
,那么它会根据规则进行检查,有可能阻止setAccessible(true)
。例如,某个SecurityManager
可能不允许对java
和javax
开头的package
的类调用setAccessible(true)
,这样可以保证JVM核心库的安全。
3.1 Constructor
为了调用任意的构造方法,Java 的反射 API 提供了Constructor
对象,它包含一个构造方法的所有信息,可以创建一个实例。
T newInstance(Object ... initargs)
:创建实例
注意 Constructor
总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。
3.2 Method
Method
对象很像 C++ 中的函数指针。
一个Method
对象包含一个方法的所有信息:
getName()
:返回方法名称getReturnType()
:返回方法返回值类型,也是一个Class
实例,例如:String.class
;getParameterTypes()
:返回方法的参数类型,是一个Class
数组,例如:{String.class, int.class}
;getModifiers()
:返回方法的修饰符,它是一个int
,不同的比特位表示不同的含义。
通过 Method
对象程序也可以来调用它对应的方法:
Object invoke(Object obj, Object… args)
:其中obj
是执行该方法的主调,后面的args
是执行该方法时传入该方法的实参。如果方法类型是static
的,obj
参数传入null
。
通过反射调用方法时,仍然遵循多态原则。
3.3 Field
利用反射机制可以查看在编译时还不知道的对象字段。将来的库有可能使用可变句柄(variable handles)而不是反射来读写字段。
一个Field
对象包含了一个字段的所有信息:
getName()
:返回字段名称,例如,"name"
;getType()
:返回字段类型,也是一个Class
实例,例如,String.class
;getModifiers()
:返回字段的修饰符,它是一个int
,不同的 bit 表示不同的含义。
Field
提供了如下两组方法来读取或设置字段值:
方法 | 作用 |
---|---|
getXxx(Object obj) | 获取 obj 对象的该字段(基本类型)的值,此处Xxx 对应8种基本类型 |
get(Object obj) | 获取 obj 对象的该字段(引用类型)的值。若字段类型是static 的,obj 参数传入null |
setXxx(Object obj, Xxx val) | 将 obj 对象的该字段(基本类型)设置成 val 值,此处Xxx 对应8种基本类型 |
set(Object obj, Xxx val) | 将 obj 对象的该字段(引用类型)设置成 val 值。若字段类型是static 的,obj 参数传入null |
对于返回字段的类型,可能涉及泛型:
方法 | 作用 |
---|---|
Class<?> getType() | 获取字段的类型 |
Type getGenericType() | 获取字段的泛型类型 |
3.4 Array
java.lang.reflect
包下的 Array
类可以代表所有的数组,程序可通过使用 Array
来动态地创建数组、操作数组元素等。Array
提供的方法主要有:
方法 | 作用 |
---|---|
static Object newInstance(Class<?> componentType, int… length) | 创建一个具有指定元素类型、指定维度的新数组 |
static xxx getXxx(Object array, int index) | 返回array 数组中第index 个元素,此处Xxx 对应8种基本类型 |
static xxx get(Object array, int index) | 同上,对应引用类型 |
static void setXxx(Object array, int index, xxx val) | 将array 数组中第index 个元素的值设为val |
static void set(Object array, int index, xxx val) | 同上,对应引用类型 |
3.5 泛型
JDK5添加了泛型以后,Java 允许使用泛型来限制 Class
类,例如 String.class
的类型实际上是 Class<String>
。如果对 Class
对应的类暂时未知,则使用 Class<?>
。
通过在反射中使用泛型,可以避免使用反射生成的对象需要强制类型转换。
为了获得指定字段的泛型类型,应先使用 Type gType = field.getGenericType();
来获取该字段的泛型类型,然后将 Type
对象强转为 ParameterizedType
对象,ParameterizedType
代表参数化的类型,也即增加了泛型限制的类型。ParameterizedType
类提供了如下两个方法:
getRawType()
:返回没有泛型信息的原始类型。getActualTypeArguments()
:返回泛型参数的类型。
public class GenericTest {
private Map<String, Integer> score;
@Test
public void testGeneric() throws Exception {
Class<GenericTest> clazz = GenericTest.class;
Field field = clazz.getDeclaredField("score");
Class<?> scoreType = field.getType();
System.out.println(scoreType); // interface java.util.Map
Type gScoreType = field.getGenericType();
if (gScoreType instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) gScoreType;
Type rawType = pType.getRawType(); // interface java.util.Map
System.out.println(rawType);
Type[] actualTypeArguments = pType.getActualTypeArguments();
System.out.println(Arrays.toString(actualTypeArguments)); // [class java.lang.String, class java.lang.Integer]
} else {
System.out.println("获取泛型类型出错!");
}
}
}
4. 其他类
Executable 抽象基类
java.lang.reflect
包下有一个 Executable
抽象基类,该对象代表可执行的类成员,该类派生了 Constructor
, Method
两个子类。
Executable 基类的方法 | |
---|---|
isVarArgs() | 判断该方法/构造器是否包含数量可变的形参 |
getModifiers() | 获取该方法/构造器的修饰符 |
int getParameterCount() | 获取该构造器/方法的形参个数 |
Parameter[] getParameters() | 获取该构造器/方法的所有形参 |
Parameter 类
Parameter
类:每个 Parameter
对象代表方法/构造器的一个形参,其方法有:
方法 | 作用 |
---|---|
getModifiers() | 获取修饰该形参的修饰符 |
String getName() | 获取形参名 |
Type getParameterizedType() | 获取带泛型的形参类型 |
Class<?> getType() | 获取形参类型 |
boolean isNamePresent() | 返回该类的 Class 文件中是否包含了方法的形参名信息 |
boolean isVarArgs() | 判断该参数是否为个数可变的形参 |
需要指出的是,使用 javac
命令编译 Java 源文件时,默认生成的 class
文件并不包含方法的形参名信息,因此调用 isNamePresent()
方法将返回 false
,调用 getName()
方法也不能得到该参数的形参名。若希望 javac
命令编译 Java 源文件时可以保留形参信息,需要为该命令指定 -parameters
选项。