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

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

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

1、为何须要RTTI

咱们来看一个例子:数组

 

  这是一个典型的类层次结构图,基类位于顶部,派生类向下扩展。面向对象编程中的基本目的是:让代码只操纵对基类(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<Shape> shapes = Arrays.asList(new Circle(), new Triangle(), new Square());
        for (Shape shape : shapes) {
            shape.draw();
        }
    }
}

结果:dom

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

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

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

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

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

      5.可是例子中RTTI转型并不完全:Object被转型为Shape而不是具体的派生类型,这是由于List<Shape>提供的信息是保存的都是Shape类型。在编译时,由容器和泛型保证这点,在运行时,由类型转换操做来确保这点。code

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

2、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) 断定此  对象所表示的类或接口与指定的  参数所表示的类或接口是否相同,或是不是其超类或超接口ClassClass
public native boolean isInterface();判断是不是接口
public native boolean isArray();判断是不是数组
public native boolean isPrimitive();判断是不是基本类型
public boolean isAnnotation() 判断是不是注解
public boolean isSynthetic() 判断是不是同步代码块
public String getName() 返回带包的全名,包含类的类型信息(是引用类型 仍是各类基本类型)

public ClassLoader getClassLoader() 获取该类的类加载器。
public TypeVariable<Class<T>>[] getTypeParameters() 获取泛型
public native Class<? super T> 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<T> getConstructor(Class<?>... parameterTypes) 根据参数获取public构造方法
public Constructor<T> 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<Integer> intClass= int.class; 若是想放松限制,可使用通配符"?":Class<?> anyClass = AnyClass.class;若是想限定某类型的子类可使用extends 关键字:Class<? extends Number> numCLass = int.class; numClass = double.class;

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

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

相关文章
相关标签/搜索