运行时类型信息使得你可以在运行时发现和使用类型信息,主要有两种方式:
- “传统的”RTTI,它假定我们在编译时已经知道了所有的类型;
- “反射”机制,它允许我们在运行时发现和使用类的信息。
14.1 为什么需要RTTI
RTTI维护类型类型的信息,为多态机制的实现提供基础。
14.2 Class对象
类型信息在运行时是通过Class
对象来表示的,完成的Class
对象包含了与类有关的信息。Class
对象就是用来创建所有“常规”对象的,Java使用Class
对象来执行RTTI
类是程序的一部分,每个类都有一个Class
对象,被保存在一个同名的.class
文件中。
类加载器
- 类加载器子系统实际上可以包含一条类加载器链,但是只有一个原生类加载器,它是JVM实现的一部分。原生类加载器加载的是可信类,包括Java API类。
- 所有类都是在对其第一次使用(静态成员或new对象)时,动态加载到JVM的。
Class
对象仅在需要的时候才会加载,static
初始化是在类加载时进行的。- 类加载器首先会检查这个类的
Class
对象是否已被加载过,如果尚未加载,默认的类加载器就会根据类名查找对应的.class
文件。
Class类方法
想在运行时使用类型信息,必须获取对象的Class对象的引用:Class.forName("s2.A");
。该方法会自动初始化该Class对象,注意必须使用全限定名(包含包名)。
clz.getSimpleName()
// 获取全限定名
clz.getCanonicalName()
// 获取接口
clz.getInterfaces();
// 获取父类
clz.getSuperClass();
// 创建该类对象
clz.newInstance();
14.2.1 类字面常量
Java还提供了类字面常量的方式来生成对Class对象的引用:Class clz = A.class
。注意这种方式不会自动初始化该Class对象。
基本类型
- 类字面常量不仅可以用于普通的类,还可以用于接口、数组(int[].class)和基本数据类型(int.class)。
- 基本类型的包装类,都有一个标准字段TYPE,这是一个指向对应基本数据类型Class对象的引用:如
public static final Class<Integer> TYPE = (Class<Integer>) Class.getPrimitiveClass("int");
类的准备过程
- 加载:由类加载器完成,该步骤查找对应的字节码,创建一个Class对象
- 链接:验证类中的字节码,为静态域分配空间;并且如果必须的话,将解析这个类创建的对其他类的所有引用。
- 初始化:如果该类有超类,则对其初始化,执行静态初始化器和静态初始化块
初始化的惰性
- 使用
Class.forName()
会自动初始化;使用A.class
不会自动初始化类 - 编译期常量:
static final int i = 1;
的值,则不需要初始化就可以被读取。 - 如果只是将一个域设置为
static final
不足以保证是编译器常量,如static final int ii = new Random().nextInt();
。 - 如果一个
static
域不是final
的,那么访问之前要先进行链接和初始化。
14.2.2 泛化的Class引用
Java SE5之后,Class也可以支持范型了。
向Class引用添加范型语法的原因仅仅是为了提供编译期类型检查。
14.2.3 新的转型语法
- cast()方法接受参数对象,将其转型为Class引用的类型。
- Class.asSubclass(),该方法允许你讲一个类对象转型为更加具体的类型。
Class<String> clz = String.class;
String str1 = clz.cast("");
14.3 类型转换前先做检查
- Java提供类
instanceOf
关键字,可以判断对象是否是某个类(及其父类)的实例。 clz.isInstance()
方法接受一个对象,判断该对象是否是该clz
指向的类的实例。clz.isAssignableFrom()
方法接受一个Class
对象,判断该Class
对象是否是clz
自身或子类。
public class Test {
public static void main(String[] args) throws Exception {
System.out.println(A.class.isAssignableFrom(C.class));
System.out.println(B.class.isAssignableFrom(C.class));
System.out.println(A.class.isInstance(new C()));
System.out.println(B.class.isInstance(new C()));
}
}
class A { }
interface B {}
class C extends A implements B {}
// Output:
// true
// true
// true
// true
14.4 注册工厂
使用工厂方法设计模式, 将对象的创建工作交给类自己去完成。 工厂方法可以被多态地调用, 从而为你创建恰当类型的对象。
14.5 instanceOf和Class的等价性
- instanceOf和isInstance()的结果完全一样,比较的时候都考虑了继承关系
- A.class.equals(B.class) 和 A.class == B.class 只能比较是否为同一个类,没有考虑继承关系
14.6 反射:运行时的类信息
RTTI的限制
- 如果不知道某个对象的确切类型,RTTI可以告诉你,但这有个限制:这个类型在编译时必须已知。换句话说,编译器在编译时必须知道所有要通过RTTI来处理的类。
- 假设你获取了一个指向某个并不在你程序空间中对象的引用,在编译时你的程序根本无法获知这个对象所属的类。
- 运行时获取类的信息场景:基于构件的编程、远程方法调用(RMI)。
反射机制并没有什么神奇之处
- 当通过反射与一个未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个特定的类(就像RTTI那样)。
- 在用它做其他事情之前必须先加载那个类的Class对象。因此,那个类的.class文件对于JVM来说必须是可获取的:要么在本地机器上,要么可以通过网络取得。
- 所以RTTI和反射之间真正的区别只在于,对RTTI来说,编译器在编译时打开和检查.class文件;而对于反射机制来说,.class文件在编译时是不可获取的,所以是在运行时打开和检查class文件。
反射的作用
反射在Java中是用来支持其他特性的,例如对象序列化和JavaBean。
14.7 动态代理
代理是基本的设计模式之一,它是为你提供额外的或者不同的操作,而插入的用来代替“实际”对象的对象。这些操作通常设计与“实际”对象的通信,因此代理通常充当着中间人的角色。
静态代理
- 静态代理就是写死了在代理对象中执行这个方法前后执行添加功能的形式。
- 优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。
- 缺点:我们得为每一个服务都得创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。
public class Test {
public static void main(String[] args) throws Exception {
new RealObject().doSomething();
System.out.println("代理之后:");
new SimpleProxy(new RealObject()).doSomething();
}
}
interface MyInterface {
void doSomething();
}
class RealObject implements MyInterface {
@Override
public void doSomething() {
System.out.println("RealObject");
}
}
class SimpleProxy implements MyInterface {
private MyInterface myInterface;
public SimpleProxy(MyInterface myInterface) {
this.myInterface = myInterface;
}
// 代理后增加方法
@Override
public void doSomething() {
System.out.println("SimpleProxy");
myInterface.doSomething();
}
}
动态代理
- Java的动态代理比代理的思想更向前迈进了一步, 因为它可以动态地创建代理并动态地处理对所代理方法的调用。在动态代理上所做的所有调用都会被重定向到单一的调用处理器上。
- 通过Proxy.newProxyInstance()可以创建动态代理,需要一个类加载器(通常是被加载的对象获取)、一个希望实现的接口列表(不是类或抽象类)、以及InvocationHandler的一个实现。
- 动态代理可以将所有对接口的调用重定向为对代理的调用。
- 使用动态代理来编写一个系统以实现事务,其中,代理在被代理的调用执行成功(不抛出任何异常)执行提交,而在执行失败时执行回滚。你的提交和回滚都针对一个外部的文本文件,该文件不在Java异常的控制范围之内。你必须注意操作的原子性。
MyInterface myInterface = (MyInterface) Proxy.newProxyInstance(MyInterface.class.getClassLoader(), new Class[]{MyInterface.class}大专栏 编程思想14.类型信息oken punctuation">, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代理方法");
return method.invoke(new RealObject(), args);
}
});
myInterface.doSomething();
14.8 空对象
- 使用
null
的时候每次都要检查是否为null
,这是一件很麻烦的事。 - 引人空对象的思想将会很有用,它可以接受传递给它的所代表的对象的消息,但是将返回表示为实际上并不存在任何“真实”对象的值。通过这种方式,你可以假设所有的对象都是有效的,而不必浪费编程精力去检查
null
。 - 通常空对象是单例的,所以你不仅可以用
instanceOf
来比较,还可以用equals
或==
来比较。 - 注意:在某些地方仍然必须测试空对象,这与检查是否为
null
没有区别,但在很多地方就不必执行额外的测试了,可以直接假设所有对象都是有效的。
public class Test {
public static void main(String[] args) throws Exception {
// 在使用的时候可以直接使用而不会报错空指针
Person p = Person.NULL_PERSON;
System.out.println(p.toString());
}
}
// 空标记接口
interface Null {}
class Person {
void func() {
System.out.println("Person");
}
// 空对象
private static class NullPerson extends Person implements Null {
private NullPerson() {}
@Override
public String toString() {
return "NullPerson";
}
}
public static final Person NULL_PERSON = new NullPerson();
}
动态代理创建空对象
假设有不同的多个Person
的子类,我们相对每一个都创建一个空对象。无论何时,如果你需要一个空Person
对象,只需要调用newNullPerson()
并传递需要代理的Person
的类型。
public class Test {
public static Person newNullPerson(Class<? extends Person> type) {
return (Person) Proxy.newProxyInstance(NullPerson.NULL_PERSON.getClass().getClassLoader(), new Class[]{type}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代理");
return method.invoke(NullPerson.NULL_PERSON, args);
}
});
}
public static void main(String[] args) throws Exception {
Person p = newNullPerson(Person.class);
p.func();
}
}
// 空标记接口
interface Null {}
// 父接口
interface Person {
void func();
}
// 空Person
class NullPerson implements Person, Null {
@Override
public void func() {
System.out.println("NullPerson");
}
public static final Person NULL_PERSON = new NullPerson();
private NullPerson() {}
}
14.8.1 模拟对象与桩
空对象的逻辑变体是模拟对象和桩。与空对象一样,它们都表示在最终的程序中所使用的“实际”对象。但是,模拟对象和桩都只是假扮可以传递实际信息的存活对象,而不是像空对象那样可以成为null
的一种更加智能化的替代物。
模拟对象和桩之间的差异在于程度不同。模拟对象往往是轻量级和自测试的,通常很多模拟对象被创建出来是为了处理各种不同的测试情况。桩只是返回桩数据,它通常是重量级的,并且经常在测试之间被复用。桩可以根据它们被调用的方式,通过配置进行修改,因此桩是一 种复杂对象,它要做很多事。然而对于模拟对象,如果你需要做很多事情,通常会创建大量小而简单的模拟对象。
14.9 接口与类型信息
interface关键字的一种重要目标就是允许程序员隔离构件,进而降低耦合性。如果你编写接口,那么就可以实现这一目标,但是通过类型信息,这种耦合性还是会传播出去——接口并非是对解耦的一种无懈可击的保障。
public class Test {
public static void main(String[] args) {
A a = new B();
a.a();
// 我们需要的是用户使用接口,但是强制转型还是可以访问不存在于接口中的方法
((B) a).b();
}
}
interface A {
void a();
}
class B implements A {
@Override
public void a() {}
public void b() {}
}
解决方法1:方法是直接声明
如果程序员不使用接口而是子类,它们要对自己负责。即B a = new B();
代替A a = new B();
。
解决方法2:包访问权限隐藏
此时在此包外只能使用Hidden.newA()来获取对象,而且由于没有B类的信息,也无法强制转型。
class B implements A {
@Override
public void a() {}
public void b() {}
}
public class HiddenB {
public static A newA() {
return new B();
}
}
反射的后门
- 通过使用反射,仍旧可以到达并调用所有方法,甚至是
private
方法!如果知道方法名,你就可以在其Method
对象上调用setAccessible(true)
。 final
域实际上在遭遇修改时是安全的。运行时系统会在不抛异常的情况下接受任何修改尝试,但是实际上不会发生任何修改。
14.10 总结
- RTTI允许通过匿名基类的引用来发现类型信息。
- 面向对象编程语言的目的是让我们在凡是可以使用的地方都使用多态机制,只在必需的时候使用RTTI。
- 可继承一个新类,然后添加你需要的方法。在代码的其他地方,可以检査你自己特定的类型,并调用你自己的方法,这样做不会破坏多态性以及程序的扩展能力。
- 但如果在程序主体中添加需要的新特性的代码,就必须使用RTTI来检査你的特定的类型。
- 一致的错误报告模型的存在使我们能够通过使用反射编写动态代码。当然,尽力编写能够进行静态检査的代码是值得的,只要你确实能够这么做。但是我相信动态代码是将Java与其他例如C++这样的语言区分开的重要工具之一。