Java——类型信息

0 什么是类型信息?什么是RTTI?

类型信息,即每一个对象或者数据的类型所包含的信息。比如字符串类String以及自己创建的某个类的类型。

运行时类型信息(RTTI,Run-Time Type Information),可以让程序员在程序运行的过程中发现和使用类型信息。在RTTI使用之前,我们必须在程序的编译期确定所有对象和数据的类型信息,因为一旦程序运行起来,就无法再实时、动态地获取类型信息。

Java实现RTTI的方式有两种:

  • 传统的RTTI——假定我们在编译期就已经预知程序运行过程中将会出现的所有类型,然后在编写程序的时候把所有的可能性都包括了;
  • 反射机制——提供了一种很棒的方式,让我们在程序运行的过程中实时地发现和使用类型信息。

1 为什么需要RTTI?

在Java中,所有的类型转换都是在运行时进行正确性检查的。

在编译期,类型之间的正确关系由容器和Java的泛型机制进行强制检查,而在运行时,则交由类型转换操作来保证。如何能在运行时完成类型的正确转换,保证类型之间的正确关系?那么就需要能够在运行时获取和使用类型信息,即RTTI。

之后的工作(调用什么方法,执行什么操作),就交给Java多态完成(动态绑定等)。

3 RTTI 与 多态

其实RTTI的存在,与多态的思想是有一定的冲突的。

作为面向对象编程的基本目标之一,多态希望做到大部分代码尽可能少地了解对象的具体类型,外部实现只需要与一个通用类型打交道即可,多态可以通过动态绑定等机制,将外部实现定向到具体某一个类的方法上,从而完成工作,而在这个过程中,在外部实现看来,它自己一直在与这个通用类型打交道而已。这样可以保持代码的易写易读特性,设计和维护起来都变得容易。

而RTTI则提供了可以获取对象类型信息的方式,这样一来,代码就可以在需要的时候实时地获取对象的类型信息。

所以,在两者的使用上,需要做到一个好的抉择。

  • Java作为面向对象编程的语言,要求我们在凡是可以使用多态的地方都使用多态机制,只在必需的地方使用RTTI获取对象的类型信息。
  • 由于多态机制下方法的调用,要求我们拥有基类定义的控制权,因为在扩展程序的时候,可能会发现基类并未定义相关的方法(这时候最方便的方式就是在基类中定义,派生类中继承实现)。如果基类定义的权限在他人手中,那么这时候RTTI是一个很好的解决方法:继承基类,生成一个新类,在新类中添加所需的方法,然后在代码的其他地方,通过检查特定的类型信息,从而调用新添加的方法。这么做可以在不破坏多态性和保持程序良好扩展能力的基础上,实现新的特性。
  • RTTI对于效率问题的解决——具体以后再说,参见profiler
  • RTTI实现方式之一——反射机制的使用,将会给程序带来更多动态的风格,而动态代码是将Java与C++等语言区分开来的重要工具之一。

4 Class对象

理解RTTI在Java中的工作原理,首先需要知道类型信息在运行时是如何表示的——由Class对象完成

每一个类都有一个类对象——Class对象,Class对象包含与类有关的信息,类中所有对象的创建都是通过该类的Class对象实现的。(为了生成Class对象,运行这个程序的JVM将使用被称为“类加载器”的子系统…以后看了《深入理解Java虚拟机》再说吧2333)

所有类都是在对其第一次使用的时候,动态加载到JVM中的。

当程序创建第一个对类的静态成员(包括成员属性和成员方法)的引用的时候,就会加载这个类。这证明,类的构造器也是类的静态方法,即使构造方法本身没有加”static”修饰符。因此,使用”new”创建类的对象的时候,即是对类的静态成员的引用。

因此,Java是一种动态加载的语言,在它开始运行之前并非被完全加载(即创建类的对象之前),其各个部分是在必需的时候才加载。

必需的时候到来时,类加载器将首先检查这个类的Class对象是否已经加载。若尚未加载,默认的类加载器将会根据类名查找.class文件(可能是数据库中的字节码)。若加载了Class对象,则将会对Class对象或者.class文件进行验证,确保没有损坏或者被破坏,且不包含不良的Java代码。然后,类的Class对象将会被载入内存,之后用于创建该类的所有对象。

无论如何,如果希望在程序运行的时候使用类型信息,那么就必须首先获得相应的Class对象的引用

4.1 Class.forName()

1
2
3
Class.forName("ClassName");
// Class类的静态方法,返回类名为"ClassName"的类的Class对象的引用
// 该方法有一个副作用,即若该类还没有加载,那就加载它

Class.forName()方法的便捷之处在于,不需要事先持有或者创建该类的对象,就可以获得Class对象的引用。但其缺点在于,该方法可能在编译期通过,但是在运行时出错,所以需要将它放在try语句块内。

此外,obj.getClass(),也可以用于获取Class对象的引用,只不过该方法需要事先持有该类的对象,通过类的对象来调用。

其他的Class对象可调用的方法

1
2
3
4
5
6
class.getInterfaces(); // 返回Class对象,该class对象中将包含相应的接口的信息
class.getName(); // 用于获取类对象对应的类的名称(或者接口名称)
class.getSimpleName(); // 获取不含包名的类的名称
class.getCanonicalName(); // 获取包含包名的类的名称(即 全限定名)
class.getSuperclass(); // 返回当前Class对象的父类,应该是一定有的,毕竟Object是所有类的父类
class.newInstance(); // 即创建某对象,该方法是实现“虚拟构造器”的途径之一,可以在不知道所要创建对象的确切类型的前提下,通过Class对象的引用创建一个对象,但调用该方法对应的类需要有默认的构造器

4.2 类字面常量

int.class这样。类字面常量不仅简单,而且更安全,因为在编译期时就会受到检查(意味着不需要放在try语句块中)。同时,类字面常量是一个常量,不需要调用方法,也因此更高效。

类字面常量不仅可以用于普通的类,也可以用于接口、数组以及基本数据类型。此外,对于基本类型的相应包装器类,有一个标准字段TYPETYPE字段是一个引用,指向包装器类对应的基本数据类型的Class对象,即int.class等价于Integer.TYPE,以此类推。

值得注意的是,类字面常量.class用于创建对Class对象的引用时,不会自动地初始化该Class对象,Class对象的初始化会推迟到对静态方法或者非常数静态域进行首次引用时才执行,即有效地实现了初始化尽可能的“惰性”

4.3 泛化的Class引用&通配符?

由于泛型类引用只能赋值为指向其声明的类型,所以使用泛型语法,可以让编译器在编译期强制执行额外的类型检查,更安全。这也是使用泛化的作用和原因。

1
2
3
4
Class class = int.class; // OK
class = double.class; // OK
Class<Integer> intClass = int.class; // OK
// intClass = double.class; // Illegal

但是,有时候我们又会嫌弃泛型语法限制了类型之间的转换,想要将限制放宽一点。这个时候,就需要使用通配符(Wildcard),即 ?,表示任何事物

1
2
Class<?> class = int.class; // OK
class = double.class; // OK

可以看到,Class<?>和平凡的Class功能上是等价的,但是意义上前者是优于后者的,因为使用通配符的意思是:你经过考虑之后,才放宽了泛型的限制,而不是你一时疏忽才放宽了限制。

经常使用的情况是:创建一个Class引用,该引用需要限定在一个类的范围内,即只能赋值为这个范围内的类的对象。

1
2
3
4
5
6
Class<? extends Number> class = int.class;
// OK, int.class = Integer.TYPE and Integer extends Number
class = double.class;
// OK, double.class = Double.TYPE and Double extends Number
// class = Pet.class; // Illegal
// 即 Number类 以及 任何Number类的派生类 的类对象都可以赋值给该引用

5 关键字 instanceof

除了传统的类型转换方式,即byte b = 2; int a = (int) b;,以及Java的向上/向下转型方式之外,涉及到类型转换之前的检查,还可以使用关键字instanceof,该关键字返回一个布尔值,说明某一个对象是不是某一个类的实例,以防在进行类型转换等操作的时候,产生ClassCastException的异常。

1
2
3
if(obj instanceof ClassName){
// Do Something
}

6 动态的 instanceof

Class类的class.isInstance(obj)方法,通过使用类对象调用,可以实现一种动态测试对象的途径,因为调用该方法的类对象和方法中的参数都可以使用变量,通过向变量传递相应的类对象的引用和实例的引用,可以动态地测试对象,而不像关键字instanceof,需要在编写的时候就需要写死。

7 instanceof 与 equals() /==的区别

instanceofisInstance()保持了类型的概念,即指的是“你是这个类么?或者你是这个类的派生类么?”

equals()== ,则比较“耿直”,它们比较的是实际的Class对象,不考虑继承关系。

8 设计模式——工厂模式

工厂模式,即将对象的创建工作交给类自己去完成,之后只需要多态调用相应的工厂方法即可获得相应的类的对象的引用。

1
public interface Factory<T> { T create(); }

创建一个基类接口,需要使用工厂方法创建对象的类,都可以通过实现该接口的方式,实现适合自己的create()方法。

使用泛型参数T,可以让create()方法在每一种Factory接口的实现中返回不同的类型,也即充分利用了Java的协变返回类型

关于设计模式的具体内容,会有Java设计模式详细介绍。

9 反射:运行时的类型信息

反射,提供了一种机制——检查某一个类的属性、可用的方法以及方法所需的参数类型等

即当你想知道该类有什么属性或者有什么方法,但是不想通过查阅文档的方式慢慢找的时候,就可以编写一段具有反射机制的代码帮助你快速解决这个问题。又或者你希望将灵活和动态注入你的代码,实现更高级的东西,比如动态代理反射机制实例化一个类的对象等。

其实,反射机制并没有什么神奇之处,重要的不是知道多少的类和方法,而是要理解其中的原理:当通过反射与一个未知类型的对象交互的时候,JVM只是简单地检查这个对象,判断该对象属于哪个特定的类,在用该对象做其他事情之前必须先加载对应类的Class对象,因此该类的.class文件对于JVM来说必须是可以获取的(要么在本地机器上,要么可以通过网络获取)。

因此,反射与RTTI之间真正的区别是:对于RTTI,编译器在编译时打开和检查.class文件对于反射机制,编译期无法获取.class文件,只有在运行时才会获取和检查.class文件

Class类与java.lang.reflect类库一起对反射的概念提供支持,其中包含有Field类Method类以及Constructor类(每个类都实现了Member接口)。上述的类比较特殊,因为这些类型的对象是由JVM在运行时创建的,用于表示未知类中对应的成员。

  • Field类,以及相应的类方法,用于获取某个类的所有属性以及相关信息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 取得本类的全部属性
    Field[] field = class.getDeclaredFields();
    // 取得实现的接口或者父类的属性
    Field[] filed1 = class.getFields();
    // 权限修饰符
    int mo = field[i].getModifiers();
    String priv = Modifier.toString(mo);
    // 属性类型
    Class<?> type = field[i].getType();
  • Method类,以及相应的类方法,用于获取某个类的所有方法以及相关信息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 取得类的所有方法
    Method method[] = class.getMethods();
    // 获取方法的返回类型
    Class<?> returnType = method[i].getReturnType();
    // 获取方法的参数类型(可能是多个参数,所以用数组存储)
    Class<?> para[] = method[i].getParameterTypes();
    // 方法的权限修饰符
    int temp = method[i].getModifiers();
    // 方法的抛出异常类型,如果有的话
    Class<?> exce[] = method[i].getExceptionTypes();
    // 有了该类的方法名以及方法所需的参数类型,其实就可以不通过使用该类实际对象的方式调用该类方法,即实现动态代理,见后详细阐述
  • Constructor类,以及相应的方法,用于获取某个类的构造方法以及相关信息:

    1
    2
    3
    4
    5
    6
    // 获取类的构造方法,因为可能会有多个构造函数
    Constructor<?> cons[] = class.getConstructors();
    // 获取构造方法的参数类型
    Class<?> paras[] = cons[i].getParameterTypes();
    // 有了构造方法名以及相应的参数类型,其实就可以创建该类的对象了,即动态创建该类的对象
    cons[i].newInstance(2,"John");

10 设计模式——代理与动态代理

代理,即一个中间角色,用来代替“实际”对象的对象,通过代理可以调用“实际”对象对应类的方法,在这个过程中实现额外或者不同的操作。

什么时候使用代理呢?只要你想借助某一个类的方法实现额外的操作或者实现不同的操作,但是又希望保持原有类的封装独立性,不想将额外操作的代码添加到原有类的代码中(核心代码不变,只是借用一下方法,然后在其上添加点东西)。

动态代理,即可以动态地创建代理并动态地处理对所代理方法的调用。

在动态代理上所做的所有操作,都会被重定向到单一的调用处理器上。

创建动态代理,使用静态方法:

1
Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces, InvocationHandler handler);

其中参数的含义为:

  • ClassLoader loader:类加载器,一般可以由已经被加载的对象获取类对象,然后由类对象获取类加载器,即obj.getClass().getClassLoader(),或者ClassName.class.getClassLoader()

  • Class<?>[] interfaces:希望该代理实现的接口列表,注意这里不是类或者抽象类,是接口列表;

  • InvocationHandler handler:即InvocationHandler接口的一个实现,也即一个调用处理器,动态代理会将所有调用重定向到实现了接口的调用处理器上,所以通常会向调用处理器的构造器传递一个”实际“对象的引用,从而使得调用处理器在执行代理服务的时候,可以将请求转发,实现调用”实际“对象的类的方法。

    同时,在实现了InvocationHandler接口的调用处理器类中,除了构造方法,还有一个invoke()方法(静态方法写在调用处理器里也是可以的):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class MyInvocationHandler implememts InvocationHandler {
    private Object obj = null;
    public Object proxyBind(Object obj) {
    this.obj = obj;
    return Proxy.newProxyInstance(obj.getClass().getClassLoader(),obj.getClass().getInterfaces(),this);
    }
    public Object invoke(Object proxy, Method method, Object[] args) {
    // Do Something
    return method.invoke(this.obj,args);
    }
    }
    ......
    public static void main(String[] args) throws Exception {
    MyInvocationHandler demo = new MyInvocationHandler();
    // 向动态代理的静态方法传递需要代理的对象,作为参数,返回值是代理
    ClassX objX = (ClassX) demo.proxyBind(new ClassX());
    // 将返回值——代理对象向下转型,赋值给所代理的对象的类的一个对象引用,然后用该对象引用调用该类的方法,在这个过程中,会调用调用处理器类中的 invoke() 方法,先完成 // Do Something中的代码块内容,最后再 method.invoke() 之处才会调用被代理对象的类中的方法
    objX.ClassXMethod(param1,param2,...);
    }

11 空对象

一般来说,我们都会使用null表示对象为空。虽然这种处理方式简单,但是缺点是:每次使用对象的引用之前都需要检测它是否为null,而且null除了在产生异常NullPointerException之外,没有其他任何作用。

空对象,所包含的内容和功能则更为丰富。

  • 接受传递给它的 它之后所要代表的对象的 信息,但是空对象将会返回一个“空值”——表示实际上并不存在任何“真实”对象的值,即吸收了相应的信息,但对外显示为“空”;

  • 空对象更接近于数据,因为空对象吸收了所要代表的对象的信息,表示的是一个实体,尽管它对外显示是“空”;

  • 即使空对象可以响应“实际”对象所能响应的所有消息,但是仍需要使用某种方式测试其是否为空,最简单的方式是创建一个标记接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public interface Null {}
    // 然后在类和标记接口的基础上,创建一个空对象的类
    Class Person {...}
    public static class NullPerson extends Person implements Null {
    private NullPerson() { super(...); } // 写上你所想的,对于“空”的定义
    public String toString() { return "NullPerson"; }
    ...
    }
    // 创建一个空对象
    public static final Person NULL = new NullPerson();

空对象,一般都是单例,即一个类中只有一个,所以一般需要用static final修饰,即让空对象是不可变的,只能在构造器中初始化它的值,然后读取这些值。

空对象,也是对象,是一个类的静态对象,可以将其赋值给对应类的引用。同时,也可以使用关键字instanceof以及动态 instanceof 探测一个对象的引用是不是空对象所属的类,还可以使用equals()或者==来与Person.NULL比较。

利用Person.NULL的好处:可以将Person类的空信息全都放在空对象中,作为一个整体,在引用它的类中涉及到Person类对象信息为空的情况,只需要一个Person.NULL空对象既可以表示,至于空对象中包含哪些信息,只有Person类中知道(如果是这样,那相应的,添加信息的方法就需要在Person类中实现了…要想好)

附:

1 为使用一个类,做的准备工作有三个步骤:

(1)加载,由类加载器执行,查找.class文件关于类的字节码,并从字节码中创建一个Class对象;

(2)链接,验证类中的字节码,为静态域分配存储空间,必需的时候将会解析这个类创建的对其他类的所有引用;

(3)初始化,若该类具有父类,则对其初始化,执行静态初始化器和静态初始化块。

若一个static final值是“编译期常量”,那么这个值不需要对类进行初始化就可以读取,否则需要先完成上述三个步骤才能读取。

2 class.newInstance()方法

使用一个Class对象的引用调用该方法,可以生成Class对象对应的类的新实例。

3 用于分类标识的类

并非所有在继承结构中的类都应该被实例化,有的类只是用作分类标识,这部分类不需要被实例化。

4 极限编程(XP)原则——YAGNI

You Aren’t Going to Need It.——即“做可以工作的最简单的事情”。

5 空对象的逻辑变体——模拟对象和桩

与空对象一样,他们都表示在最终程序中所使用的“实际”对象,但是后两者都只是“假扮”对象,空对象则是一个真的对象。

模拟对象是轻量级和自测试的,桩则是重量级可以反复使用的。

6 反射与private

反射机制,可以通过属性名访问到最私有的private域,也可以通过方法名和方法参数访问到私有的private方法。

貌似只要 final 修饰的常量在遭遇访问和修改的时候是安全的:final 修饰的常量只会在编译时初始化一次,运行时系统会在不抛出任何异常的情况下接受任何的修改尝试,但是实际上常量的值不会发生任何的变化。