反射机制
是 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 |
|