Java类型信息rtti

RTTI Run-Time Type Infomation(运行时类型信息),在Java运行时,RTTI维护类的相关信息,识别类和对象的信息。 多态(polymorphism)是基于RTTI实现的。RTTI的功能主要是由Class类实现的。

严格的说,反射也是一种形式的RTTI,不过,一般的文档资料中把RTTI和反射分开,因为一般的,大家认为RTTI指的是传统的RTTI ,通过继承和多态来实现,在运行时通过调用超类的方法来实现具体的功能(超类会自动实例化为子类,或使用instance of)。

传统的RTTI有3种实现方式:

1.向上转型或向下转型(upcasting and downcasting),在java中,向下转型(父类转成子类)需要强制类型转换。

2.Class对象(用了Class对象,不代表就是反射,如果只是用Class对象cast成指定的类,那就还是传统的RTTI)。

3.instanceof或isInstance()。

其中cast的用法如下所示:

public class Test1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Father f = new Child();
        Class<Child> cc = Child.class ;
        Child c = cc.cast(f) ; 
    }
}
class Father {
}
class Child extends Father {
}
    

这里 Child c = cc.cast(f) ; 等价于 Child c = (Child)f ;

传统的RTTI与反射最主要的区别:RTTI,编译器在编译时打开和检查.class文件。 而反射不需要,反射在运行时打开和检查.class文件。传统的RTTI使用转型或Instance形式实现, 但都需要指定要转型的类型,比如:

public void rtti(Object obj){
    Toy toy = Toy(obj);
    // Toy toy = Class.forName("com.rtti.Toy")
    // obj instanceof Toy
}

注意其中的obj虽然是被转型了,但在编译期,就需要知道要转成的类型Toy,也就是需要Toy的.class文件。 相对的,反射完全在运行时在通过Class类来确定类型,不需要提前加载Toy的.class文件。

Class类

Class类是”类的类”(class of classes)。如果说类是对象的抽象和集合的话,那么Class类就是对类的抽象和集合。 每一个Class类的对象代表一个其他的类。比如下面的程序中,Class类的对象c1代表了Father类,c2代表了Child类。

public class Test1 {
    public static void main(String[] args)
    {
        Father father = new Father();
        Class c1  = father.getClass();
        System.out.println(c1.getName());
        Father child = new Child();
        Class c2  = child.getClass();
        System.out.println(c2.getName());
    }
}
class Father {
}
class Child extends Father {
}
    

当我们调用对象的getClass()方法时,就得到对应Class对象的引用。 在c2中,即使我们将child对象的引用向上转换为Father对象的引用,对象所指向的Class类对象依然是Child。

Java中每个对象都有相应的Class类对象,因此,我们随时能通过Class对象知道某个对象“真正”所属的类。无论我们对引用进行怎样的类型转换, 对象本身所对应的Class对象都是同一个。当我们通过某个引用调用方法时,Java总能找到正确的Class类中所定义的方法,并执行该Class类中的代码。 由于Class对象的存在,Java不会因为类型的向上转换而迷失。这就是多态的原理。

除了getClass()方法外,我们还有其他方式调用Class类的对象。

public class Test1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class c1  = Class.forName("com.souly.myapplication.test.Father");
        System.out.println(c1.getName());
        Class c2  = Child.class ;
        System.out.println(c2.getName());
    }
}
class Father {
}
class Child extends Father {
}
  

上面显示了两种方式:

1.forName()方法接收一个字符串作为参数,该字符串是类的名字。这将返回相应的Class类对象。 2.Child.class方法是直接调用类的class成员。这将返回相应的Class类对象。

使用forName(String str)有一个副作用:如果类没有被加载,调用它会触发类的static子句(静态初始化块)。与之相比,更好用的是类字面常量, 例如Child.class。支持编译时检查,所以不会抛出异常。使用类字面常量创建Class对象的引用与forName(String str) 不同,不会触发类的static子句(静态初始化块)。所以,更简单更安全更高效。类字面常量支持类、接口、数组、基本数据类型。

Class类的加载

Java程序在运行之前并没有被完全加载,各个部分是在需要时才被加载的。

为了使用类而作的准备包含三步:

1.加载。由classloader查找class字节码文件,创建一个Class对象。 2.链接:验证字节码文件,为静态域分配存储空间,如果必需的话,会解析这个类创建的对其他类的所有引用(比如说该类持有static域)。 3.初始化:初始化父类,执行静态初始化器和静态初始化块。

其中静态初始化器可以理解为静态域在定义处的初始化,如:static Dog d = new Dog(0);。

final static成员和static成员的在以上三个过程中不一样,final static成员被称为“编译器常量”,在编译时已经被赋值, 所以可以在类加载前就进行访问,而静态成员(非final)需要在类加载后、class对象初始化之后赋值。

当Java创建某个类的对象,jvm虚拟机的classLoader会检测对象对应的Class对象是否已加载, 比如Child类对象时,Java会检查内存中是否有相应的Class对象。如果内存中没有相应的Class对象,会依据相关途径查询对应.class文件 (如 通过classPath在本地文件系统进行查找,在获取到.class文件之后会对文件进行有效验证,之后会依据Class对象进行详细类型对象的创建。 在Class对象加载成功后,其他Child对象的创建和相关操作都将参照该Class对象。

更详细的介绍可以参考:

Java 类加载与初始化

举例说明

interface HasBatteries {}
interface Waterproof {}
interface Shoots {}
class Toy {
    Toy() {}
    Toy(int i) {}
}
class FancyToy extends Toy implements HasBatteries, Waterproof, Shoots {
    FancyToy() { super(1); }
}
public class ToyTest {
    static void printInfo(Class cc) {
        System.out.println("Class name: " + cc.getName() +
                " is interface? [" + cc.isInterface() + "]");
    }
    public static void main(String[] args) {
        Class c = null;
        try {
            c = Class.forName("com.souly.myapplication.test.FancyToy");
        } catch(ClassNotFoundException e) {
            System.out.println("Can't find FancyToy");
            System.exit(1);
        }
        printInfo(c);
        Class[] faces = c.getInterfaces();
        for(int i = 0; i < faces.length; i++)
            printInfo(faces[i]);
        Class cy = c.getSuperclass();
        Object o = null;
        try {
            // Requires default constructor:
            o = cy.newInstance(); // (*1*)
        } catch(InstantiationException e) {
            System.out.println("Cannot instantiate");
            System.exit(1);
        } catch(IllegalAccessException e) {
            System.out.println("Cannot access");
            System.exit(1);
        }
        printInfo(o.getClass());
    }
}
    

运行后输出结果如下:

   Class name: com.souly.myapplication.test.FancyToy is interface? [false]
   Class name: com.souly.myapplication.test.HasBatteries is interface? [true]
   Class name: com.souly.myapplication.test.Waterproof is interface? [true]
   Class name: com.souly.myapplication.test.Shoots is interface? [true]
   Class name: com.souly.myapplication.test.Toy is interface? [false]
    

从中可以看出,class FancyToy相当复杂,因为它从Toy中继承,并实现了HasBatteries,Waterproof以及ShootsThings的接口。在main()方法中 有一个Class对象,它通过Class.forName()初始化成FancyToy Class。Class.getInterfaces方法会返回Class对象的一个数组,用于表示Class对象内的接口。 若有一个Class对象,也可以用getSuperclass()查询该对象的直接父类是什么。当然,这种做会返回一个Class对象,可用它作进一步的查询。 这意味着在运行期的时候,完全有机会调查到对象的完整层次结构。

若从表面看,Class的newInstance()方法似乎是克隆(clone())一个对象的另一种手段。但两者是有区别的。利用newInstance(), 我们可在没有现成对象供“克隆”的情况下新建一个对象。就像上面的程序演示的那样,当时没有Toy对象,只有cy——即Toy的Class对象。 利用它可以实现“虚拟构建器”。在上述例子中,cy只是一个Class对象,编译期间并不知道进一步的类型信息。一旦新建了一个实例后, 可以得到Object对象。但那是一个Toy对象。

用newInstance()创建的类必须有一个无参数的构造方法。没有办法用newInstance()创建拥有非默认构建方法的对象,如果我们注释掉Toy() {}构造方法, 将会报错:java.lang.InstantiationException。

参考阅读:

Thinking in Java中对RTTI的介绍

    原文作者:mxn原创
    原文地址: http://souly.cn/%E6%8A%80%E6%9C%AF%E5%8D%9A%E6%96%87/2016/04/23/java%E7%B1%BB%E5%9E%8B%E4%BF%A1%E6%81%AFRTTI/
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞