反射机制 是 Java 提供的一种让我们在运行时识别对象类型的方式。 通过反射可以进行更加动态的编程,这也正是框架技术十分喜欢使用的方式,了解反射后,也会对 Java 有更加深刻的认识。
RTTI
RTTI(Run-Time Type Identification) : 运行时识别对象类型。
Java 提供了三种方式, 让我们可以在运行时识别一个对象的类型。
强制类型转换 和 instanceof 都不会发生空指针异常。
| RTTI方式 | 说明 | 表现形式 | 
|---|---|---|
传统的RTTI | 
假定在编译时已知道了所有的类型。 | 主要是类型转换,例如 主动强制类型转换 和 泛型的自动转换 等。 | 
反射 | 
在运行时发现和使用类的信息。 | 通过 Class类 和 reflect包 发现和使用类信息。 | 
instanceof / isInstance | 
java 关键字和Class 方法,可以判断对象是不是某个特定类型的实例。 | 比直接进行 Class 的 == 和 equal() 判断更好,保持了类型的概念,是考虑继承的。 | 
传统RTTI 和 反射 的 真正区别
传统的 RTTI 是在编译时打开和检查
*.class文件的;反射机制中,编译时无法获得
*.class文件,所以在运行时才打开和检查的。
认识反射
JDK 提供了 Class类 和 java.lang.reflect包 来支持反射功能。
Class
简单理解
Java 中万物皆对象,类本身也是 Class类 的对象。通过 Class类 可以获取类的相关信息,但有不仅限于类。
java 中只有两种不是面向对象的
- 基本数据类型:会有对应的封装类。
 - 静态的成员:属于类,不面向对象。
 
深入了解
Class类 的实例表示 Java 应用运行时的类和接口。 enum 是一种类,注解 是一种接口。所有 数组 属于类。 基本类型 和 void 也有 类Class 的实例对象。
一般称 Class对象 为对应类的 类类型(class type) 或 字节码对象 。
*.java文件编译后会生成字节码*.class文件,其在应用运行时,被第一次使用时才会被 JVM 动态加载。
获取方式
任何类都可以获取 Class实例对象。Class的构造方法式私有的,但提供了 三种获取方式 :
1  | Foo foo1 = new Foo();  | 
任何类的类类型只有一个。
1  | System.out.println(c1 == c2 && c2 == c3); // true  | 
通过该对象也可以对应类的实例对象。
需要有无参数的构造方法
1  | Foo foo2 = (Foo) c1.newInstance();  | 
获取类信息
类Class的实例表示 Java 应用运行时的类和接口。enum是一种类,注解是一种接口。所有数组属于类。基本类型和void也有类Class的实例对象。
1  | Class c1 = Integer.class;  | 
通过 类类型 可以获得对应类的相关信息。例如类名:
1  | System.out.println(c1.getSimpleName());// Integer  | 
这里列举一些常用的获取类信息的方法, Field 、 Method 和 Constructor 等会在之后着重说明。
| Class 常用方法 | 说明 | 
|---|---|
String getName() | 
获取完整类名,包括包名。 | 
String getSimpleName() | 
获取简单类名,不包括报名。 | 
Field[] getDeclaredFields() | 
获取所有该类自己声明的成员变量,不论访问权限。 | 
Method[] getDeclaredMethods() | 
获取所有该类自己声明的方法,不论访问权限。 | 
Constructor[] getDeclaredConstructors | 
获取所有该类自己声明的构造函数,不论访问权限。 | 
Field : 字段
Field类 : 通过 Field 可以获取和操作字段。
1  | public static void printFieldMessage(Object obj) {  | 
1  | // 获取成员变量信息  | 
Method : 方法
Method类 : 通过 Method 可以获取和操作方法。
1  | public static void printClassMethodMessage(Object obj){  | 
1  | // 获取方法信息  | 
Constructor : 构造器
Constructor类 : 通过 Constructor 可以获取和操作构造方法。
1  | public static void printConstructMessage(Object obj){  | 
1  | // 构造方法信息  | 
使用反射
方法反射
1  | public class Foo {  | 
1  | Foo foo = new Foo();  | 
字段反射
反射字段可以让我们直接访问和操作字段,下面通过两个示例来看一下。
提供模拟数据:
常见的数据源 : xml , json 等文件。
1  | Map<String, Object> map = new HashMap<>();  | 
直接反射
反射会进行访问域检查,一般只能访问到 public 域,可以使用 setAccessible(true) 来跳过检查。
虽然不会有什么影响,但 一般还是不推荐使用这种方式 。
1  | Class clazz = User.class;  | 
反射Getter和Setter
这里推荐使用 PropertyDescriptor ,通过它可以友好的使用 Getter 和 Setter 。
1  | Class clazz = User.class;  | 
构造反射
反射无参构造器:
1  | Class c1 = Foo.class;  | 
反射有参构造器:
1  | Class clazz = User.class;  | 
常见应用
动态代理
代理 是一种基础的设计模式,通过它可以有效的扩展对象的功能或做一些额外的操作。 
JDK 提供了 java.lang.reflect.Proxy 类,其利用反射可以简单的完成动态代理。
通过 Proxy.newProxyInstance() 可以获得一个代理对象实例。通过参数列表等,可以轻松的看出, JDK 代理只能代理接口方法。  
1  | 
  | 
Proxy.newProxyInstance()
1  | /**  | 
InvocationHandler
注意 : 因为要反射调用方法,所以
InvocationHandler一定需要被代理的对象。
1  | package java.lang.reflect;  | 
动态加载
 Class.forName(...) 方法又称之为 动态加载 。 静态加载 作用于编译期,而 动态加载 作用于运行期。
JVM在运行时才会去加载 *.class 文件,若找不到会 throw ClassNotFoundException 。
我们常会看到使用它来加载数据库驱动。
JDBCDriver都要求自我注册DriverManager.registerDriver(...),所以只要加载即可,不需要初始化。
1  | Class.forName("com.mysql.jdbc.Driver");  | 
绕过编译: 认识泛型
反射方法作用于运行期,通过反射可以绕过泛型检查,说明编译后是去泛型化的。
实际上,编译会擦除泛型,这些容器持有对象时会将其都视为 Object 对象,取出时会进行类型转换。
1  | 
  |