Java 框架的核心 -- 反射
Java程序在运行时都会出现两种类型:编译时类型和运行时类型。
例如代码:Person p = new Student(); 变量 p 的编译时类型为 Person,而运行时类型为 Student。
这些变量编译时类型和运行时类型不一致的,程序员需要在运行时发现对象和类的真实信息,解决这个问题有两种做法:
- 假设在编译时和运行时都完全知道类型的具体信息。可以先用 instanceof 运算符进行判断,再进行强制转换。
- 若编译时根本无法预知该对象和类可能属于哪些类,程序只依靠运行时信息来发现该对象和类的真实信息,就必须用到反射。
获得 Class 对象
每个类被加载后,系统就会为该类生成一个对应的 Class 对象,通过该 Class 对象就可以访问到 JVM 中的这个类。
获取 Class 对象通常有三种方式:
- 使用 Class 类的
forName(String clazzName)
静态方法。参数为某个类的全限定类名。 - 调用某个类的 class 属性来获取该类对象的 Class 对象。例如,Person.class 将会返回 Person 类的 Class 对象。
- 调用某个类的 getClass() 方法。
第一种和第二种方式都是直接根据类来获取,不过相比之下,第二种方式更有优势。
所以一般情况下,都是通过类的 class 属性获取指定类的 Class 对象。如果只能获得一个字符串,就只能用第一种方式了。
一旦取得了某个类的 Class 对象之后,程序就可以调用 Class 对象的方法来获得该对象的真实信息了!
从 Class 中获取信息
Class 类提供了大量的实力方法来获取该 Class 对象所对应类的详细信息,大致如下。而每个方法包括多个重载,详细应该去 查阅 API。该类位于 java.lang.Class。
获取 Class 对应类所包含的构造器:
Constructor<T> getConstructor(Class<?>... parameterTypes)
:返回指定形参列表的 public 修饰的构造器。Constructor<?>[] getConstructors()
:返回所有 public 构造器。Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
:返回指定形参列表的构造器,与构造器的访问权限无关。Constructor<?>[] getDeclaredConstructors()
:返回所有 public 构造器,与构造器的访问权限无关。
获取 Class 对应类所包含的方法:
Method getMethod(String name, Class<?>... parameterTypes)
:返回该类指定形参列表的 public 方法。Method[] getMethods()
:返回该类所有的 public 方法。Method getDeclaredMethod(String name, Class<?>... parameterTypes)
:返回该类指定形参列表的方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。Method[] getDeclaredMethods()
:返回该类所有的方法,与方法的访问权限无关,但不包括继承的方法。
访问 Class 对应类所包含的成员变量:
Field getField(String name)
:返回指定名称的 public 成员变量。Field[] getFields()
:返回所有 public 成员变量数组。Field getDeclaredField(String name)
:返回指定名称的成员变量,与成员变量的访问权限无关。Field[] getDeclaredFields()
:返回所有成员变量数组,与成员变量的访问权限无关。
。。。
。。
。
方法太多了,看着有点繁琐。还是自己有空去看看 API 文档吧,该类位于 java.lang.Class。
使用反射生成并操作对象
Class 对象可以获得该类里的方法(Method 对象)、构造器(Constructor 对象)、成员变量(Field 对象),这三个类都位于 java.lang.reflect 包下。
程序可以通过 Method 对象来执行对应的方法,通过 Constructor 对象来调用对应的构造器创建实例,能通过 Field 对象直接访问并次改对象的成员变量值。
创建对象
通过反射来创建对象有如下两种方式:
- 使用 Class 对象的 newInstance() 方法创建实例。这种方式要求该 Class 对象的对应类有默认构造器,通过默认构造器创建实例。
- 先使用 Class 对象索取指定的 Constructor 对象,再调用 Constructor 对象的 newInstance() 方法创建实例。这种方式可以选择使用指定的构造器来创建实例。
通过第一种方式来创建对象是比较常见的。很多Java EE 框架中都需要根据配置文件信息来创建对象,从配置文件读取某个类的字符串类名,然后通过反射生成实例。
举例
使用第一种方式创建对象
// 传入一个字符串类名,根据类名生成 Java 对象
private Object createObject(String calzzName) throws Exception{
// 根据字符串来获取对应的 Class 对象
Class<?> calzz = Class.forName(calzzName);
// 使用 calzz 对应类的默认构造器创建对象
return calzz.newInstance();
}
使用第二种方式创建对象
public class CreateJFrame{
public static void main(String[] args) throws Exception{
// 获取 JFrame 对应的 Class 对象
Class<?> jframeClazz = Class.forName("javax.swing.JFrame");
// 获取 JFrame 中带一个字符串参数的构造器
Constructor ctor = jframeClazz.getConstructor(String.calzz);
// 调用 newInstance 方法创建对象
Object obj = ctor.newInstance("测试窗口");
//输出对象
System.out.println(obj);
}
}
对于上面的 CreateJFrame.java 中已知 javax.swing.JFrame 类的情形,通常没必要使用反射来创建该对象,毕竟通过反射创建对象时性能要稍微低一些。
实际上,只有当程序需要动态创建某个类的对象时,才需要考虑使用反射,通常在开发通用性比较广的框架、基础平台时可能会大量使用反射。
调用方法
当通过 Class 对象的 getMethod() 方法或者 getMethods() 获取方法后,就得到了 Method 对象或对象数组,每个 Method 对象对应一个方法。
得到 Method 对象后,程序就可通过 Method 的 invoke()
方法来调用 Method 对应的方法。
invoke(Object obj, Object... args)
:该方法中的 obj 是执行该方法的主调,args 是执行该方法时传入的实参。
访问成员变量
通过 Class 对象的 getFields() 或 getField() 方法可以获取该类所包含的全部成员变量或指定成员变量。
Field 提供了如下两组方法来读取或设置成员变量值:
getXxx(Object obj)
:获取 obj 对象的该成员变量的值。此处的 Xxx 对应 8 种基本类型,如果成员变量的类型是应用类型,则取消 get 后面的 Xxx。setXxx(Object obj, Xxx val)
:将 obj 对象的该成员变量设置成 val 值。此处的 Xxx 对应 8 种基本类型,如果成员变量的类型是应用类型,则取消 get 后面的 Xxx。
使用这两个方法可以随意地访问指定对象的所有成员变量,包括 private 修饰的成员变量。
小结
反射的内容,对于使用 Java 不多的人来说,显得有点不太实用了。但是随着开发经验慢慢积累,会发现在很多时候使用反射可以解决很多问题。特别是在 Spring 框架中,了解反射之后去使用和阅读 Spring 会轻松很多。
不过反射涉及了非常多 API,这里只是简单介绍了一些常用的,想深入了解还是要针对性去看看 API 接口文档。