《Thanking in Java》14. 类型信息

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

有两种方式可以在运行时识别对象和类的信息,一种是传统的RTTI,它假定我们再编译时已经知道了所有的类型;另一种是反射机制,它允许在运行时发现和使用类的信息。

14.1 RTTI

RTTI含义是在运行时,识别一个对象的类型。当有一个指向基础型别(父类)的reference(引用)时,RTTI机制让你找出其所指的确切型别。

RTTI是一种思想,Java中多态和反射的使用都利用了这一思想。什么时候用到传统的RTTI呢?我认为是在使用多态的时候,Java的所有方法绑定都采用“后期绑定”技术,若一种语言实现了后期绑定,那么同时还要提供一些机制,以便在运行时间正确判断对象类型,并调用适当的方法。也就是说,编译器此时仍然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。反射是RTTI发展产生的概念和技术,反射的作用是分析类的结构,并可以创建类的对象。总结为一句话:多态是隐式地利用RTTI,反射则是显式地使用RTTI。

14.2 Class对象

类是程序的一部分,每个类都有一个Class对象,每当编写并编译了一个新类,就会产生一个Class对象。

所有的类都是在对其第一次使用时,动态加载到JVM中的,当程序创建第一个对类的静态成员的引用时,就会加载这个类。使用new操作符创建类的新对象也会被当做对类的静态成员的引用。

java程序在它开始运行之前并非被完全加载,其各部分是在必须时才加载的。

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

Class的相关方法,forName根据传入的文件名获取class实例, getSimpleName获取这个class实例的不包含包名的类名,getInterfaces获取class对象中所包含的接口 getSuperclass获取class对象的直接父类, isInterface判断class对象是否是接口类型。需要注意的是如果使用newInstance方法创建类,必须有默认的构造器。

java还提供了另一种方法来生成对Class对象的引用,即使用类字面常量。使用.class这样做不仅更简单,而且更安全,因为它在编译时就会受到检查。类字面常量不仅可以应用于普通的类,也可以应用与接口、数组以及基本数据类型。

当使用.class来创建对Class对象的引用时,不会自动地初始化该Class对象。为了使用类而做的准备工作实际包含三个步骤:

  1. 加载。这是由类加载器执行的。将查找字节码,并从这些字节码中创建一个Class对象。
  2. 链接。在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必须的话,将解析这个类创建的对其他类的所有引用。
  3. 初始化。如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。

注意如果一个值是一个static final的编译器常量,这个值不需要对类进行初始化就可以被读取。如果一个static域不是final的,那么对它进行访问时,总是要求在它被读取之前,要先进行链接和初始化。

//: typeinfo/ClassInitialization.java
import java.util.*;

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 initable = Initable.class;
    System.out.println("After creating Initable ref");
    // Does not trigger initialization:
    System.out.println(Initable.staticFinal);
    // Does trigger initialization:
    System.out.println(Initable.staticFinal2);
    // Does trigger initialization:
    System.out.println(Initable2.staticNonFinal);
    Class initable3 = Class.forName("Initable3");
    System.out.println("After creating Initable3 ref");
    System.out.println(Initable3.staticNonFinal);
  }
} /* Output:
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74
*///:~

向Class引用添加泛型语法的原因仅仅是为了提供编译期类型检查,因此如果操作有误,立即就会发现这一点。

//: typeinfo/BoundedClassReferences.java

public class BoundedClassReferences {
  public static void main(String[] args) {
    Class bounded = int.class;
    bounded = double.class;
    bounded = Number.class;
    // Or anything else derived from Number.
  }
} ///:~

up.newInstance()的返回值不是精确类型,而只是Object。

//: typeinfo/toys/GenericToyTest.java
// Testing class Class.
package typeinfo.toys;

public class GenericToyTest {
  public static void main(String[] args) throws Exception {
    Class ftClass = FancyToy.class;
    // Produces exact type:
    FancyToy fancyToy = ftClass.newInstance();
    Class up = ftClass.getSuperclass();
    // This won't compile:
    // Class up2 = ftClass.getSuperclass();
    // Only produces Object:
    Object obj = up.newInstance();
  }
} ///:~

14.2 类型转换前先做检查

如果不适用显式的类型转换,编译器就不允许执行向下转型赋值,以告知编译器拥有额外的信息,这些信息是你知道该类型是某种特定类型。

在进行向下转型前,如果没有其他信息可以告诉这个对象是什么类型,那么使用instanceof是非常重要的。

对instanceof有比较严格的限制,只可以将其与命名类型进行比较,而不能与Class对象作比较。

Instanceof有一个额外的功能:它可以确保第一个操作数所引用的对象不是null,当第一个操作数所引用的对象为null时,instanceof运算符返回false,而不会报异常。

Class.isInstance方法提供了一种动态测试对象的途径。它与instanceof表达式的区别是:Class.isInstance方法更加适合泛类型的检测(如代理,接口,抽象类等规则),常与泛化Class对象出现,而instanceof表达式适合直接类型的检查,常与普通的Class对象出现。

Class类的isAssignableFrom(Class cls)方法,如果调用这个方法的class或接口 与 参数cls表示的类或接口相同,或者是参数cls表示的类或接口的父类,则返回true。

14.4 注册工厂

对于不同的类型对象初始化操作可以使用工厂方法设计模式,将对象的创建工作交给类自己去完成。工厂方法可以被多态地调用,从而为你创建恰当类型的对象。泛型参数T使得create可以在每种Factory实现中返回不同的类型。这也充分利用了协变返回类型。

14.5 instanceof与Class的等价性

instanceof保持了类型的概念,它指的是“你是这个类吗,或者你是这个类的派生类吗”,而如果用==比较实际的Class对象,就没有考虑继承——它或者是这个确切的类型,或者不是。

14.6 反射

Class类与reflect类库一起对反射的概念进行了支持,该类库包含了Field、Method以及Constructor类,这些类型的对象时由JVM在运行时创建的,用以标识未知类里对应的成员。

14.7 动态代理

代理是基本的设计模式之一,它是为了提供额外的或不同的操作,而插入的用来代替“实际”对象的对象。这些操作通常设计与“实际”对象的通信,因此代理通常充当中间人的角色。

java的动态代理比代理的思想更向前迈进了一步,因为它可以动态地创建代理并动态地处理对所代理方法的调用。在动态代理上所作的所有调用都会被重定向到单一的调用处理器上,他的工作是揭示调用的类型并确定相应的对策。

//: typeinfo/SimpleDynamicProxy.java
import java.lang.reflect.*;

class DynamicProxyHandler implements InvocationHandler {
  private Object proxied;
  public DynamicProxyHandler(Object proxied) {
    this.proxied = proxied;
  }
  public Object
  invoke(Object proxy, Method method, Object[] args)
  throws Throwable {
    System.out.println("**** proxy: " + proxy.getClass() +
      ", method: " + method + ", args: " + args);
    if(args != null)
      for(Object arg : args)
        System.out.println("  " + arg);
    return method.invoke(proxied, args);
  }
}   

class SimpleDynamicProxy {
  public static void consumer(Interface iface) {
    iface.doSomething();
    iface.somethingElse("bonobo");
  }
  public static void main(String[] args) {
    RealObject real = new RealObject();
    consumer(real);
    // Insert a proxy and call again:
    Interface proxy = (Interface)Proxy.newProxyInstance(
      Interface.class.getClassLoader(),
      new Class[]{ Interface.class },
      new DynamicProxyHandler(real));
    consumer(proxy);
  }
} /* Output: (95% match)    
doSomething
somethingElse bonobo
**** proxy: class $Proxy0, method: public abstract void Interface.doSomething(), args: null
doSomething
**** proxy: class $Proxy0, method: public abstract void Interface.somethingElse(java.lang.String), args: [Ljava.lang.Object;@42e816
  bonobo
somethingElse bonobo
*///:~

可以在invoke方法中对将要调用的方法进行参数过滤。

package chapter14;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * Created by Blue on 2017/11/1.
 */

class MethodSelector implements InvocationHandler {
    private Object proxied;

    MethodSelector(Object proxied) {
        this.proxied = proxied;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().equals("interesting"))
            System.out.println("Proxy detected the interesting method");
        return method.invoke(proxied, args);
    }
}

interface SomeMethods {
    void boring1();

    void boring2();

    void interesting(String arg);

    void boring3();
}

class Implementation implements SomeMethods {
    public void boring1() {
        System.out.println("boring1");
    }

    public void boring2() {
        System.out.println("boring2");
    }

    public void interesting(String arg) {
        System.out.println("interesting " + arg);
    }

    public void boring3() {
        System.out.println("boring3");
    }
}

public class SelectingMethods {
    public static void main(String[] args) {
        SomeMethods proxy = (SomeMethods) Proxy.newProxyInstance(
                SomeMethods.class.getClassLoader(),
                new Class[]{SomeMethods.class},
                new MethodSelector(new Implementation()));
        proxy.boring1();
        proxy.boring2();
        proxy.interesting("bonobo");
        proxy.boring3();
    }
}

14.9 接口与类型信息

通过反射可以调用任何所定义的方法,无论是被private修饰,还是定义在私有内部类或是匿名内部类,都无法有效地限制反射机制。

你可能感兴趣的:(《Thanking in Java》14. 类型信息)