反射
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和privateMethod[] getMethods()返回本类及其父类的所有 public方法,注意不包含protected和privateMethod 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 选项。



