运行时类型信息可以在程序运行时发现和使用类型信息。
有两种方式可以在运行时识别对象和类的信息,一种是传统的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对象。为了使用类而做的准备工作实际包含三个步骤:
- 加载。这是由类加载器执行的。将查找字节码,并从这些字节码中创建一个Class对象。
- 链接。在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必须的话,将解析这个类创建的对其他类的所有引用。
- 初始化。如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。
注意如果一个值是一个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 extends Number> 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 super FancyToy> 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修饰,还是定义在私有内部类或是匿名内部类,都无法有效地限制反射机制。