*文章内容基于《Java Reflection In Action》前三章部分内容整理
博文链接:伊地知虾饺的博客-Java反射-Reflection
什么是反射?
反射(reflection)是程序在运行时检查或改变其自身行为和结构的一种能力。程序了解自身(对于Java来说指的是有关类的信息)的能力被称为“内省(introspection)”。书中写到“程序的内省就好像你照镜子时看见自己一样”。人在照镜子时能看见自己的外貌和仪态,类似的,Java程序通过内省能获取有关类的结构和行为有关的信息。
有关Method
获取类中的方法
Method getMethod(String name, Class[] parameterTypes); //1
Method[] getMethods(); //2
Method getDeclaredMethod(String name, Class[] parameterTypes); //3
Method[] getDeclaredMethods(); //4
上述代码块中, String name
是方法的名字, Class[] parameterTypes
是要获取的方法接受的参数的类。第二个参数可以是数组,也可是可变个数的参数。
getDeclaredMethod
和getDeclaredMethods
返回在这个类中声明的方法,不包括继承自父类的方法。然而,这两个方法不会关注方法的可见性——无论方法是public,protected,package还是private,都会被返回。
getMethod
和 getMethods
返回类中的所有方法——无论是在类中声明的还是继承自父类的。但是这两个方法只会返回被声明为public的方法。
当找不到方法时,会抛 NoSuchMethodException
。
有关基本类型、接口和数组
在上一节的方法中,第二个参数必须包含想要获得的方法的参数的类。然而,基本类型、接口和数组并不能够成为具体的类对象(虽然Java中有Integer这样的类,但这里指的是int这样的基本类型。即使JVM会自动拆箱/装箱,但在作为参数时二者仍是不同的)。
虽然基本类型不能作为一个类对象,Java还是为其提供了class属性(如: int.class
)。可以使用下面的语句得到Vector类中的get方法:
Method m = Vector.class.getMethod("get", int.class);
可以使用 isPrimitive()
来判断一个类对象(class object)是否是基本类型。另外,即使void并不是Java中的类,它也有class属性。需要注意的是,isPrimitive()
对void.class
返回 true
。
对于接口,Java也为其引入了类对象。例如,Vector中addAll
方法接收一个Collection
接口的具体实现类,要获取该方法,可以使用:
Method m = Vector.class.getMethod( "addAll", Collection.class);
类似的,可以使用 isInterface()
判断一个类对象是否代表一个接口。
在Java中,数组是对象,但数组的对象在运行时由JVM创建。数组的类名常量与其他的类相同。例如,Vector中的copyInto
方法接收一个Object数组,可以使用下面的语句获取这个方法:
Method m = Vector.class.getMethod( "copyInto", Object[].class);
类似的,可以使用isArray()
来判断一个类对象是否代表数组。另外,可以用getComponentType()
来获取数组包含的类的对象。对于二维数组来说,int[][].class.getComponentType()
返回int[].class
。需要注意的是,int[][]
的component type是int[]
,而element type是 int
(不知道怎么翻译这两个词最恰当,故保留原文中的说法)。
有关Method对象
常用API:
Method | Description |
---|---|
Class getDeclaringClass() | 返回声明了该Method对象代表的方法的类的类对象 |
Class[] getExceptionTypes() | 以类对象数组的形式返回该Method对象代表的方法抛出的异常 |
int getModifiers() | 返回该Method对象代表的方法的修饰符,以int形式(?) |
String getName() | 返回该Method对象代表的方法的方法名 |
Class[] getParameterTypes() | 以类对象数组的形式返回该Method对象代表的方法的参数 |
Class getReturnType() | 返回该Method对象代表的方法的返回值类型 |
Object invoke(Object obj, Object[] args) | 在指定的Object上调用该Method对象代表的方法 |
动态调用方法(Dynamic Invocation)
动态调用方法指的是程序调用编译时并未确定的方法,具体到API,指的是上面表格中的invoke
方法:
Object invoke(Object obj, Object[] args)
方法的第一个参数obj表示,在这个对象上调用Method对象代表的方法;第二个参数args表示向这个方法传递的参数,可以是数组,也可以是可变个数的参数。
当调用静态方法时,第一个参数会被忽略,因此也可以直接写null
;对于无参方法,第二个参数可以是空数组,也可以是null
。
在动态调用方法时使用基本类型
在调用invoke
时,第二个参数是一个Object数组。当传入基本类型参数时,需要使用Integer
这样的包装类装箱。当方法的返回值是int时,将返回的Object强转为Integer并拆箱。在运行时,JVM会自动拆箱/装箱,也就是说,在实际写代码时,并不需要在传参时手动装箱,或是将返回值手动拆箱。
public class Main {
public static void main(String[] args) throws Exception{
//创建TestObject的类对象
Class<TestObject> testObjectClass = TestObject.class;
//得到类对象中的method方法
Method method = testObjectClass.getDeclaredMethod("method", int.class, int.class);
TestObject obj = new TestObject();
/*在obj上调用method方法,并将返回得Object强转为Integer
可以看到并不需要手动拆箱/装箱
传入的第二个参数不是数组,而是可变个数的参数
*/
int value = (Integer) (method.invoke(obj, 3, 3));
System.out.println(value);
}
}
class TestObject {
public int method(int arg1, int arg2) {
return arg1 + arg2;
}
}
常见异常
对于Java来说,方法由方法签名和声明它的类同时决定。也就是说,两个方法的方法签名完全相同,但是在不同的类中声明,就不是同一个方法。当动态调用方法时,如果传入的Object代表的类和声明这个方法的类不一样,就会抛出IllegalArgumentException
。
另外,如果调用方法的类对方法没有权限(比如方法被声明为private),会抛出IllegalAccessException
。
IllegalArgumentException
在以下两种情况会被抛出:
- 传入的Object代表的类不支持该方法
- 传入的数组或可变个数参数类型或个数与方法不匹配
如果调用的方法内部抛出了异常,该异常会被包装为InvocationTargetException
然后抛出。
有关Field(字段)
在运行中找到Field(类的字段 )
与获取类中的方法相似,在运行时获取类的Field也有四个常用方法。getDeclaredField
和getDeclaredFields
获取仅在类中声明的常量,无论可见性;而getField
和getFields
获取类和所有父类中的public常量
Method | Description |
---|---|
Field getField(String name) | 返回类对象代表的类中指定public字段的Field对象 |
Field[] getFields() | 以Field对象数组的形式返回类对象代表的类中的所有public字段的Field对象 |
Field getDeclaredField(String name ) | 返回仅在该类对象代表的类中声明的字段的Field对象 |
Field[] getDeclaredFields() | 以Field对象数组的形式返回所有在该类对象代表的类中声明的字段的Field对象 |
有关Field对象
Field对象代表类中特定的一个字段。每个Field对象都含有这个字段的元信息,包括字段的名称、声明这个字段的类以及修饰符。Field对象还提供若干获取和设置方法。下表罗列了常用的与Field对象有关的API。其中,getBoolean
和setBoolean
方法下面的省略号表示还有许多类似的方法,这些方法都像getBoolean
和setBoolean
方法一样返回一个基本类型的值或是设置一个基本类型的值。
Method | Description |
---|---|
Class getType() | 返回代表了 该Field对象代表的字段 被声明时的类 的类对象 |
Class getDeclaringClass() | 返回声明了该Field对象代表的字段的类的类对象 |
String getName() | 返回字段被声明时的名称 |
int getModifiers() | 以int的形式返回该Field对象代表的字段被声明时的修饰符 |
Object get( Object obj ) | 返回该Field对象代表的字段中特定对象的值 |
boolean getBoolean( Object obj ) …… | 返回该Field对象代表的字段中布尔类型字段的值 |
void set( Object obj, Object value ) | 设置Field对象代表的字段中特定对象的值 |
void setBoolean( Object obj, boolean value ) …… | 设置该Field对象代表的字段中布尔类型字段的值 |
常见异常
- 如果该字段未被定义,抛出
IllegalArgumentException
- 如果调用
set
方法时,传递的参数类型与Field对象中的不匹配,抛出IllegalArgumentException
- 当调用
get
方法获取基本类型的返回值,而实际获取到的值不能被转换为基本类型时,同样抛出IllegalArgumentException
- 如果字段对调用
get
或set
方法的类是不可见的,抛出IllegalAccessException
。对于Field和Method来说,不可见对象可以通过调用field.setAccessible(true)
来越过修饰符。
动态加载类以及通过反射创建对象
动态加载类
使用静态方法Class.forName(String name)
, 例:
Class cls = Class.forName(dbClassName);
该方法在确保类被载入后返回一个代表该类的类对象。该过程由类加载器(class loader)实现,如果在调用forName
方法之前类加载器已经加载过该类,它就会直接返回一个代表这个类的类对象;如果此前没加载过,类加载器就会在所有可能的路径下查找对应的.class
文件,如果没找到,就会抛出ClassNotFoundException
(这部分是全书的第三章。作者在原书中写到,这部分更详细的内容将在第六章讨论)
通过反射创建对象
通过反射创建对象的基础
调用X.class.newInstance()
(该方法在Java9之后被弃用)。使用此方法在效果上与调用X的无参构造方法是一样的。二者的不同之处在于,newInstance
可以通过forName
创建,即在编译时并不需要知道确切的类名,从而达到动态创建对象的效果。
使用Constructor对象
使用Constructor对象,可以获取并调用含有参数的构造方法,常见API如下表:
Method |
---|
Constructor getConstructor(Class[] parameterTypes) |
Constructor getDeclaredConstructor(Class[] parameterTypes) |
Constructor[] getConstructors() |
Constructor[] getDeclaredConstructors() |
具体用法及特性与前文写到的Method和Field的get方法都是类似的,此处不做赘述。同样的,参数中的类对象数组同样可以替换为可变个数的参数。
Constructor
与Method
很像,但要调用Constructor对象代表的构造方法,应该调用newInstance()
而不是invoke
。Constructor对象常用的API如下表:
Method |
---|
Class getDeclaringClass() |
Class[] getExceptionTypes() |
int getModifiers() |
String getName() |
Class[] getParameterTypes() |
Object newInstance( Object[] initargs ) |
同样的,newInstance
方法中的参数也可以是可变个数的参数而不是数组。