RTTI - 运行期类型识别
RTTI的概念:当我们手上只有基类型的一个引用时,利用它判断一个对象的正确类型。
本章将讨论如何利用 Java 在运行期间查找对象和类信息。这主要采取两种形式
01) 一种是传统 RTTI ,它假定我们已在编译和运行期拥有所有类型
02) 另一种是 Java 1.1 特有的 “反射” 机制,利用它可以在运行期独立查找类信息
Class对象
要理解 RTTI 在 Java 中的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工作是由称为 Class 对象的特殊对象完成的,它包含了与类有关的信息。事实上,Class对象就是用来创建所有的”常规”对象的。Java使用 Class 对象来执行其 RTTI ,比如类型转换这样的操作,Class 类还拥有大量其它的使用 RTTI 的方式。
所有的类都是在对其第一次使用时,动态加载到 JVM 中的,当程序创建第一个对类的静态成员的引用时,就会加载这个类。这证明构造器也是类的静态方法 (虽然未使用 static 关键字)
一旦某个类的Class 对象被载入内存,他就被用来创建这个类的所有对象
1 | public class ForNameTest { |
2 | public static void run(){ |
3 | System.out.println("in run"); |
4 | Class c = null; |
5 | Candy candy = new Candy(); |
6 | |
7 | try { |
8 | // 需要传入全限定名 |
9 | c = Class.forName("com.example.rtti.Gum"); |
10 | } catch (ClassNotFoundException e) { |
11 | e.printStackTrace(); |
12 | } |
13 | |
14 | Cookie cookie = new Cookie(); |
15 | } |
16 | |
17 | } |
18 | |
19 | class Candy{ |
20 | static { |
21 | System.out.println("candy"); |
22 | } |
23 | } |
24 | |
25 | class Gum{ |
26 | static { |
27 | System.out.println("Gum"); |
28 | } |
29 | } |
30 | |
31 | class Cookie{ |
32 | static { |
33 | System.out.println("cookie"); |
34 | } |
35 | } |
36 | |
37 | ------------------------------------------------------------- |
注意这个函数:Class.forName(“Gum”)
这个方法是 Class 类(所有 Class 对象都属于这个类)的一个 static 成员。forName 是取得 Class 对象的引用的一种方法,对 forName() 的调用是为了它产生的 “副作用”:如果类 Gum 还没被加载就加载他(注意还没创建Gum对象哈,加载了不一定创建对象了),在加载过程中,Gum的static 子句被执行
无论什么时候,只要你想在运行时使用类型信息,就必须首先获得对恰当的 Class 对象的引用。你可以通过 Class.forName(不持有该类型的对象)或者通过 object.class(通过持有的对象,属于根类 Object 方法,它将返回该对象的 实际类型 的Class引用)
getName / getConnicalName 的区别:getName() 返回的是虚拟机里面的 class 的表示,而getCannonicalName() 返回的是更容易理解的表示。对大部分 class 来说,这两个方法没有什么不同,但是对于 array 或 内部类来说是有区别的。
你可以用 getSuperclass()方法查询其直接基类(一层继承关系),这将返回你可以用来进一步查询的Class对象。因此,你可以在运行时发现一个对象完整的类继承结构
Class.forName() : forName 是 Class 的一个静态方法,
Class 的 newInstance() 方法是实现 “虚拟构造器” 的一种途径。虚拟构造器允许你声明:”我不知道你的确切类型,但是无论如何要正确地创建你自己”,在下面的示例中, up 仅仅是一个 Class 引用,在编译期不具备更进一步的类型信息。当你创建实例时,会得到 Object 引用,但是这个引用指向的是 Toy 对象,如果你需要使用到 Object 不具有的属性,那么你就必须进行转型。关于newInstance和new的区别,可以看看这篇文章 new 与 newInstance 的区别.
需要注意的是,使用 Class # newInsatnce 的时候,必须保证该类有默认构造函数,且必须以经被加载。
Class # getInterfaces : 返回的也是 Class 对象数组 Class<?>[],他们表示在感兴趣的 Class 对象中所包含的接口的集合
1 | package com.example.danlib; |
2 | import java.util.*; |
3 | import java.io.*; |
4 | import java.lang.*; |
5 | |
6 | interface IFirst{ } |
7 | interface ISecond{ } |
8 | |
9 | class Toy{ |
10 | Toy(){} |
11 | Toy(int i){} |
12 | } |
13 | |
14 | class FancyToy extends Toy implements IFirst,ISecond{ |
15 | FancyToy(){ super(1); } |
16 | } |
17 | |
18 | public class ToyTest{ |
19 | |
20 | private static void printInfo(Class cc){ |
21 | System.out.println("Class name: "+cc.getName()+ |
22 | " is interface ? ["+cc.isInterface()+"]"); |
23 | System.out.println("Simple name: "+cc.getSimpleName()); |
24 | // |
25 | System.out.println("Canonical name: "+cc.getCanonicalName()); |
26 | } |
27 | |
28 | public static void main(String[] args){ |
29 | Class c = null; |
30 | |
31 | try{ |
32 | c = Class.forName("com.example.danlib.FancyToy"); |
33 | }catch(ClassNotFoundException e){ |
34 | System.out.println("Can't find FancyToy"); |
35 | System.exit(1); |
36 | } |
37 | |
38 | printInfo(c); |
39 | |
40 | for(Class face:c.getInterfaces()){ |
41 | printInfo(face); |
42 | } |
43 | |
44 | Class up = c.getSuperclass(); |
45 | Object obj = null; |
46 | try{ |
47 | obj = up.newInstance(); |
48 | }catch(InstantiationException e){ |
49 | System.out.println("Cannot instantiate "); |
50 | System.exit(1); |
51 | }catch(IllegalAccessException e){ |
52 | System.out.println("Cannot access"); |
53 | System.exit(1); |
54 | } |
55 | |
56 | printInfo(obj.getClass()); |
57 | } |
58 | } |
59 | |
60 | ------------------------------------------------------------------------------- |
61 | Class name: com.example.danlib.FancyToy is interface? [false] |
62 | Simple name: FancyToy |
63 | Canonical name: com.example.danlib.FancyToy |
64 | Class name: com.example.danlib.IFirst is interface? [true] |
65 | Simple name: IFirst |
66 | Canonical name: com.example.danlib.IFirst |
67 | Class name: com.example.danlib.ISecond is interface? [true] |
68 | Simple name: ISecond |
69 | Canonical name: com.example.danlib.ISecond |
70 | Class name: com.example.danlib.Toyis interface? [false] |
71 | Simple name: Toy |
72 | Canonical name: com.example.danlib.Toy |
getName() vs getCanonicalName()
1 | package com.example.danlib; |
2 | |
3 | public class NestedClassTest { |
4 | |
5 | public class NestedClass{ |
6 | |
7 | } |
8 | |
9 | public static void main(String[] args){ |
10 | NestedClassTest nestedClass = new NestedClassTest(); |
11 | NestedClassTest.NestedClass innerClass = nestedClass.new NestedClass(); |
12 | |
13 | Class c = null; |
14 | c = innerClass.getClass(); |
15 | |
16 | |
17 | if(c!=null){ |
18 | printName(c); |
19 | } |
20 | } |
21 | |
22 | |
23 | public static void printName(Class c){ |
24 | System.out.println("getName() : "+c.getName()); |
25 | System.out.println("getCanonicalName(): "+ c.getCanonicalName()); |
26 | System.out.println("getSimpleName() : "+c.getSimpleName()); |
27 | System.out.println("getTypeName() : "+c.getTypeName()); |
28 | } |
29 | } |
30 | ------------------------------------------------------------------------------ |
31 | getName() : com.example.danlib.NestedClassTest$NestedClass |
32 | getCanonicalName(): com.example.danlib.NestedClassTest.NestedClass |
33 | getSimpleName() : NestedClass |
34 | getTypeName() : com.example.danlib.NestedClassTest$NestedClass |
可以看到,在表示内部类的时候,CanonicalName 依然是用 “.” 分割,而 Name则是使用 “$” 来区分是不是进入了一个类的内部
类字面常量
Java 还提供了另外一种方法来生成对 Class 对象的引用,即使用 类字面常量。
1 | FancyToy.class |
这样做不仅更简单,也更安全,因为它在编译时就会受到检查(因此不需要置于try块中 ,不过我想 forName 会更灵活一点),类字面量不仅可以用于普通的类,还可以用于 接口、数组 以及 基本数据类型。另外,对于基本数据类型的包装器类,还有一个标准字段 TYPE 。TYPE 字段是一个引用,指向对应的基本数据类型的 Class 对象。
1 | public static final Class<Boolean> TYPE = Class.getPrimitiveClass("boolean"); |
要尤其注意 使用 .class 获取引用 和 使用forName 获取引用 的区别:当使用 .class 来创建对 Class 对象的引用时,不会自动地初始化该 Class 对象
泛化的 Class 引用
Class 引用总是指向某个 Class 对象,它可以制造类的实例,并包含可作用于这些实例的所有方法代码。它还包含该类的静态成员。因此,Class 引用表示的就是它所指向的对象的确切类型,而该对象(指向某个具体类型的Class对象)就是Class类的一个对象。
Class
Class<?> 优于平凡的 Class ,即便他们是等价的,并且平凡的 Class 不会产生编译器警告信息。 Class
第三,注意 Class
向 Class 引用添加泛型语法的原因仅仅是为了提供编译期类型检查,因此如果你操作有误,稍后会立即发现这一点。在使用普通Class引用,你不会误入歧途,但是一旦犯了错误,就会直到运行时你才发现。
再来看下面这个实例,它使用了泛型语法。它存储了一个类引用,稍后又差生了一个list(使用定义了的create方法),List的对象是使用 newInstance 方法(我们假定传入的任何类型都有一个默认的构造器),通过该Class引用生成的:
注意,newInstance() 将返回该对象的确切类型,而不仅仅只是在 ToyTest.java 中看到的基本的 Object。这在某种程度上来说有些受限:
1 | Class<FancyToy> ftClass = FancyToy.class; |
2 | FancyToy fancyToy = ftClass.newInsatnce(); |
3 | |
4 | Class<? super FancyToy> up = ftClass.getSuperclass(); |
5 | //Class<Toy> up2 = ftClass.getSuperclass(); This didn't compile. |
6 | |
7 | // only produces Object |
8 | Object obj = up.newInsatnce(); |
如果你手头的是超类,那编译器将只允许你声明超类引用是 “某个类,它是FancyToy的超类”。这看上去有点奇怪,因为编译器在编译期就知道它是什么类型了–在本例中就是 Toy.class,而不仅仅是 “某个类,它是 fancyToy 的超类”。总之,记住这个结论就好,并且注意,由于这种含糊性, up.newInstance 仅仅是返回一个 Object,而不是精确类型。
反射:运行时的类信息
如果不知道某个对象的确切类型,RTTI可以告诉你。但是有一个限制,这个类型在编译时必须已知,这样才能使用 RTTI 来识别他,并利用这些i西南西做一些有用的事情。换句话说,在编译时,编译器必须知道所有要通过 RTTI 来处理的类。
初看起来似乎这并不是一个限制,但是假设你获取了一个指向某个并不在你程序空间的对象的引用,事实上,在编译时你的程序根本没法获知这个对象所属的类。
Class类与 java.lang.reflect 类库一起对 反射 的概念进行了支持。该类库包含了 Field、Method、Constructor 类(每个类都实现了 Member 接口)。这些类型的对象是由 JVM 在运行时创建的,用以表示未知类里对应的成员。这样你就可以使用 Constructor 创建新的对象,用 get 和 set 方法读取和修改与 Filed 对象关联的字段,用 invoke 方法调用与 Method 对象关联的方法。
1 | import java.lang.reflect.Constructor; |
2 | import java.lang.reflect.Method; |
3 | import java.util.regex.Pattern; |
4 | |
5 | import static com.example.libreflect.PrintUtils.print; |
6 | |
7 | public class ShowMethods{ |
8 | private static String useage = |
9 | "usegae:\n"+ |
10 | "ShowMethods qualified.class.name\n"+ |
11 | "To show all methods in class or:\n"+ |
12 | "ShowMethods qualified.class.name word\n"+ |
13 | "To search for methods involving word"; |
14 | private static Pattern p = Pattern.compile("\\w+\\."); |
15 | |
16 | public static void main(String[] args){ |
17 | if(args.length<1){ |
18 | print(useage); |
19 | System.exit(0); |
20 | } |
21 | int lines = 0; |
22 | |
23 | try{ |
24 | Class<?> c = Class.forName(args[0]); |
25 | Method[] methods = c.getMethods(); |
26 | Constructor[] ctors = c.getConstructors(); |
27 | if(args.length==1){ |
28 | for(Method method:methods){ |
29 | print(p.matcher(method.toString()).replaceAll("")); |
30 | } |
31 | // 打印构造方法 |
32 | for(Constructor ctor:ctors){ |
33 | print(p.matcher(ctor.toString()).replaceAll("")); |
34 | } |
35 | lines = methods.length + ctors.length; |
36 | }else{ |
37 | for(Method method:methods){ |
38 | if(method.toString().indexOf(args[1])!=-1){ |
39 | print(p.matcher(method.toString()).replaceAll("")); |
40 | lines++; |
41 | } |
42 | } |
43 | for(Constructor ctor:ctors){ |
44 | if(ctor.toString().indexOf(args[1])!=-1){ |
45 | print(p.matcher(ctor.toString()).replaceAll("")); |
46 | lines++; |
47 | } |
48 | } |
49 | } |
50 | }catch ( ClassNotFoundException e){ |
51 | print("Class not found"); |
52 | } |
53 | } |
54 | } |
55 | |
56 | // BaseClass.java |
57 | public class BaseClass { |
58 | public void superMethod(){} |
59 | } |
60 | |
61 | // TestClass.java |
62 | public class TestClass extends BaseClass { |
63 | public void subMethod(){} |
64 | } |
65 | |
66 | //------------------------------------------------------------------------------ |
67 | input: java ShowMethods TestClass |
68 | output: |
69 | public void subMethod() |
70 | public void superMethod() |
71 | public final void wait() throws InterruptedException |
72 | public final void wait(long,int) throws InterruptedException |
73 | public final native void wait(long) throws InterruptedException |
74 | public boolean equals(Object) |
75 | public String toString() |
76 | public native int hashCode() |
77 | public final native Class getClass() |
78 | public final native void notify() |
79 | public final native void notifyAll() |
80 | public TestClass() |
如果我们覆写一下 BaseClass 的 superMethod 方法,我们看看会怎样:
1 | ... |
2 | |
3 | public class TestClass{ |
4 | public void subMethod(){} |
5 | |
6 | |
7 | public void superMethod(){} |
8 | } |
9 | |
10 | //----------------------------------------------------- |
11 | output: Same as before <只会列出子类的方法声明> |
- Class 的 getMethods() 和 getConstructors() 方法分别返回 Method 对象的数组 和 Constructor 对象的数组(包含默认构造器)。并且这两个类都提供了更深层的方法,用以解析其对象所代表的方法,并获取其名字、输入参数以及返回值。上面仅仅是利用 toString 生成一个完整的方法特征签名的字符串。
- 上面的例子是一个实用工具,如果不是特别记得一个类是否有某个方法,或者不知道一个类究竟能做什么,例如 Color 对象,而又不想去查找 JDK 文档,这时这个工具确实能节省很多时间。(不过现在的IDE好像查询功能很齐全了)
反射常用 API 总结
Class.java
01) 获取构造器
1 | // 返回所有的public类型的构造器 |
2 | Constructor<?>[] getConstructors() |
3 | |
4 | // 返回指定参数类型 public的构造器。 |
5 | Constructor<T> getConstructor(类<?>... parameterTypes) |
6 |
|
7 | // 返回所有的构造器 |
8 | Constructor<?>[] getDeclaredConstructors() |
9 |
|
10 | // 返回指定参数类型的构造器(public, protected, private)。 |
11 | Constructor<T> getDeclaredConstructor(类<?>... parameterTypes) |
02)获取变量
1 | // 获取类中所有public类型的属性变量 |
2 | Field[] getFields() |
3 | |
4 | // 根据变量名获取对应public类型的属性变量 |
5 | Field getField(String name) |
6 |
|
7 | // 获得类中所有属性变量 |
8 | Field[] getDeclaredFields() |
9 |
|
10 | // 根据变量名获得对应的变量,访问权限不限; |
11 | Field getDeclaredField(String name) |
03)获取方法
1 | // 获取全部的public的函数(包括从基类继承的、从接口实现的所有public函数) |
2 | public Method[] getMethods() |
3 | |
4 | // 获取“名称是name,参数是parameterTypes”的public的函数(包括从基类继承的、从接口实现的所有public函数) |
5 | public Method getMethod(String name, Class[] parameterTypes) |
6 |
|
7 | // 获取全部的类自身声明的函数,包含public、protected和private方法。 |
8 | public Method[] getDeclaredMethods() |
9 |
|
10 | // 获取“名称是name,参数是parameterTypes”,并且是类自身声明的函数,包含public、protected和private方法。 |
11 | public Method getDeclaredMethod(String name, Class[] parameterTypes) |
12 |
|
13 | // 如果这个类是“其它类中某个方法的内部类”,调用getEnclosingMethod()就是这个类所在的方法;若不存在,返回null。 |
14 | public Method getEnclosingMethod() |
Field.java / Method.java / Constructor.java
1 | //三个类共有的API(common APIs) |
2 | Class getDeclaringClass() |
3 | int getModifiers() |
4 | String getName() |
5 |
|
6 | //单独的API (specific APIs) |
7 | Class[] getParameterTypes() // Method.java/Constructor.java |
8 | Class getReturnType() // Method.java |
- getModifiers() : 返回一个用于描述构造器、方法 或 域 的修饰符的整型数值。使用 Modifier 类中的方法可以分析这个返回值。
Modifier.java
1 | static String toString(int modifiers); |
2 | static boolean isAbstract(int modifiers); |
3 | static boolean isFinal(int modifiers); |
4 | static boolean isInterface(int modifiers); |
5 | static boolean isPrivate(int modifiers); |
6 | static boolean isPublic(int modifiers); |