12.0 第21章 反射 21.1 Class类
第21章 反射
从本章开始,我们来探讨Java中的一些动态特性,包括反射、注解、动态代理、类加载器等。利用这些特性,可以优雅地实现一些灵活通用的功能,它们经常用于各种框架、库和系统程序中,比如:
1)14.5节介绍的Jackson,利用反射和注解实现了通用的序列化机制。
2)有多种库(如Spring MVC、Jersey)用于处理Web请求,利用反射和注解,能方便地将用户的请求参数和内容转换为Java对象,将Java对象转变为响应内容。
3)有多种库(如Spring、Guice)利用这些特性实现了对象管理容器,方便程序员管理对象的生命周期以及其中复杂的依赖关系。
4)应用服务器(如Tomcat)利用类加载器实现不同应用之间的隔离,JSP技术利用类加载器实现修改代码不用重启就能生效的特性。
5)面向方面的编程AOP(Aspect Oriented Programming)将编程中通用的关注点(如日志记录、安全检查等)与业务的主体逻辑相分离,减少冗余代码,提高程序的可维护性, AOP需要依赖上面的这些特性来实现。
本章主要介绍反射机制,后续章节介绍其他内容。
在一般操作数据的时候,我们都是知道并且依赖于数据类型的,比如:
1)根据类型使用new创建对象。
2)根据类型定义变量,类型可能是基本类型、类、接口或数组。
3)将特定类型的对象传递给方法。
4)根据类型访问对象的属性,调用对象的方法。
编译器也是根据类型进行代码的检查编译的。
反射不一样,它是在运行时,而非编译时,动态获取类型的信息,比如接口信息、成员信息、方法信息、构造方法信息等,根据这些动态获取到的信息创建对象、访问/修改成员、调用方法等。这么说比较抽象,下面我们会具体说明。反射的入口是名称为Class的类,我们先介绍Class类,随后举例说明反射的应用,接着讨论反射与泛型,最后进行总结。
21.1 Class类
在介绍类和继承的实现原理时,我们提到,每个已加载的类在内存都有一份类信息,每个对象都有指向它所属类信息的引用。Java中,类信息对应的类就是java.lang.Class。注意不是小写的class, class是定义类的关键字。所有类的根父类Object有一个方法,可以获取对象的Class对象:
1 | public final native Class<? > getClass() |
Class是一个泛型类,有一个类型参数,getClass()并不知道具体的类型,所以返回Class<?>
。
获取Class对象不一定需要实例对象,如果在写程序时就知道类名,可以使用<类名>.class获取Class对象,比如:
1 | Class<Date> cls = Date.class; |
接口也有Class对象,且这种方式对于接口也是适用的,比如:
1 | Class<Comparable> cls = Comparable.class; |
基本类型没有getClass方法,但也都有对应的Class对象,类型参数为对应的包装类型,比如:
1 | Class<Integer> intCls = int.class; |
void作为特殊的返回类型,也有对应的Class:
1 | Class<Void> voidCls = void.class; |
对于数组,每种类型都有对应数组类型的Class对象,每个维度都有一个,即一维数组有一个,二维数组有一个不同的类型。比如:
1 | String[] strArr = new String[10]; |
枚举类型也有对应的Class,比如:
1 | enum Size { |
Class有一个静态方法forName,可以根据类名直接加载Class,获取Class对象,比如:
1 | try { |
注意forName可能抛出异常ClassNotFoundException。
有了Class对象后,我们就可以了解到关于类型的很多信息,并基于这些信息采取一些行动。Class的方法很多,大部分比较简单直接,容易理解,下面,我们分为若干组,包括名称信息、字段信息、方法信息、创建对象和构造方法、类型信息等,进行简要介绍。
1.名称信息
Class有如下方法,可以获取与名称有关的信息:
1 | public String getName() |
getSimpleName返回的名称不带包信息,getName返回的是Java内部使用的真正的名称,getCanonicalName返回的名称更为友好,getPackage返回的是包信息,它们的不同如表格21-1所示。
需要说明的是数组类型的getName返回值,它使用前缀[表示数组,有几个[表示是几维数组;数组的类型用一个字符表示,I表示int, L表示类或接口,其他类型与字符的对应关系为:boolean(Z)、byte(B)、char(C)、double(D)、float(F)、long(J)、short(S)。对于引用类型的数组,注意最后有一个分号;。
2.字段信息
类中定义的静态和实例变量都被称为字段,用类Field表示,位于包java.lang.reflect下,后文涉及的反射相关的类都位于该包下。Class有4个获取字段信息的方法:
1 | //返回所有的public字段,包括其父类的,如果没有字段,返回空数组 |
Field也有很多方法,可以获取字段的信息,也可以通过Field访问和操作指定对象中该字段的值,基本方法有:
1 | //获取字段的名称 |
在get/set方法中,对于静态变量,obj被忽略,可以为null,如果字段值为基本类型, get/set会自动在基本类型与对应的包装类型间进行转换;对于private字段,直接调用get/set会抛出非法访问异常IllegalAccessException,应该先调用setAccessible(true)以关闭Java的检查机制。看段简单的示例代码:
1 | List<String> obj = Arrays.asList(new String[]{"老马", "编程"}); |
代码比较简单,就不赘述了。除了以上方法,Field还有很多其他方法,比如:
1 | //返回字段的修饰符 |
getModifiers返回的是一个int,可以通过Modifier类的静态方法进行解读。比如,假定Student类有如下字段:
1 | public static final int MAX_NAME_LEN = 255; |
可以这样查看该字段的修饰符:
1 | Field f = Student.class.getField("MAX_NAME_LEN"); |
输出为:
1 | public static final |
3.方法信息
类中定义的静态和实例方法都被称为方法,用类Method表示。Class有如下相关方法:
1 | //返回所有的public方法,包括其父类的,如果没有方法,返回空数组 |
通过Method可以获取方法的信息,也可以通过Method调用对象的方法,基本方法有:
1 | //获取方法的名称 |
对invoke方法,如果Method为静态方法,obj被忽略,可以为null, args可以为null,也可以为一个空的数组,方法调用的返回值被包装为Object返回,如果实际方法调用抛出异常,异常被包装为InvocationTargetException重新抛出,可以通过getCause方法得到原异常。看段简单的示例:
1 | Class<? > cls = Integer.class; |
Method还有很多方法,可以获取其修饰符、参数、返回值、注解等信息,具体就不列举了。
4.创建对象和构造方法
Class有一个方法,可以用它来创建对象:
1 | public T newInstance() throws InstantiationException, IllegalAccessException |
它会调用类的默认构造方法(即无参public构造方法),如果类没有该构造方法,会抛出异常InstantiationException。看个简单示例:
1 | Map<String, Integer> map = HashMap.class.newInstance(); |
newInstance只能使用默认构造方法。Class还有一些方法,可以获取所有的构造方法:
1 | //获取所有的public构造方法,返回值可能为长度为0的空数组 |
类Constructor表示构造方法,通过它可以创建对象,方法为:
1 | public T newInstance(Object ... initargs) throws InstantiationException, |
看个例子:
1 | Constructor<StringBuilder> contructor= StringBuilder.class |
除了创建对象,Constructor还有很多方法,可以获取关于构造方法的很多信息,包括参数、修饰符、注解等,具体就不列举了。
5.类型检查和转换
我们之前介绍过instanceof关键字,它可以用来判断变量指向的实际对象类型。instanceof后面的类型是在代码中确定的,如果要检查的类型是动态的,可以使用Class类的如下方法:
1 | public native boolean isInstance(Object obj) |
也就是说,如下代码:
1 | if(list instanceof ArrayList){ |
和下面代码的输出是相同的:
1 | Class cls = Class.forName("java.util.ArrayList"); |
除了判断类型,在程序中也往往需要进行强制类型转换,比如:
1 | List list = .. |
在这段代码中,强制转换到的类型是在写代码时就知道的。如果是动态的,可以使用Class的如下方法:
1 | public T cast(Object obj) |
比如:
1 | public static <T> T toType(Object obj, Class<T> cls){ |
isInstance/cast描述的都是对象和类之间的关系,Class还有一个方法,可以判断Class之间的关系:
1 | //检查参数类型cls能否赋给当前Class类型的变量 |
比如,如下表达式的结果都为true:
1 | Object.class.isAssignableFrom(String.class) |
6.Class的类型信息
Class代表的类型既可以是普通的类,也可以是内部类,还可以是基本类型、数组等,对于一个给定的Class对象,它到底是什么类型呢?可以通过以下方法进行检查:
1 | public native boolean isArray() //是否是数组 |
7.类的声明信息
Class还有很多方法,可以获取类的声明信息,如修饰符、父类、接口、注解等,如下所示:
1 | //获取修饰符,返回值可通过Modifier类进行解读 |
8.类的加载
Class有两个静态方法,可以根据类名加载类:
1 | public static Class<? > forName(String className) |
ClassLoader表示类加载器,第24章会进一步介绍,initialize表示加载后,是否执行类的初始化代码(如static语句块)。第一个方法中没有传这些参数,相当于调用:
1 | Class.forName(className, true, currentLoader) |
currentLoader表示加载当前类的ClassLoader。
这里className与Class.getName的返回值是一致的。比如,对于String数组:
1 | String name = "[Ljava.lang.String; "; |
需要注意的是,基本类型不支持forName方法,也就是说,如下写法:
1 | Class.forName("int"); |
会抛出异常ClassNotFoundException。那如何根据原始类型的字符串构造Class对象呢?可以对Class.forName进行一下包装,比如:
1 | public static Class<? > forName(String className) |
需要说明的是,Java 9还有一个forName方法,用于加载指定模块中指定名称的类:
1 | public static Class<? > forName(Module module, String name) |
参数module表示模块,这是Java 9引入的类,当找不到类的时候,它不会抛出异常,而是返回null,它也不会执行类的初始化。
9.反射与数组
对于数组类型,有一个专门的方法,可以获取它的元素类型:
1 | public native Class<? > getComponentType() |
比如:
1 | String[] arr = new String[]{}; |
输出为:
1 | class java.lang.String |
java.lang.reflect包中有一个针对数组的专门的类Array(注意不是java.util中的Arrays),提供了对于数组的一些反射支持,以便于统一处理多种类型的数组,主要方法有:
1 | //创建指定元素类型、指定长度的数组 |
需要注意的是,在Array类中,数组是用Object而非Object[]
表示的,这是为什么呢?这是为了方便处理多种类型的数组。int[]
、String[]
都不能与Object[]
相互转换,但可以与Object相互转换,比如:
1 | int[] intArr = (int[])Array.newInstance(int.class, 10); |
除了以Object类型操作数组元素外,Array也支持以各种基本类型操作数组元素,如:
1 | public static native double getDouble(Object array, int index) |
10.反射与枚举
枚举类型也有一个专门方法,可以获取所有的枚举常量:
1 | public T[] getEnumConstants() |