Published on

Java编程思想读书笔记(二)第14章-类型信息

Authors
  • avatar
    Name
    Leon
    Twitter

1. RTTI(Runtime Type Identification)运行阶段类型识别

1.1 用途:

  为了确定基类指针实际指向的子类的具体类型。——《C++ Primer Plus》

1.2 工作原理:

  通过类型转换运算符回答“是否可以安全地将对象的地址赋给特定类型的指针”这样的问题。——《C++ Primer Plus》

1.3 Java中

  在Java中,所有的类型转换都是在运行时进行正确性检查的。这也是RTTI的含义:在运行时,识别一个对象的类型。

1.3.1 丢失具体类型信息的问题
  • 多态中表现的类型转换是RTTI最基本的使用形式,但这种转换并不彻底。如数组容器实际上将所有元素当作Object持有,取用时再自动将结果转型回声明类型。而数组在填充(持有)对象时,具体类型可能是声明类型的子类,这样放到数组里就会向上转型为声明类型,持有的对象就丢失了具体类型。而取用时将由Object只转型回声明类型,并不是具体的子类类型,所以这种转型并不彻底
  • 多态中表现了具体类型的行为,但那只是“多态机制”的事情,是由引用所指向的具体对象而决定的,并不等价于在运行时识别具体类型
    以上揭示了一个问题就是具体类型信息的丢失!有了问题,就要解决问题,这就是RTTI的需要即在运行时确定对象的具体类型
1.3.2 证实具体类型信息的丢失

  以下示例证实了上面描述的问题(具体类型信息的丢失):

package net.mrliuli.rtti;

import java.util.Arrays;
import java.util.List;

/**
 * Created by leon on 2017/12/3.
 */

abstract class Shape{
    void draw(){
        System.out.println(this + ".draw()");
    }
    abstract public String toString();  //要求子类需要实现 toString()
}

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

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> shapeList = Arrays.asList(
                new Circle(), new Square(), new Triangle()  // 向上转型为 Shape,此处会丢失原来的具体类型信息!!对于数组而言,它们只是Shape类对象!
        );
        for(Shape shape : shapeList){
            shape.draw();   // 数组实际上将所有事物都当作Object持有,在取用时会自动将结果转型回声明类型即Shape。
        }
        //shapeList.get(0).drawCircle(); //这里会编译错误:在Shape类中找不到符号drawCircle(),证实了具体类型信息的丢失!!
    }
}

2 Class对象

2.1 RTTI在Java中的工作原理

  要能够在运行时识别具体类型,说明必然有东西在运行时保存了具体类型信息,这个东西就是Class对象,一种特殊对象。即Class对象表示了运行时的类型信息,它包含了与类有关的信息。

  • 事实上Class对象就是用来创建类的所有的“常规”对象的。
  • 每个类都有一个Class对象。换言之,每当编写并且编译了一个新类,就会产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。
  • 也就是说,Class对象在.java文件编译成.class文件时就生成了,且就保存在这个.class文件
2.2 Class对象用来生成对象(常规对象,非Class对象)

  运行程序的JVM使用所谓的“类加载器”的子系统(class loader subsystem)通过加载Class对象(或者说.class文件)来生成一个类的对象。

  • 所有的类都是在对其第一次使用时,动态加载到JVM中的。当程序第一次使用类的静态成员时,就会加载这个类,这说明构造器也是静态方法,即使构造器前面没加static关键字。
  • 因此,Java程序在它开始运行之前并非被完全加载,其各个部分是在必须时才被加载的。(C++这种静态加载语言是很难做到的。)
2.3 类加载器的工作(过程)
  • 首先检查一个类的Class对象(或理解.class文件)是否已被加载;
  • 如果尚未加载,默认的类加载器就会根据类名查找.class文件
  • 一旦Class对象(.class文件)被加载了(载入内存),它就被用来创建这个类的所有对象。

  以下程序证实上一点。

package net.mrliuli.rtti;

/**
 * Created by leon on 2017/12/3.
 */

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

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

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

public class SweetShop {
    public static void main(String[] args){
        System.out.println("inside main");
        new Candy();
        System.out.println("After creating Candy");
        try{
            Class.forName("net.mrliuli.rtti.Gum");
        }catch (ClassNotFoundException e){
            System.out.println("Couldn't find Gum");
        }
        System.out.println("After Class.forName(\"Gum\")");
        new Cookie();
        System.out.println("After creating Cookie");
    }
}

  • 以上程序每个类都有一个static子句,static子句在类第一次被加载时执行。
  • 从输出中可以看出,
    • Class对象仅在需要时才被加载,
    • static初始化是在类加载时进行的。
  • Class.forName(net.mrliuli.rtti.Gum)是Class类的一个静态成员,用来返回一个Class对象的引用(Class对象和其他对象一样,我们可以获取并操作它的引用(这也就是类加载器的工作))。使用这个方法时,如果net.mrliuli.rtti.Gum还没有被加载就加载它。在加载过程中,Gum的static子句被执行。

  总之,无论何时,只要你想在运行时使用类型信息,就必须首先获得对恰当的Class对象的引用

2.4 获得Class对象引用的方法
  • 通过Class.forName(),就是一个便捷途径,这种方式不需要为了获得Class对象引用而持有该类型的对象。(即没有创建过或没有这个类型的对象的时候就可以获得Class对象引用。)
  • 如果已经有一个类型的对象,那就可以通过调用这个对象的getClass()方法来获取它的Class对象引用了。这个方法属于Object,返回表示该对象的实际类型的Class对象引用。
2.5 Class包含的有用的方法

  以下程序展示Class包含的很多有用的方法:

  • getName() 获取类的全限定名称
  • getSimpleName() 获取不含包名的类名
  • getCanonicalName() 获取全限定的类名
  • isInterface() 判断某个Class对象是否是接口
  • getInterfaces() 返回Class对象实现的接口数组
  • getSuperClass() 返回Class对象的直接基类
  • newInstance() 创建一个这个Class对象所代表的类的一个实例对象。
    • Class引用在编译期不具备任何更进一步的类型信息,所以它返回的只是一个Object引用,但是这个Object引用指向的是这个Class引用所代表的具体类型。即需要转型到具体类型才能给它发送Object以外的消息
    • newInstance()这个方法依赖Class对象所代表的类必须具有可访问的默认的构造函数Nullary constructor,即无参的构造器),否则会抛出InstantiationExceptionIllegalAccessException 异常
package net.mrliuli.rtti;

/**
 * Created by li.liu on 2017/12/4.
 */

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() + "]");
        System.out.println("Simple name: " + cc.getSimpleName());
        System.out.println("Canonical name: " + cc.getCanonicalName());
    }
    public static  void main(String[] args){
        Class c = null;
        try{
            c = Class.forName("net.mrliuli.rtti.FancyToy");
        }catch (ClassNotFoundException e){
            System.out.println("Can't find FancyToy");
            System.exit(1);
        }
        printInfo(c);
        System.out.println("=============================");
        for(Class face : c.getInterfaces()){
            printInfo(face);
        }
        System.out.println("=============================");
        Class up = c.getSuperclass();
        Object obj = null;
        try{
            // Requires default constructor:
            obj = up.newInstance();
        }catch (InstantiationException e){
            System.out.println("Cannot instantiate");
            System.exit(1);
        }catch (IllegalAccessException e){
            System.out.println("Cannot access");
            System.exit(1);
        }
        printInfo(obj.getClass());
    }
}

2.6 类字面常量
  2.6.1 使用类字面常量.class是获取Class对象引用的另一种方法。如 FancyToy.class。建议使用这种方法。
  • 编译时就会受到检查(因此不需要放到try语句块中),所以既简单又安全。根除了对forName()的调用,所以也更高效。
  • 类字面常量.class不仅适用于普通的类,也适用于接口、数组基本类型
  • 基本类型的包装器类有一个标准字段TYPE,它是一个引用,指向对应的基本数据类型的Class引用,即有boolean.class 等价于 Boolean.TYPEint.class 等价于 Integer.TYPE...
  • 注意,使用.class来创建Class对象的引用时,不会自动地初始化该Class对象
  2.6.2 为了使用类而做的准备工作实际包含三个步骤:
  • 加载,这是由类加载器执行的。该步骤将查找字节码(通常在CLASSPATH所指定的路径中查找),并从这些字节码中创建一个Class对象
  • 链接。在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用。
  • 初始化。如果该类具有超类,则对其初始化,执行静态初始化器和静态初始块。
  2.6.3 初始化惰性

  初始化被延迟到了对静态方法(构造器隐式地是静态的)或者非常数静态域进行首次引用时才执行,即初始化有效地实现了尽可能 的“惰性”。
以下程序证实了上述观点。注意,将一个域设置为staticfinal的,不足以成为“编译期常量”或“常数静态域”,如 static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);就不是编译期常量,对它的引用将强制进行类的初始化。

package net.mrliuli.rtti;

import java.util.Random;

class Initable{
	static final int staticFinal = 47;		// 常数静态域
	static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);		// 非常数静态域(不是编译期常量)
	static{
		System.out.println("Initializing Initable");
	}
}

class Initable2{
	static int staticNonFinal = 147;	// 非常数静态域
	static {
		System.out.println("Initializing Initable2");
	}
}

class Initable3{
	static int staticNonFinal = 74;		// 非常数静态域
	static {
		System.out.println("Initializing Initable3");
	}
}

public class ClassInitialization {
	
	public static Random rand = new Random(47);
	public static void main(String[] args) throws Exception {
		Class initalbe = Initable.class;				// 使用类字面常量.class获取Class对象引用,不会初始化
		System.out.println("After creating Initable ref");
		System.out.println(Initable.staticFinal);		// 常数静态域首次引用,不会初始化
		System.out.println(Initable.staticFinal2);		// 非常数静态域首次引用,会初始化
		System.out.println(Initable2.staticNonFinal);	// 非常数静态域首次引用,会初始化
		Class initable3 = Class.forName("net.mrliuli.rtti.Initable3");		// 使用Class.forName()获取Class对象引用,会初始化
		System.out.println("After creating Initable3 ref");
		System.out.println(Initable3.staticNonFinal);	// 已初始化过
	}

}
2.7 泛化的Class引用
2.7.1 Class对象类型限制

  Class引用总是指向某个Class对象,此时,这个Class对象可以是各种类型的,当使用泛型语法对Class引用所指向的Class对象的类型进行限定时,这就使得Class对象的类型变得具体,这样编译器编译时也会做一些额外的类型检查工作。如

package net.mrliuli.rtti;

public class GenericClassReferences {
	public static void main(String[] args){
		Class intClass = int.class;
		Class<Integer> genericIntClass = int.class;
		genericIntClass = Integer.class; 	// Same thing
		intClass = double.class;
		// genericIntClass = double.class; 	// Illegal, genericIntClass 限制为Integer 的Class对象
	}
}

2.7.2 使用通配符?放松对Class对象类型的限制

  通配符?是Java泛型的一部分,?表示“任何事物”。以下程序中Class<?> intClass = int.class;Class intClass = int.class; 是等价的,但使用Class<?>优于使用Class,因为它说明了你是明确要使用一个非具体的类引用,才选择了一个非具体的版本,而不是由于你的疏忽。

package net.mrliuli.rtti;

/**
 * Created by li.liu on 2017/12/4.
 */
public class WildcardClassReferences {
    public static void main(String[] args){
        Class<?> intClass = int.class;
        intClass = double.class;
    }
}

2.7.3 类型范围

  将通配符与extends关键字相结合如Class<? extends Number>,就创建了一个范围,使得这个Class引用被限定为Number类型或其子类型

package net.mrliuli.rtti;

/**
 * Created by li.liu on 2017/12/4.
 */
public class BoundedClassReferences {
    public static void main(String[] args){
        Class<? extends Number> bounded = int.class;
        bounded = double.class;
        bounded = Number.class;
        // Or anything derived from Number
    }
}

  泛型类语法示例:

package net.mrliuli.rtti;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by li.liu on 2017/12/4.
 */

class CountedInteger{
    private static long counter;
    private final long id = counter++;
    public String toString(){
        return Long.toString(id);
    }
}

public class FilledList<T> {
    private Class<T> type;
    public FilledList(Class<T> type){
        this.type = type;
    }
    public List<T> create(int nElements){
        List<T> result = new ArrayList<T>();
        try{
            for(int i = 0; i < nElements; i++){
                result.add(type.newInstance());
            }
        }catch(Exception e){
            throw new RuntimeException(e);
        }
        return result;
    }
    public static void main(String[] args){
        FilledList<CountedInteger> fl = new FilledList<CountedInteger>(CountedInteger.class);   // 存储一个类引用
        System.out.println(fl.create(15));      // 产生一个list
    }
}

  总结,使用泛型类后

  • 使得编译期进行类型检查
  • .newInstance()将返回确切类型的对象,而不是Object对象
2.7.4 Class<? Super FancyToy>的含糊性(vagueness)
package net.mrliuli.rtti;

public class GenericToyTest {
	public static void main(String[] args) throws Exception{

		Class<FancyToy> ftClass = FancyToy.class;
		// Produces exact type:
		FancyToy fancyToy = ftClass.newInstance();

		Class<? super FancyToy> up = ftClass.getSuperclass();	//

		// This won't compile:
		// Toy toy = up.newInstance();
		// Class<Toy> up2 = up.getSuperclass(); 	// 这里 getSuperclass() 已经知道结果是Toy.class了,却不能赋给 Class<Toy>,这就是所谓的含糊性(vagueness)

		// Only produces Object:	(because of the vagueness)
		Object obj = up.newInstance();
		
	}
}
2.7.5 类型转换前先做检查

  RTTI形式包括:

  • 传统类型转换,如(Shape)
  • 代表对象的类型的Class对象
  • 每三种形式,就是关键字 instanceof。它返回一个布尔值,告诉我们对象是不是某个特定类型或其子类。如if(x instanceof Dog)语句会检查对象x是否从属于Dog类。
  • 还一种形式是动态的instanceof:Class.isInstance()方法提供了一种动态地测试对象的途径。Class.isInstance()方法使我们不再需要instanceof表达式。
2.7.6 isAssignableFrom()

  Class.isAssignableFrom() :调用类型可以被参数类型赋值,即判断传递进来的参数是否属于调用类型继承结构(是调用类型或调用类型的子类)。

3 注册工厂

4 instanceof 与 Class 的等价性

  • instanceofisInstance() 保持了类型的概念,它指的是“你是这个吗,或者你是这个类的派生类吗?
  • ==equals() 没有考虑继承——它要么是这个确切的类型,要么不是。

5 反射:运行时的类信息(Reflection: runtime class information)

  Classjava.lang.reflect类库一起对反射的概念进行了支持。

  RTTI与反射的真正区别在于:

  • 对于RTTI来说,是编译时打开和检查.class文件。(换句话说,我们可以用“普通”方式调用对象的所有方法。)
  • 对于反射机制来说,.class文件在编译时是不可获取的,所以是在运行时打开和检查.class文件。

6 动态代理

  • Java的动态代理比代理的思想更向前迈进了一步,因为它可以动态地创建代理动态地处理对所代理方法的调用。
  • 在动态代理上所做的所有调用都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型并确定相应的对策
  • 通过调用静态方法Proxy.newProxyInstance()可以创建动态代理,需要三个参数:
    • ClassLoader loader 一个类加载器,通常可以从已经被加载的对象中获取其类加载器
    • Class<?>[] interfaces 一个希望代理要实现的接口列表(不是类或抽象类)
    • InvocationHandler h 一个调用处理器接口的实现
  • 动态代理可以将所有调用重定向到调用处理器,因此通常会向调用处理器传递一个“实际”对象(即被代理的对象)的引用,从而使得调用处理器在执行其中介任务时,可以将请求转发(即去调用实际对象)。

6.1 动态代理的优点及美中不足

  • 优点:动态代理与静态代理相较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法(InvocationHandler.invoke)中处理。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。
  • 美中不足:它始终无法摆脱仅支持interface代理的桎梏,因为它的设计注定了这个遗憾。

7. 空对象

7.1 YAGNI

   极限编程(XP)的原则之一,YAGNI(You Aren't Going to Need It,你永不需要它),即“做可以工作的最简单的事情”。

7.2 模拟对象与桩(Mock Objects & Stubs)

  空对象的逻辑变体是模拟对象

8. 接口与类型信息

通过使用反射,可以到达并调用一个类的所有方法,包括私有方法!如果知道方法名,就可以在其Method对象上调用setAccessible(true),然后访问私有方法。

  以下命令显示类的所有成员,包括私有成员。-private标志表示所有成员都显示。

javap -private 类名

  因此任何人都可以获取你最私有的方法的名字和签名,即使这个类是私有内部类或是匿名内部类。

package net.mrliuli.rtti;

/**
 * Created by li.liu on 2017/12/6.
 */

import java.lang.reflect.Method;

/**
 * 通过反射调用所有方法(包括私有的)
 */
public class HiddenImplementation {

    static void callHiddenMethod(Object obj, String methodName, Object[] args) throws Exception{
        Method method = obj.getClass().getDeclaredMethod(methodName);
        method.setAccessible(true);
        method.invoke(obj, args);
    }

    public static void main(String[] args) throws Exception{
        callHiddenMethod(new B(), "g", null);
    }
}

interface A {
    void f();
}
class B implements A{
    @Override
    public void f(){}
    private void g(){
        System.out.println("B.g()");
    }
}

参考文献:

  1. 《Java编程思想(第4版)》——(美)Bruce Eckel 著 陈昊鹏 译