Android Tech And Thoughts.

Java RTTI [include reflect]

Word count: 3.6kReading time: 15 min
2019/12/04 Share

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 与 Class<? extends T> 的区别 。

向 Class 引用添加泛型语法的原因仅仅是为了提供编译期类型检查,因此如果你操作有误,稍后会立即发现这一点。在使用普通Class引用,你不会误入歧途,但是一旦犯了错误,就会直到运行时你才发现。

genericClass.png

再来看下面这个实例,它使用了泛型语法。它存储了一个类引用,稍后又差生了一个list(使用定义了的create方法),List的对象是使用 newInstance 方法(我们假定传入的任何类型都有一个默认的构造器),通过该Class引用生成的:

newInstance.png

注意,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 来处理的类。

初看起来似乎这并不是一个限制,但是假设你获取了一个指向某个并不在你程序空间的对象的引用,事实上,在编译时你的程序根本没法获知这个对象所属的类。

知乎:Java传统RTTI与Reflection的区别

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
    @Override
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);
CATALOG
  1. 1. RTTI - 运行期类型识别
  2. 2. Class对象
    1. 2.1. 类字面常量
    2. 2.2. 泛化的 Class 引用
  3. 3. 反射:运行时的类信息
    1. 3.0.1. 反射常用 API 总结