浅谈Java之RTTI与反射

反射机制 是 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
2
3
4
5
6
7
8
9
10
Foo foo1 = new Foo();

// 1. 通过类获取。 类字面常量:可以看做,任何一个类都有一个隐含的静态成员变量class。
Class c1 = Foo.class;

// 2. 通过对象获取。已知该类的对象,通过getClass()方法。
Class c2 = foo1.getClass();

// 3. 通过类的全称获取。也称之为动态加载类 (throws ClassNotFoundException)。
Class c3 = Class.forName("tk.gushizone.java.basic.reflect.Foo");

任何类的类类型只有一个。

1
System.out.println(c1 == c2 && c2 == c3); // true

通过该对象也可以对应类的实例对象。

需要有无参数的构造方法

1
Foo foo2 = (Foo) c1.newInstance();

获取类信息

  • 类Class 的实例表示 Java 应用运行时的类和接口。

  • enum 是一种类,注解 是一种接口。所有 数组 属于类。

  • 基本类型void 也有 类Class 的实例对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
Class c1 = Integer.class;  
Class c2 = Collection.class;

Class c3 = ElementType.class;
Class c4 = Override.class;
Class c5 = int[].class;

Class c6 = int.class;
Class c7 = void.class;

// 基本类型和对应包装类的类类型不相同,但和包装类型的标准字段相同。
System.out.println(c1 == c6);// false
System.out.println(c6 == Integer.TYPE);// true

通过 类类型 可以获得对应类的相关信息。例如类名:

1
2
3
4
5
6
7
System.out.println(c1.getSimpleName());// Integer
System.out.println(c2.getName());//java.util.Collection
System.out.println(c3.getName());// java.lang.annotation.ElementType
System.out.println(c4.getName());// java.lang.Override
System.out.println(c5.getName());// [I
System.out.println(c6.getName());// int
System.out.println(c7.getName());// void

这里列举一些常用的获取类信息的方法, FieldMethodConstructor 等会在之后着重说明。

Class 常用方法 说明
String getName() 获取完整类名,包括包名。
String getSimpleName() 获取简单类名,不包括报名。
Field[] getDeclaredFields() 获取所有该类自己声明的成员变量,不论访问权限。
Method[] getDeclaredMethods() 获取所有该类自己声明的方法,不论访问权限。
Constructor[] getDeclaredConstructors 获取所有该类自己声明的构造函数,不论访问权限。

Field : 字段

Field类 : 通过 Field 可以获取和操作字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void printFieldMessage(Object obj) {
Class clazz = obj.getClass();

// Field类: 成员变量也是对象
// getFields(): 所有public成员变量,包括由父类继承来的
// getDeclaredFields: 所有该类自己声明的成员变量,不论访问权限
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
Class fieldType = field.getType();
String fieldName = field.getName();

System.out.println(fieldType + " " + fieldName + ";");
}
}
1
2
3
4
5
6
// 获取成员变量信息
ClassUtil.printFieldMessage(1);
// int MIN_VALUE;
// int MAX_VALUE;
// class java.lang.Class TYPE;
// ...

Method : 方法

Method类 : 通过 Method 可以获取和操作方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void printClassMethodMessage(Object obj){
// 获取类类型: JNI本地方法获取,通过c语言实现。
Class clazz = obj.getClass();

System.out.println("类的名称: " + clazz.getName());

// Method类:方法也是对象,一个方法对应一个Method对象
// getMethods(): 所有public方法,包括由父类继承来的
// getDeclaredMethods(): 所有该类自己声明的方法,不论访问权限
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
Class returnType = method.getReturnType();
String methodName = method.getName();
Class[] paramTypes = method.getParameterTypes();

String methodInfo = returnType.getName() + " " + methodName;
methodInfo += "(";
for (Class item : paramTypes) {
methodInfo += item.getName() + ",";
}
methodInfo += ")";
System.out.println(methodInfo);
}

}
1
2
3
4
5
// 获取方法信息
ClassUtil.printClassMethodMessage(Class.class);
//类的名称: java.lang.Class
//void checkPackageAccess(java.lang.ClassLoader,boolean,)
//java.lang.Class forName(java.lang.String,)

Constructor : 构造器

Constructor类 : 通过 Constructor 可以获取和操作构造方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void printConstructMessage(Object obj){
Class clazz = obj.getClass();

// Constructor 构造方法也是函数
// getFields(): 所有public成员变量,包括由父类继承来的
// getDeclaredFields: 所有该类自己声明的构造函数
Constructor[] constructors = clazz.getDeclaredConstructors();
for (Constructor cs : constructors) {
String csName = cs.getName();
Class[] paramTypes = cs.getParameterTypes();

String csInfo = csName + "(";
for (Class item : paramTypes) {
csInfo += item.getName() + ",";
}
csInfo += ")";
System.out.println(csInfo);
}
}
1
2
3
4
5
// 构造方法信息
ClassUtil.printConstructMessage("hello");
// java.lang.String([B,int,int,)
// java.lang.String([B,java.nio.charset.Charset,)
// java.lang.String([B,java.lang.String,)

使用反射

方法反射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Foo {

public void print(){
System.out.println("I'm Foo!");
}

public void print(String s1, String s2){
System.out.println(s1 + " " + s2);
}

public void print(Integer n1, Integer n2){
System.out.println(n1 + " " + n2);
}
}
1
2
3
4
5
6
7
8
9
10
11
Foo foo = new Foo();
Class clazz = Foo.class;

Method method1 = clazz.getMethod("print", String.class, String.class);
method1.invoke(foo, "abc", "def");// abc def

Method method2 = clazz.getMethod("print", new Class[]{Integer.class, Integer.class});
method2.invoke(foo, 12, 21);// 12 21

Method method3 = clazz.getDeclaredMethod("print");
method3.invoke(foo);// I'm Foo!

字段反射

反射字段可以让我们直接访问和操作字段,下面通过两个示例来看一下。

提供模拟数据:

常见的数据源 : xml , json 等文件。

1
2
3
Map<String, Object> map = new HashMap<>();
map.put("id", 1);
map.put("name", "abc");

直接反射

反射会进行访问域检查,一般只能访问到 public 域,可以使用 setAccessible(true) 来跳过检查。

虽然不会有什么影响,但 一般还是不推荐使用这种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Class clazz = User.class;
Object obj = clazz.newInstance();

Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
int mod = field.getModifiers();
if (Modifier.isStatic(mod) || Modifier.isFinal(mod)) {
continue;
}

// 操作私有域:不进行访问域检查
field.setAccessible(true);
field.set(obj, map.get(field.getName()));
}

System.out.println(obj);// Item(id=1, name=abc)

反射Getter和Setter

这里推荐使用 PropertyDescriptor ,通过它可以友好的使用 GetterSetter

1
2
3
4
5
6
7
8
9
10
11
Class clazz = User.class;
Object obj = clazz.newInstance();

Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
PropertyDescriptor pd = new PropertyDescriptor(field.getName(), clazz);
Method wm = pd.getWriteMethod(); // 写方法
wm.invoke(obj, map.get(field.getName()));
}

System.out.println(obj); // Item(id=1, name=abc)

构造反射

反射无参构造器:

1
2
3
4
Class c1 = Foo.class;

// 需要有无参数的构造方法
Foo foo = (Foo) c1.newInstance();

反射有参构造器:

1
2
3
4
5
Class clazz = User.class;
Constructor constructor = clazz.getConstructor(new Class[]{String.class, int.class});
Object object = constructor.newInstance("Hello", 123);

System.out.println(object); // Item(id=1, name=abc)

常见应用

动态代理

代理 是一种基础的设计模式,通过它可以有效的扩展对象的功能或做一些额外的操作。

JDK 提供了 java.lang.reflect.Proxy 类,其利用反射可以简单的完成动态代理。

通过 Proxy.newProxyInstance() 可以获得一个代理对象实例。通过参数列表等,可以轻松的看出, JDK 代理只能代理接口方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
@SuppressWarnings("unchecked")
public void test() {
List<String> list = new ArrayList<>();

Class clazz = list.getClass();

List<String> proxyInstance = (List<String>) Proxy.newProxyInstance(
clazz.getClassLoader(), clazz.getInterfaces(), new InvocationHandler() {

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(list, args); // 需要被代理的对象。
}
});

proxyInstance.add("abc");

System.out.println(list); // [abc]
}

Proxy.newProxyInstance()

1
2
3
4
5
6
7
8
/**
* 获取代理对象实例
* @param loader 类加载器
* @param interfaces 接口列表
* @param h 调用处理器
* @return 代理对象实例
*/
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h);

InvocationHandler

注意 : 因为要反射调用方法,所以 InvocationHandler 一定需要被代理的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package java.lang.reflect;
/**
* 调用处理器(代理对象调用方法时,会由它处理)
*/
public interface InvocationHandler {

/**
* 代理对象调用方法时,实际会调用此方法。
* @param proxy 当前代理实例。
* @param method 代理的方法,其只会来自于被代理对象所实现的接口(或超接口)。
* @param args 方法入参。
* @return 方法返回值,其类型为被代理的方法决定不能改变。
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

动态加载

Class.forName(...) 方法又称之为 动态加载静态加载 作用于编译期,而 动态加载 作用于运行期。

JVM在运行时才会去加载 *.class 文件,若找不到会 throw ClassNotFoundException

我们常会看到使用它来加载数据库驱动。

JDBCDriver 都要求自我注册 DriverManager.registerDriver(...) ,所以只要加载即可,不需要初始化。

1
Class.forName("com.mysql.jdbc.Driver");

绕过编译: 认识泛型

反射方法作用于运行期,通过反射可以绕过泛型检查,说明编译后是去泛型化的。

实际上,编译会擦除泛型,这些容器持有对象时会将其都视为 Object 对象,取出时会进行类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Test
public void test() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
List list1 = new ArrayList();
List<String> list2 = new ArrayList<>();
List<Integer> list3 = new ArrayList<>();

list1.add(1d);
list2.add("abc");
list3.add(1000);

Class c1 = list1.getClass();
Class c2 = list2.getClass();
Class c3 = list3.getClass();

// 泛型不会影响类类型
System.out.println(c1 == c2 && c2 == c3);//true

Method method = c2.getMethod("add", Object.class);

// 反射方法作用于运行期,通过反射可以绕过泛型检查,说明编译后是去泛型化的。
method.invoke(list2, 100);
method.invoke(list3, "def");

System.out.println(list1);// [1.0]
System.out.println(list2);// [abc, 100]
System.out.println(list3);// [1000, def]
}