Java 基础之反射

反射能够让运行在JVM中的程序检测和修改运行时的行为。在Java中使用反射API可以在运行时获取对象的内部结构信息,就仿佛相应的Java类的内部结构(字段、方法、构造方法等)在镜子前清晰地“反射”出来一样。

我们知道,JVM的类加载机制允许从多种源动态地加载Java类,但是动态类加载有一个重要的缺点,即我们在使用动态加载的类对象时,往往对这个类知之甚少,甚至于一无所知。这种情况下就需要使用一种动态编程技术——反射。

在JavaScript等动态语言中,可以很方便地获取、修改一个对象内部的结构,即便事先对这个对象一无所知。如:

1
2
3
4
5
6
7
8
9
//JavaScript
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key, obj[key]);
}
}

foo = 2;
eval("foo = foo + 2; alert(foo);");

然而,在Java中,如果编译期间不知道一个类的内部信息,要怎么获取呢?反射机制到底要怎么实现呢?这一切都要从Class类说起。简单地说,反射使用Class对象提供的基本元数据,能够从Class对象中获取方法和字段的名称,并获取表示方法和对象的字段。基于此,我们就能在之前类型未知的对象上获取甚至调用它的任意方法。

从Class类说起

Java中存在一个Class类,Class对象是在运行中的Java进程里表示实时类型的方式。Class对象包含了指定类型的元数据,包括这个类中定义的方法、字段、构造方法等。在JVM中,每加载一个类,就会在堆内存中为这个类生成一个java.lang.Class 对象。Class类并没有提供public的构造方法,在类加载完成后,JVM会自动为其生成相应的Class对象。要使用反射,首先就必须获取到Class对象。Class对象有多种获取方式:

getClass()方法

1
2
3
Class<?> cls = obj.getClass();
//还可以获取super class
Class<?> superCls = obj.getClass();

.class方式

1
2
3
Class<?> c1 = int.getClass();
Class<?> c2 = String.getClass();
Class<?> c2 = int[].getClass();

基本类型包装类的TYPE字段

1
2
3
4
5
6
7
8
9
10
//仅针对基本类型,包括void
Class<?> c1 = Boolean.TYPE;
Class<?> c2 = Byte.TYPE;
Class<?> c3 = Character.TYPE;
Class<?> c4 = Short.TYPE;
Class<?> c5 = Integer.TYPE;
Class<?> c6 = Long.TYPE;
Class<?> c7 = Float.TYPE;
Class<?> c8 = Double.TYPE;
Class<?> c9 = Void.TYPE;

Class.forName()

Class类的静态方法,按照路径显式加载相应的类。

1
2
Class<?> c1 = Class.forName("java.lang.String");
Class<?> c2 = Class.forName("java.util.LinkedList$Entry");

在获取了一个类的Class对象后,就可以获取这个类的结构信息了,基本上都是通过调用Class对象的成员方法的方式。详细可参考JDK文档

对于一个类来说,最重要的信息莫过于构造函数、成员变量及成员方法了,这三个重要的信息都可以通过Class对象获取,与之相关的三个类分别为java.lang.reflect.Constructor<T>java.lang.reflect.Methodjava.lang.reflect.Field。与之相对应的一些方法为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Constructor<T>      getConstructor(Class<?>... parameterTypes)
Constructor<?>[] getConstructors()
Constructor<T> getDeclaredConstructor(Class<?>... params)
Constructor<?>[] getDeclaredConstructors()

Field getField(String name)
Field[] getFields()
Field getDeclaredField(String name)
Field[] getDeclaredFields()

Method getMethod(String name, Class<?>... parameterTypes)
Method[] getMethods()
Method getDeclaredMethod(String name, Class<?>... parameterTypes)
Method[] getDeclaredMethods()

其中getDeclaredXXX()getXXX()的区别在于:

Class API List of members? Inherited members? Private members?
getDeclaredField() no no yes
getField() no yes no
getDeclaredFields() yes no yes
getFields() yes yes no

使用反射生成对象

通过反射来生成对象的方法有两种:

  1. 使用Class对象的newInstance()方法来创建该Class对象对应类的实例(使用默认的无参构造方法)。
  2. 先获取指定的Constructor(构造方法)对象,再调用该构造方法的newInstance()方法来生成相应的实例(可使用指定参数的构造方法)。

直接使用Class对象的newInstance()方法来创建实例很常见,在一些Java框架中都可以见到,如根据配置文件生成相应的对象池:

1
2
3
4
5
class Pool {
private static Object getInstance(String className) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
return Class.forName(className).newInstance();
}
}

第二种方式在创建对象的时候则要先获取Constructor对象:

1
2
3
4
5
6
7
try {
Constructor constructor = MyClass.class.getConstructor(int.class); //获取构造方法
MyClass myClassReflect = constructor.newInstance(10); //创建对象
//...
} catch (Exception e) {
e.printStackTrace();
}

鉴于数组的特殊性,反射API中的java.lang.reflect.Array提供了一系列特殊的方法来创建和操作数组:

1
2
3
4
Object array = Array.newInstance(String.class, 10); //等价于 new String[10]
Array.set(array, 0, "Hello"); //等价于array[0] = "Hello"
Array.set(array, 1, "World"); //等价于array[1] = "World"
System.out.println(Array.get(array, 0)); //等价于array[0]

使用反射调用方法、访问成员变量

使用反射来调用方法需要先获取Method对象,随后调用invoke(Object receiver, Object... args)即可,其中第一个参数指名调用的对象。注意调用结束的返回值类型是Object。如果是静态方法,第一个参数可以使null。

1
2
3
4
5
6
7
8
9
10
11
12
13
Object receiver = "a string";
try {
Class<?>[] argTypes = new Class[] {}; //方法参数类型列表,为空则列表为空即可
Object[] args = null; //实际调用时传入的参数,无参则为null

Method meth = receiver.getClass().getMethod("hashCode", argTypes);
Object ret = meth.invoke(receiver, args);
System.out.println(ret);
} catch(IllegalArgumentException | NosuchMethodException | SecurityException e) {
e.printStackTrace();
} catch (IllegalAccessException | InvocationTargetException x) {
x.printStackTrace();
}

对于非public的方法的引用,还要使用setAccessible(true)来覆盖Java的访问控制子系统,然后才能执行非公开方法。

1
2
3
4
5
6
7
try{
Method method = clz.getDeclaredMethod("privateMethod", argTypes);
method.setAccessible(true); //非public方法必须修改后才能调用
method.invoke(receiver, args);
}catch (Exception e) {

}

使用反射访问和修改成员变量则通过 Field 对象进行,基本和 Method 一致,获取和修改分别通过 get() 和 set() 方法,这里不再赘述。

使用反射处理泛型

Java的反射API提供了对泛型的支持。我们通常以为由于泛型擦除机制,泛型类中的类型参数等信息,在运行时刻是不存在的,JVM只能看到原始类型。但实际上,在引入泛型后,类文件的格式做了修订,通过反射依然能获取到泛型信息。下面通过几个简单的例子来说明如何使用反射来获取泛型信息。

泛型变量类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyClass {
public List<String> stringList = ...;
}

Field field = MyClass.class.getField("stringList");
Type genericFieldType = field.getGenericType();

if(genericFieldType instanceof ParameterizedType){
ParameterizedType aType = (ParameterizedType) genericFieldType;
Type[] fieldArgTypes = aType.getActualTypeArguments(); //获取实际的类型参数
for(Type fieldArgType : fieldArgTypes){
Class fieldArgClass = (Class) fieldArgType;
System.out.println("fieldArgClass = " + fieldArgClass);
}
}
//output: typeArgClass = java.lang.String

泛型方法返回类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyClass {
protected List<String> stringList = ...;
public List<String> getStringList(){
return this.stringList;
}
}

Method method = MyClass.class.getMethod("getStringList", null);
Type returnType = method.getGenericReturnType();
if(returnType instanceof ParameterizedType){
ParameterizedType type = (ParameterizedType) returnType;
Type[] typeArguments = type.getActualTypeArguments();
for(Type typeArgument : typeArguments){
Class typeArgClass = (Class) typeArgument;
System.out.println("typeArgClass = " + typeArgClass);
}
}
//output: typeArgClass = java.lang.String

泛型方法参数类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyClass {
protected List<String> stringList = ...;
public void setStringList(List<String> list){
this.stringList = list;
}
}

method = Myclass.class.getMethod("setStringList", List.class);
Type[] genericParameterTypes = method.getGenericParameterTypes();
for(Type genericParameterType : genericParameterTypes){
if(genericParameterType instanceof ParameterizedType){
ParameterizedType aType = (ParameterizedType) genericParameterType;
Type[] parameterArgTypes = aType.getActualTypeArguments();
for(Type parameterArgType : parameterArgTypes){
Class parameterArgClass = (Class) parameterArgType;
System.out.println("parameterArgClass = " + parameterArgClass);
}
}
}
//output: typeArgClass = java.lang.String

Type接口

在Java中,Type是所有类型的超接口,Class类就是Type接口的一个实现。

Type is the common superinterface for all types in the Java programming language. These include raw types, parameterized types, array types, type variables and primitive types.

为了通过反射获取泛型信息,Java分别新增了几个Type接口的子接口,除了上面提到的ParameterizedType外,还有GenericArrayType,TypeVariableWildcardType

Class 通常的Class类型
ParameterizedType 参数化类型,List
TypeVariable 各种类型变量的通用接口,T
GenericArrayType 一种元素类型是参数化类型或者类型变量的数组类型, T[]
WildcardType 一种通配符类型表达式, 如?, ? extends Number, ? super Integer

一种常见的处理各种类型的逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (type instanceof Class) {
Class<?> c = (Class<?>) type;
} else if (type instanceof ParameterizedType) {
ParameterizedType p = (ParameterizedType) type;
//p.getOwnerType()
//p.getRawType()
//p.getActualTypeArguments()
} else if (type instanceof GenericArrayType) {
GenericArrayType g = (GenericArrayType) type;
//g.getGenericComponentType()
} else if (type instanceof WildcardType) {
WildcardType w = (WildcardType) type;
//w.getUpperBounds()
//w.getLowerBounds()
} else {
// This type is unsupported!
}

泛型与异常

Method对象还有一个方法是 getGenericExceptionTypes()。初看这个方法感觉比较奇怪,我们知道由于类型擦除机制,Java中是不允许对异常类使用泛型的。

1
2
GenericException<T> extends Throwable{ /// NOT ALLOWED
}

那么,getGenericExceptionTypes()方法的用意何在呢?在JDK中的说明是这样的:

If an exception type is a type variable or a parameterized type, it is created. Otherwise, it is resolved.

如果异常本就是一个类型变量就可以说通了,举个例子:

1
2
3
4
5
6
7
8
9
10
11
public class Example {
public static <T extends Throwable> void method() throws T {}
public static void main(String[] args) throws Exception {
Method m = Example.class.getMethod("method");
System.out.println(m.getGenericExceptionTypes()[0]);
System.out.println(m.getExceptionTypes()[0]);
}
}
//output:
//T
//class java.lang.Throwable

使用反射获取注解信息

Java同样可以通过反射来获取注解信息,下面给一个简单的例子。找出一个类中被标记为弃用的方法(使用 @Deprecated 标记):

1
2
3
4
5
6
7
8
9
public void findDeprecatedMethods(Class<?> clz) {
for (Method m : clz.getMethods()) {
for (Annotation a : m.getAnnotations()) {
if (a.annotationType() == Deprecated.class) {
System.out.println(m.getName());
}
}
}
}

小结

Java的反射机制十分强大,提供了一种在运行时动态获取对象元数据的手段,并且可以动态地调用对象的方法。即便编译前不知道类的详细信息,只要知道了类的名称,就可以使用反射机制动态地加载类,创建类的实例对象,并动态地调用方法。在很多的Java框架中,反射机制都有着大量的应用。因为类的名称可以通过配置文件等方式获取,反射也可以降低程序的耦合性。当然,使用Java的反射机制也会带来一些问题,如性能问题,安全性问题等,也会导致程序内部逻辑模糊,增加系统的复杂性。

-EOF-

参考
Trail: The Reflection API
Java Reflection - Generics
Java 反射
Java反射机制的学习