Java笔记 – RTTI,Class,泛化的Class引用
本文由发表于4年前 | J2EE | 暂无评论 |  被围观 3,624 views+
1、RTTI:2、Class对象:2.1、加载类的时机:2.2、Class相关方法,newInstance()2.3、类字面常量:2.4、使用.class与使用getName()方法创建对象引用的区别:2.5、下面是判断是否执行初始化的一些情况:2.6、泛化的Class引用:2.6.1、使用通配符?放松泛型的限定:2.6.2、泛型下的newInstance()方法:2.6.3、新的转型语法:cast()方法
1、RTTI:

运行时类型信息可以让你在程序运行时发现和使用类型信息。

在Java中运行时识别对象和类的信息有两种方式:传统的RTTI,以及反射。下面就来说下RTTI。

RTTI:在运行时,识别一个对象的类型。但是这个类型在编译时必须已知。

下面通过一个例子来看下RTTI的使用。这里涉及到了多态的概念:让代码只操作基类的引用,而实际上调用具体的子类的方法,通常会创建一个具体的对象(Circle,Square,或者Triangle,见下例),把它向上转型为Shape(忽略了对象的具体类型),并在后面的程序中使用匿名(即不知道具体类型)的Shape引用:

20131220-type--1

abstract class Shape {
    // this 调用当前类的toString()方法,返回实际的内容
    void draw(){ System.out.println(this + "draw()"); }
    // 声明 toString()为abstract类型,强制集成在重写该方法
    abstract public String toString();
}

class Circle extends Shape {
    public String toString(){ return "Circle"; }
}

class Square extends Shape {
    public String toString(){ return "Square"; }
}

class Triangle extends Shape {
    public String toString(){ return "Triangle"; }
}

public static void main(String[] args){
    // 把Shape对象放入List<Shape>的数组的时候会向上转型为Shape,从而丢失了具体的类型信息
    List<Shape> shapeList = Arrays.asList(new Circle(), new Square(), new Triangle());
    // 从数组中取出时,这种容器,实际上所有的元素都当成Object持有,会自动将结果转型为Shape,这就是RTTI的基本的使用。
    for(Shape shape : shapeList){
        shape.draw();
    }
}

输出结果为:

Circledraw()
Squaredraw()
Triangledraw()

存入数组的时候,会自动向上转型为Shape,丢失了具体的类型,当从数组中取出的时候,(List容器将所有的事物都当做Object持有),会自动将结果转型回Shape,这就是RTTI的基本用法。Java中所有的类型转换都是在运行时进行正确性检查的,也就是RTTI:在运行时,识别一个对象的类型。

上面的转型并不彻底,数组的元素取出时又Object转型为Shape,而不是具体的类型,编译时这是由容器和Java泛型系统来确保这一点的,而在运行时时有类型转换操作来确保这一点的。

而能够通过Shape对象执行到子类的具体代码就是又多态来决定的了,具体看Shape引用所指向的具体对象。

另外,使用RTTI,可以查询某个Shape引用所指向的对象的确切类型,然后选择性的执行子类的方法。

2、Class对象:

要了解RTTI在Java中的工作原理,必须知道类型信息在运行时是如何表示的,这里是由Class这个特殊对象完成的。

Class对象是用来创建类的所有的“常规”对象的。Java使用Class对象来执行其RTTI。

每当编译一个新类,就会产生一个Class对象(.class文件)。运行这个程序的JVM将使用“类加载器”这个子系统。

类加载器子系统:包含一条类加载器链,但只有一个原生类加载器,它是JVM实现的一部分。原生类加载器加载可信类,包括Java API类,通常是从本地磁盘加载的。当需要以某种特定的方式加载类,以支持Web服务器应用,可以挂接额外的类加载器。

2.1、加载类的时机:

当程序创建第一个对类的静态成员的引用时,就会加载这个类。这证明其实构造器也是类的静态方法,当使用new操作符创建类的新对象也会当做对类的静态成员的引用。

可见Java程序时动态加载的,按需加载。需要用到Class时,类加载器首先会检查这个类的Class对象是否已经加载,如果尚未加载,默认的类加载器就会根据类名查找到.class文件。接下来是验证阶段:加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良Java代码。

2.2、Class相关方法,newInstance()

下面通过一个例子演示Class对象的加载:

class A {
    // 静态代码库,在第一次被加载时执行,通过打印信息知道该类什么时候被加载
    static { System.out.println("Loading A"); }
}
class B {
    static { System.out.println("Loading B"); }
}
class C {
    static { System.out.println("Loading C"); }
}
public class Load {
    public static void main(String[] args){
        System.out.println("execute main...");
        new A();
        System.out.println("after new A");
        try {
            Class.forName("com.itzhai.test.type.B");
        } catch (ClassNotFoundException e) {
            System.out.println("cloud not find class B");
        }
        System.out.println("after Class.forName B");
        new C();
        System.out.println("after new C");
    }
}

输出结果为:

execute main...
Loading A
after new A
Loading B
after Class.forName B
Loading C
after new C

可见,Class对象在需要的时候才被加载,注意到这里的Class.forName()方法:

forName()方法是取得Class对象的引用的一种方法,通过这个方法获得恰当的Class对象的引用,就可以在运行时使用类型信息了。

如果你已经有了一个感兴趣的类型的对象,则可以通过跟类Object提供的getClass()方法来获得Class引用。

下面是一段Class的使用的代码:

interface X{}
interface Y{}
interface Z{}
class Letter {
    Letter(){};
    Letter(int i){};
}
class NewLetter extends Letter implements X, Y, Z{
    NewLetter(){ super(1); };
}
public class ClassTest {

    /**
     * 打印类型信息
     * @param c
     */
    static void printInfo(Class c){
        // getName()获得全限定的类名
        System.out.println("Class name: " + c.getName() + " is interface? " + c.isInterface());
        // 获得不包含包名的类名
        System.out.println("Simple name: " + c.getSimpleName());
        // 获得全限定类名
        System.out.println("Canonical name: " + c.getCanonicalName());
    }

    public static void main(String[] args){
        Class c = null;
        try {
            // 获得Class引用
            c = Class.forName("com.itzhai.test.type.NewLetter");
        } catch (ClassNotFoundException e) {
            System.out.println("Can not find com.itzhai.test.type.NewLetter");
            System.exit(1);
        }
        // 打印接口类型信息
        for(Class face : c.getInterfaces()){
            printInfo(face);
        }
        // 获取超类Class引用
        Class up = c.getSuperclass();
        Object obj = null;
        try {
            // 通过newInstance()方法创建Class的实例
            obj = up.newInstance();
        } catch (InstantiationException e) {
            System.out.println("Can not instantiate");
        } catch (IllegalAccessException e) {
            System.out.println("Can not access");
        }
        // 打印超类类型信息
        printInfo(obj.getClass());
    }
}

输出为:

Class name: com.itzhai.test.type.X is interface? true
Simple name: X
Canonical name: com.itzhai.test.type.X
Class name: com.itzhai.test.type.Y is interface? true
Simple name: Y
Canonical name: com.itzhai.test.type.Y
Class name: com.itzhai.test.type.Z is interface? true
Simple name: Z
Canonical name: com.itzhai.test.type.Z
Class name: com.itzhai.test.type.Letter is interface? false
Simple name: Letter
Canonical name: com.itzhai.test.type.Letter

注意,在传递给forName()的字符串必须使用全限定名(包括包名)。

通过printInfo里面使用到的方法,你可以在运行时发现一个对象完整的类继承结构。

通过使用Class的newInstance()方法是实现“虚拟构造器”的一种途径,用来创建Class的实例,得到的是Object引用,但是引用时指向Letter对象。使用newInstance()来创建的类,必须带有默认的构造器。(而通过反射API,可以用任意的构造器来动态的创建类的对象)。

2.3、类字面常量:

除了使用getName()方法,Java还提供了另一种方法来生成对Class对象的引用,即使用类字面常量:

NewLetter.class;

此方法简单安全,编译时就受到检查,更高效。不仅可用于普通类,也可以用于接口,数组以及基本数据类型。另外,对于基本数据类型的包装器类,还有一个标准字段TYPE,TYPE字段是一个引用,执行对应的基本数据类型的Class对象。为了统一,建议都使用.class这种形式。

2.4、使用.class与使用getName()方法创建对象引用的区别:

使用.class创建时,不会自动的初始化Class对象。创建步骤如下:

1、加载 由类加载器执行:查找字节码(通常是在classpath指定的路径中查找,但并非必须的),然后从这些字节码中创建一个Class对象。

2、链接 将验证类中的字节码,为静态域分配存储空间,如果需要,将会解析这个类创建的对其他类的所有的引用。

3、初始化 如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。

初始化被延迟到了对静态方法(构造器隐式的是静态的)或者非常数静态域进行首次引用时才执行的:

class Data1{
    static final int a = 1;
    static final double b = Math.random();
    static {
        System.out.println("init Data1...");
    }
}

class Data2{
    static int a = 12;
    static {
        System.out.println("init Data2...");
    }
}

class Data3{
    static int a = 23;
    static {
        System.out.println("init Data3...");
    }
}

public class ClassTest2 {
    public static void main(String[] args){
        System.out.println("Data1.class: ");
        Class data1 = Data1.class;
        System.out.println(Data1.a);  // 没有初始化Data1
        System.out.println(Data1.b);  // 初始化了Data1
        System.out.println(Data2.a);  // 初始化了Data2
        try {
            Class data3 = Class.forName("com.itzhai.test.type.Data3");  // 初始化了Data3
        } catch (ClassNotFoundException e) {
            System.out.println("can not found com.itzhai.test.type.Data3...");
        }
        System.out.println(Data3.a);
    }
}

输出的结果为:

Data1.class: 
1
init Data1...
0.26771085109184534
init Data2...
12
init Data3...
23

初始化有效的实现了尽可能的“惰性”。

2.5、下面是判断是否执行初始化的一些情况:

1、.class语法获得对类的引用不会引发初始化;

2、Class.forName()产生了Class引用,立即进行了初始化;

3、如果一个static final值是“编译器常量”,那么这个值不需要对类进行初始化就可以被读取;

4、如果只是把一个域设置为static final还不足以确保这种行为,例如上面的:

static final double b = Math.random();

5、如果一个static域bushifinal的,那么在对它访问时,总是要先进性链接和初始化;

2.6、泛化的Class引用:

Class引用表示的是它所指向的对象的确切类型,而该对象便是Class类的一个对象。在JavaSE5中,可以通过泛型对Class引用所指向的Class对象进行限定,并且可以让编译器强制执行额外的类型检查:

Class intCls = int.class;
// 使用泛型限定Class指向的引用
Class<Integer> genIntCls = int.class;
// 没有使用泛型的Clas可以重新赋值为指向任何其他的Class对象
intCls = double.class;
// 下面的编译会出错
// genIntCls = double.class;
2.6.1、使用通配符?放松泛型的限定:
Class<?> intCls = int.class;
intCls = String.class;

在JavaSE5中,Class<?>优于平凡的Class,更建议使用Class<?>,即便它们是等价的,因为Class<?>的好处是它表示你并非是碰巧或者疏忽,而是使用了一个非具体的类引用。

为了限定Class的引用为某种类型,或者该类型的子类型可以将通配符与extends一起使用,创建一个范围:

Class<? extends Number> num = int.class;
// num的引用范围为Number及其子类,所以可以按照如下赋值
num = double.class;
num = Number.class;
2.6.2、泛型下的newInstance()方法:

使用了泛型后的Class,调用newInstance()返回的对象是确切类型的,但是当你使用getSuperclass()获取泛型对应的超类的时候真正的类型会有一些限制:编译器在编译期就知道了超类的类型,但是,通过这个获取到的超类引用的newInstance()方法返回的不是精确类型,而是Object:

Dog dog = dogCls.newInstance();
abstract class Animal {
}
class Dog extends Animal{
}

// 下面的写法是错误的,只能返回 Class<? super Dog>类型
// Class<Animal> animalCls = dogCls.getSuperclass(); 
Class<? super Dog> animalCls = dogCls.getSuperclass();
// 通过获取的超类引用,只能创建返回Object类型的对象
Object obj = animalCls.newInstance();
2.6.3、新的转型语法:cast()方法

直接看下代码:

Animal animal = new Dog();
Class<Dog> dogCls = Dog.class;
Dog dog = dogCls.cast(animal);
// 或者直接使用下面的转型方法
dog = (Dog)animal;

可以发现,使用cast()方法的做了额外的工作,这种转换方法可以用在一下的情况中:在编写泛型带的时候,如果存储了Class引用,并希望通过这个Class引用来执行转型,就可以使用cast()方法。

除了文章中有特别说明,均为IT宅原创文章,转载请以链接形式注明出处。
本文链接:http://www.itzhai.com/java-notes-rtti-class-generalization-of-class-references.html
关键字: , , ,
arthinking Java技术交流群:280755654,入门群:428693174 more
分享到:
 
2013 12/20
如果您有更好的原创技术博文或者观点,欢迎投稿:admin@itzhai.com,或者关注订阅左侧浮动面板的微信号订阅IT宅itread)发送消息。
文章评论
    没有评论
给我留言

有人回复时邮件通知我
J2EE的相关文章
随机文章 本月热门 热评
1 Hibernate多对多单向关联和双向关联映射的方法 2011/9/29
2 Javascript Web Application笔记之MVC和类 – 类的创建 类库的封装与JSON方式添加函数 2012/6/18
3 转角处的音乐梦想家 2012/7/4
4 《從0到1》笔记 2015/8/19
5 碰撞球(连连看)游戏 连连看匹配算法分析与实现思路 2012/4/10
6 虚拟机装不了系统,镜像文件用不了?很可能是分区工具的问题 2011/5/2
友情推荐 更多
破博客 文官洗碗安天下,武将打怪定乾坤。多么美好的年代,思之令人泪落。
Mr.5's Life 白天是一名程序员,晚上就是个有抱负的探索者
行知-追寻技术之美 关注大数据,分布式系统
我爱编程 编程成长轨迹
Cynthia's Blog 学习笔记 知识总结 思考感悟
 
欢迎关注我的公众号 IT宅
关于IT宅 文章归档

IT宅中的文章除了标题注明转载或有特别说明的文章,均为IT宅的技术知识总结,学习笔记或随笔。如果喜欢,请使用文章下面提供的分享组件。转载请注明出处并加入文章的原链接。 感谢大家的支持。

联系我们:admin@itzhai.com

Theme by arthinking. Copyright © 2011-2015 IT宅.com 保留所有权利.