Java编程思想——第14章 类型信息(一)RTTI

运行时类型信息使得你可以在程序运行时发现和使用类型信息。Java是如何让我们在运行时识别对象和类的信息得呢?

主要有两种方式:1.传统RTTI,他假定我们在编译期间已经知道了所有类型;2.反射,它允许我们在运行时发现和使用类的信息。

一、为什么需要RTTI

我们来看一个例子:

Java编程思想——第14章 类型信息(一)RTTI_第1张图片

 

  这是一个典型的类层次结构图,基类位于顶部,派生类向下扩展。面向对象编程中的基本目的是:让代码只操纵对基类(Shape)的引用。这样,如果添加一个新类(比如从Shape派生的Rhomboid)来扩展程序就不会影响原来代码了。这个例子中Shape接口动态绑定了draw()方法,目的就是让客户端程序员用泛化的Shape引用来调用draw()。draw()在所有派生类里都会被覆盖,并且由于它是被动态绑定的,所以即使是通过泛化的Shape引用来调用,也能产生正确的行为。这就是多态。

 

abstract class Shape {
    void draw() {
        System.out.println(this + ".draw()");
    }

    @Override
    abstract public String toString();
}

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

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

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

public class Shapes {
    public static void main(String[] args) {
        List shapes = Arrays.asList(new Circle(), new Triangle(), new Square());
        for (Shape shape : shapes) {
            shape.draw();
        }
    }
}

结果:

Circle.draw()
Triangle.draw()
Square.draw()

分析:1.toString()被声明为abstract,以此强制继承者覆写该方法,并且防止对无格式的Shape的实例化。

      2.如果某个对象出现在字符串表达式中(比如“+”,字符串对象的表达式),toString()会自动被调用,以生成该对象的String。

      3.当Shape派生类的对象放入List的数组时会向上转型,但是当向上转型为Shape时也丢失了Shape对象的具体类型。对数组而言,他们都只是Shape类的对象。

      4.当从数组中取出元素时(这种容器把所有的事物都当作Object持有),将结果转型会Shape。这是RTTI的最基本使用形式,因为在Java中所有的类型转换都是在运行时进行正确性检查的。这也是RTTI名字的含义:在运行时,识别一个对象的类型。

      5.但是例子中RTTI转型并不彻底:Object被转型为Shape而不是具体的派生类型,这是因为List提供的信息是保存的都是Shape类型。在编译时,由容器和泛型保证这点,在运行时,由类型转换操作来确保这点。

      6.Shape对象具体执行什么,是由引用所指向的具体对象(派生类对象)决定的,而现实中大部分人正是希望尽可能少的了解对象的具体类型,而只是和家族中一个通用的类打交道,如:Shape,使得代码更易读写,设计更好实现,理解和改变,所以“多态”是面向对象编程的基本目标。

二、Class对象

  使用RTTI,可以查询某个Shape引用所指向的对象的确切类型,然后选择或者剔除某些特性。要理解RTTI在Java中的工作原理,首先应该知道类型信息在运行时是如何表示的。Class对象承担了这项工作,Class对象包含了与类有关的信息。Class对象就是用来创建所有对象的,Java使用Class对象来执行其RTTI,Class类除了执行转型操作外,还有拥有大量的使用RTTI的其他方式。

  每当编写并编译一个新类,就会生成一个Class对象,被存在同名的.class文件中。运行这个程序的Java 虚拟机(JVM)使用被称为“类加载器”的子系统,生成这个类的对象。类加载器子系统实际上可以包含以挑类加载链,但是只有一个原生类加载器,它是JVM实现的一部分。原生类加载器加载的通常是加载本地盘的内容,这条链通常不需要添加额外的类加载器,但是如果有特殊需求比如:支持Web服务器应用,那么就要挂接额外的类加载器。

  所有的类在被第一次使用时,动态加载到JVM中。当第一个对类的静态成员的引用被创建时,就会加载这个类。这也证明构造器是静态方法,使用new操作符创建类的新对象也会被当作对类的静态成员的引用。因此,Java程序在开始运行之前并未完全加载,各个部分在必须时才会加载。类加载器首先检查这个类的Class对象是否已加载,如果尚未加载,默认的类加载器就会根据类名查找.class文件。

  一旦某个类的Class对象被加载入内存,它就被用来创建这个类的所有对象:

class Candy {
    static {
        System.out.println("Loading Candy");
    }
}

class Gum {
    Gum() {
        System.out.println("Constructed Gum");
    }

    static {
        System.out.println("Loading Gum");
    }
}

public class SweetShop {
    public static void main(String[] args) {
        System.out.println("inside main");
        new Candy();
        try {
            Class.forName("Gum");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        new Gum();
        new Candy(); 
    }
}
结果:
Inside main
Loading Candy
Loading Gum
Constructed Gum

  Class.forName()会让类初始化:每个类都有一个static子句,该子句在类被第一次加载时执行,注意第二次加载时候就不走了。

  Class.forName("");这个方法是Class类的一个static成员。Class对象就和其他对象一样 我们可以获取并操作它的引用(这也就是类加载器的工作)。forName()是取得Class对象引用的一种方法,Class clazz = Class.forName("xxx"); xxx必须是带包名的全名!!! 如果你已经得到了一个类的引用xxx,还可以通过:Class clazz = xxx.getClass();方式获取class对象。Class包含很多方法 其中比较常用的有:

public T newInstance() 得到一个实例 相当于new 
public native boolean isInstance(Object obj) 是否是参数类的一个实例
public native boolean isAssignableFrom(Class cls) 判定此 Class 对象所表示的类或接口与指定的 Class 参数所表示的类或接口是否相同,或是否是其超类或超接口
public native boolean isInterface();判断是否是接口
public native boolean isArray();判断是否是数组
public native boolean isPrimitive();判断是否是基本类型
public boolean isAnnotation() 判断是否是注解
public boolean isSynthetic() 判断是否是同步代码块
public String getName() 返回带包的全名,包含类的类型信息(是引用类型 还是各种基本类型)

Java编程思想——第14章 类型信息(一)RTTI_第2张图片

public ClassLoader getClassLoader() 获取该类的类加载器。
public TypeVariable>[] getTypeParameters() 获取泛型
public native Class getSuperclass(); 获取父类和父类泛型
public Package getPackage() 获取包名
public Class[] getInterfaces() 获取该类实现的接口列表
public Type[] getGenericInterfaces() 获取实现的接口列表及泛型
public native Class getComponentType() 返回表示数组组件类型的 Class。如果此类不表示数组类,则此方法返回 null。如果此类是数组,则返回表示此类组件类型的 Class
public native int getModifiers() 返回类的修饰符
public native Object[] getSigners();// 我目前还不知道是干什么的如果有明确用处请评论告诉我 万分感谢
native void setSigners(Object[] signers)
public Method getEnclosingMethod() 获取局部或匿名内部类在定义时所在的方法
public String getSimpleName() 获取最简单的那个类名
public String getCanonicalName() 带有包名的类名
public String getTypeName() 如果是数组的话[类名] 否则和getSimpleName一样
public Field[] getFields() 获取类中public的字段
public Field[] getDeclaredFields() 获取类中所有字段
public Class[] getClasses() 获取该类以及父类所有的public的内部类
public Class[] getDeclaredClasses() 获取该类所有内部类,不包含父类的
public Method[] getMethods() 获取该类及父类所有的public的方法
public Method[] getDeclaredMethods() 获取该类中所有方法 ,不包含父类
public Constructor[] getConstructors() 获取所有public的构造方法
public Constructor[] getDeclaredConstructors() 获取该类所有构造方法
public Field getField(String name) 根据字段名public获取字段
public Field getDeclaredField(String name) 根据字段名所有获取字段
public Method getMethod(String name, Class... parameterTypes) 根据方法名和参数获取public方法
public Method getDeclaredMethod(String name, Class... parameterTypes) 根据方法名和参数获取所有方法
public Constructor getConstructor(Class... parameterTypes) 根据参数获取public构造方法
public Constructor getDeclaredConstructor(Class... parameterTypes) 根据参数获取所有构造反方法、
...  如遗漏重要方法 请提醒 谢谢~

类的字面常量

  Java还有另一种方式生成对Class的引用,即使用字面常量:ClassDemo.class;  这种方式 简单 高效(因为不用初始化类),且这种方式同样适用于接口,数组,基本类型。对于基本类型的包装类,还有一个标准字段TYPE:

        boolean.class = Boolean.TYPE
        char.class = Character.TYPE
        byte.class = Byte.TYPE
        short.class = Short.TYPE
        int.class = Integer.TYPE
        long.class = Long.TYPE
        float.class = Float.TYPE
        double.class = Double.TYPE
        void.class = Void.TYPE

  使用这种方式创建对象的引用时,不会自动初始化该Class对象,为了使用类而做的准备工作实际包含三个步骤:

1.加载,由类加载器进行。该步骤将查找字节码,并从这些字节码中创建一个Class对象。

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

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

  初始化在对静态方法(构造器是隐式地静态)或非常数静态域进行首次引用时才会执行:

class InitableA {
    static final int staticFinalA = 47;
    static final int staticFinalB = ClassInitialization.random.nextInt(1000);
    static {
        System.out.println("Initializing InitableA");
    }
}

class InitableB {
    static int staticNoFinal = 147;
    static {
        System.out.println("Initializing InitableB");
    }
}

public class ClassInitialization {
    public static Random random = new Random(47);
    public static void main(String[] args) {
        System.out.println(InitableA.staticFinalA);
        System.out.println(InitableA.staticFinalB);
        System.out.println(InitableB.staticNoFinal);
    }
}
结果:
1 Initializing InitableA 258 Initializing InitableB 147

  如果一个static final 是"编译期常量" ,就像 staticFinalA 那么需要对它的类进行初始化就可以读取,但是 staticFinalB 却会使类初始化,因为它不是编译期常量;只有static修饰符的  staticNoFinal 在对它读取之前,要先进行链接,为这个域分配存储空间并且初始化该存储空间。

泛型化的Class引用

  通过使用泛型语法,可以让编译器强制执行额外的类型检查:Class intClass= int.class; 如果想放松限制,可以使用通配符"?":Class anyClass = AnyClass.class;如果想限定某类型的子类可以使用extends 关键字:Class numCLass = int.class; numClass = double.class;

  当使用泛型语法Class用于创建Class对象时,newInstance()将返回该对象得确切信息而不只是Object。

  如果你手头是Toy的父类ToyFather,toyClass.getSuperclass()方法却只能返回 Class 而不是确切的ToyFather ,并且在newInstance()时返回Object。

三、类型转换前先做检查

  迄今为止我们已知RTTI形式包含:

1)传统的类型转换,“(Shape)” ,由RTTI确保类型转换的正确性,检查未通过会报ClassCastException.

2)代表对象的类型的Class对象,通过查询Class对象可以获得运行时所需的信息。

  向上转型或向下转型是根据类层次结构图排列顺序定义的(父类在上子类在下);由于Circle一定是一个Shape所以向上转型不用有任何转型操作,而向下转型编译器则无法知道Shape以外的任何具体信息,只能使用显示类型转换,以告知编译器额外的信息。

3)instanceof  它可以判断对象是不是某个特定类型的实例。

四、注册工厂

  工厂方法设计模式,将对象的创建工作交给类自己完成。工厂方法可以被多态地调用,从而为你创建恰当类型的对象。简易版本:

public interface FactoryDemo {
    T create();
}

create返回的就是对应的对象.

五、instanceof 与 Class的等价性

  在查询类型信息时,以instanceof的形式(即使用instanceof的形式或isInstance()的形式)与直接比较Class对象有一个比较重要的差别:

instanceof:保持了类型的概念,指的是:“你是这个类或这个类的派生类么”。

而 == 或 equals() 比较的只是Class对象,没有考虑继承。

你可能感兴趣的:(Java编程思想——第14章 类型信息(一)RTTI)