类型信息


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

它使你从只能在编译期执行面向类型的操作的禁锢中解脱了出来,并且可以使用某些非常强大的程序。对RTTI的需要,揭示了面向对象设计中许多有趣(并且复杂)的问题,同时也提出了如何组织程序的问题。

为什么需要RTTI

假如你碰到了一个特殊的编程问题——如果能够知道某个泛化引用的确切类型,就可以使用最简单的方式去解决它,那么此时该怎么办呢?例如,假设我们允许用户将某一具体类型的几何形状全都变成某种特殊的颜色,以突出显示它们。通过这种方法,用户就能找出屏慕上所有被突出显示的三角形。或者可能要用某个方法来旋转列出的所有图形,但想跳过圆形,因为对圆形进行旋转没有意义。使用RTTI,可以査询某个引用所指向的对象的确切类型,然后选择或者剔除它。

Class对象

要理解RTTI在Java中的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工作是由称为Class对象的特殊对象完成的,它包含了与类有关的信息。事实上,Class对象就是用来创建类的所有的“常规”对象的。Java使用Class对象来执行其RTTI,即使你正在执行的是类似转型这样的操作。Class类还拥有大量的使用RTTI的其他方式。

类是程序的一部分,每个类都有一个Class对象。换言之,与当编写并目编译了一个新类,就会产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。为了生成这个类的对象,运行这个程序的Java虚拟机(JVM)将使用被称为“类加载器”的子系统。

类加载器子系统实际上可以包含一条类加载器链,但是只有一个原生类加载器,它是JVM实现的一部分。原生类加载器加载的是所谓的可信类,包括Java API类,它们通常是从本地盘加载的。在这条链中,通常不需要添加额外的类加载器,但是如果你有特殊需求(例如以某种特
殊的方式加载类,以支持Web服务器应用,或者在网络中下载类),那么你有一种方式可以控接额外的类加载器。

所有的类都是在对其第一次使用时,动态加载到JVM中的。当程序创建第一个对类的静态成员的引用时,就会加载这个类。这个证明构造器也是类的静态方法,即使在构造器之前并没有使用static关键字。因此,使用new操作符创建类的新对象也会被当作对类的静态成员的引用。因此,Java程序在它开始运行之前并非被完全加载,其各个部分是在必需时才加载的。这一点与许多传统语言都不同,动态加载使能的行为,在诸如C++这样的静态加载语言中是很难或者根本不可能复制的。

类加载器首先检査这个类的Class对象是否已经加载。如果尚未加载,默认的类加载器就会根据类名査找.class文件(例如某个附加类加载器可能会在数据库中査找字节码)。在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良Java代码(这是Java中用于安全防范目的的措施之一)。一旦某个类的Class对象被载入内存,它就可以被用来创建这个类的所有对象。

Class.forName()方法是Class类(所有Class对象都属于这个类)的一个static成员。CIass对象就和其他对象一样,我们可以获取并操作它的引用(这也就是类加载器的工作)。forName()是取得Class对象的引用的一种方法。它是用一个包含目标类的文本名(注意拼写和大小写)的String作输入参数,返回的是一个Class对象的引用。如果Class.forName()找不到你要加载的类,它会抛出异常ClassNotFoundException。

无论何时,只要你想在运行时使用类型信息,就必须首先获得对恰当的Class对象的引用。ClassforName()就是实现此功能的便捷途径,因为你不需要为了获得Class引用而持有该类型的对象。但是,如果你已经拥有了一个感兴趣的类型的对象,那就可以通过调用getClass()方法来获取Class引用了,这个方法属于根类Object的一部分,它将返回表示该对象的实际类型的Class引用。Class包含很多有用的方法,下面是其中的一部分:

public class Test {
	public static void main(String[] args) throws ClassNotFoundException {
		Class c = Class.forName("thinkinjava.Demo");
		System.out.println("name:"+c.getName());
		System.out.println("simpleName:"+c.getSimpleName());
		System.out.println("canonicalName:"+c.getCanonicalName());
		System.out.println("isInterface:"+c.isInterface());
		try {
			 Demo d = (Demo) c.newInstance();
			 System.out.println(d);
		} catch (Exception e) {
			e.printStackTrace();
		}
		//...
	}
	//输出:
	//name:thinkinjava.Demo
	//simpleName:Demo
	//canonicalName:thinkinjava.Demo
	//isInterface:false
	//thinkinjava.Demo@5552e7a4
}

使用getName()来产生全限定的类名,并分别使用getSimpleName()和getCanonicalName()(在Java SE5中引入的)来产生不含包名的类名和全限定的类名 。 isInterface()方法如同其名,可以告诉你这个Class对象是否表示某个接口。因此,通过Class对象你可以发现你想要了解的类型的所有信息。
,
如果你有一个Class对象C,还可以使用getSuperclass()方法査询其直接基类,这将返回你可以用来进一步査询的Class对象。因此,你可以在运行时发现一个对象完整的类继承结构。

class的newInstance()方法是实现“虚拟构造器”的一种途径,虚拟构造器允许你声明:“我不知道你的确切类型,但是无论如何要正确地创建你自己。”在前面的示例中,c仅仅只是一个Class引用,在编译期不具各任何更进一步的类型信息。当你创建新实例时,会得到Object引用,但是这个引用指向的是Demo对象。当然,在你可以发送Object能够接受的消息之外的任何消息之前,你必须更多地了解它并执行某种转型。另外使用newInstance()来创建的类,必须带有默认的构造器。

类字面常量

Java还提供了另一种方法来生成对Class对象的引用,即使用类字面常量。对上述程序来说,就像下面这样:

Demo.class

这样做不仅更简单,而且更安全,因为它在编译时就会受到检査(因此不需要置于try语句块中)。并且它根除了对forName()方法的调用,所以也更高效。

类字面常量不仅可以应用于普通的类,也可以应用于接口、数组以及基本数据类型。另外,对于基本数据类型的包装器类,还有一个标准字段TYPE。TYPE字段是一个引用,指向对应的基本数据类型的Class对象,如下所示:
在这里插入图片描述
我建议使用“.lass”的形式,以保持与普通类的一致性。

注意,有一点很有趣,当使用“.class’来创建对Class对象的引用时,不会自动地初始化该Class对象。为了使用类而做的准各工作实际,包含三个步骤:

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

初始化被延迟到了对静态方法(构造器隐式地是静态的)或者非常数静态域进行首次引用时才执行:

class Table1{
	static{
		System.out.println("init Table1");
	}
}
class Table2{
	static{
		System.out.println("init Table2");
	}
	static final int staticFinal = 7;
	static int static_ = 47;
}
public class Test {
	public static void main(String[] args) throws Exception {
		System.out.println("1:"+Table1.class);
		Class t1 = Class.forName("thinkinjava.Table1");
		Table1 t = (Table1) t1.newInstance();
		System.out.println("2:"+"staticFinal:"+Table2.staticFinal);
		System.out.println("3:"+"static_:"+Table2.static_);
	}
	//输出:
	//1:class thinkinjava.Table1
	//init Table1
	//2:staticFinal:7
	//init Table2
	//3:static_:47
}

初始化有效地实现了尽可能的“惰性”。从对Table1引用的创建中可以看到,仅使用.class语法来获得对类的引用不会引发初始化。但是为了产生CIass引用,Class.forName()立即就进行了初始化,就像在对Table1引用的创建中所看到的。

如果一个static final值是“编译期常量”,就像Table2.staticFinal那样,那么这个值不需要对Table2类进行初始化就可以被读取。但是,如果只是将一个域设置为static或final的还不足以确保这种行为。 如果一个static域不是final的,那么在对它访问时,总是要求在它被获取之前,要先进行链接(为这个域分配存储空间)和初始化(初始化该存储空间),就像在对Table2.static_的访问中所看到的那样。

泛化的Class引用

Class引用总是指向某个Class对象,它可以制造类的实例,并包含可作用于这些实例的所有方法代码。它还包含该类的静态成员,因此Class引用表示的就是它所指向的对象的确切类型,而该对象便是Class类的一个对象。

但是,Java SE5的设计者们看准机会,将它的类型变得更具体了一些,而这是通过允许你对Class引用所指向的Class对象的类型进行限定而实现的,这里用到了泛型语法。在下面的实例中,两种语法都是正确的:

public class Test {
	public static void main(String[] args) throws Exception {
		Class intClass = int.class;
		Class<Integer> integerClass = int.class;
		intClass = double.class;
	}
}

普通的类引用不会产生警告信息,你可以看到,尽管泛型类引用只能赋值为指向其声明的类型,但是普通的类引用可以被重新赋值为指向任何其他的Class对象通过使用泛型语法,可以让编译器强制执行额外的类型检査。

如果你希望稍微放松一些这种限制,应该怎么办呢?乍一看好像你应该能够执行类似下面这样的操作:

Class genericNumberClass = int.class;

这看起来似乎是起作用的,因为Integer继承自Number。但是它无法工作,因为Integer Class对象不是Number Class对象的子类。为了在使用泛化的Class引用时放松限制,我使用了通配符,它是Java泛型的一部分。通配符就是“?’’,表示“任何事物”。因此我们可以在上例的普通Class 引用中添加通配符,并产生相同的结果:

public class Test {
	public static void main(String[] args) throws Exception {
		Class<?> intClass = int.class;
		intClass = double.class;
	}
}

在Java SE5中,Class<?>优于平凡的Class,即便它们是等价的,并且平凡的Class如你所见,不会产生编译器警告信息。Class<?>的好处是它表示你并非是碰巧或者由于疏忽,而使用了一个非具体的类引用,你就是选择一了非具体的版本。

为了创建一个Class引用,它被限定为某种类型,或该类型的任何子类型,你需要将通配符与extends关键字相结合,创建一个范围。因此,与仅仅声明Class不同现在做如下声明:

public class Test {
	public static void main(String[] args) throws Exception {
		Class<? extends Number> intClass = int.class;
		intClass = double.class;
	}
}

向Class引用添加泛型语法的原因便仅是为了提供编译期类型检査,因此如果你操作有误,稍后立即就会发现这一点。在使用普通Class引用,你不会误入歧途,但是如果你确实犯了错误,那么直到运行时你才会发现它,而这显得很不方便。

当你将泛型语法用于Class对象时,会发生一件很有趣的事情,newInstance()将返回该对象的确切类型,而不是Object。这在某种程度上有些受限:

public class Test {
	public static void main(String[] args) throws Exception {
		Class<Demo> dclass = Demo.class;
		Demo d = dclass.newInstance();
		Class<? super Demo> up = dclass.getSuperclass();
		Object o = up.newInstance();
		//Class<Demoson> up2 = up.getSuperclass();
	}
}

如果你手头的是超类,那编译器将只允许你声明超类引用是“某个类,它是Demo超类”,就像在表达式Class<? Super Demo>中所看到的,而不会接受Class<Demoson>这样的声明。这看上去显得有些怪,因为gelSuperClass()方法返回的是基类(不是接口),并且编译器在编译期就知道它是什么类型了——在本例中就是Demoson.class——而不仅仅只是“某个类,它是FancyTby超类” 。不管怎样,正是由于这种含糊性,up.newInstance()的返回值不是精确类型,而只是Object。

新的转型语法
Java SE5还添加了用于CIass引用的转型语法,即cast()方法:

class Buliding{}
class House extends Buliding{}

public class Test {
	public static void main(String[] args) throws Exception {
		Buliding b = new House();
		Class<House> houseType = House.class;
		House h = houseType.cast(b);
		h =  (House)b;
	}
}

cast()方法接受参数对象,并将其转型为Class引用的类型。当然如果你观察上面的代码,则会发现,与实现了相同功能的main()中最后一行相比,这种转型好像做了很多额外的工作 。 新的转型语法对于无法使用普通转型的情况显得非常有用,在你编写泛型代码时,如果你存储了Class引用,并希望以后通过这个引用来执行转型,这种情况就会时有发生。这被证明是一种常见的情况——我发现在整个Java SE5类库中,只有一处使用了Cast()(在com.sum.mirror.util.DeclarationFilter中)。

在Java SE5中另一个没有任何用处的新特性就是CIass.asSubclass(),该方法允许你将一个类对象转型为更加具体的类型。

类型转换前先做检查

迄今为止,我们已知的RTTI形式包括:

  1. 传统的类型转换,如“(Shape)”,由RTTI确保类型持换的正确性,如果执行了一个错误的类型转换,就会抛出一个ClassCastException异常。
  2. 代表对象的类型的Class对象。通过査的Class对象可以获取运行时所需的信息。

在C++中,经典的类型转换“(Shape)”并不使用RTTI。它只是简单地告诉编译器将这个对象作为新的类型对待。而Java要执行类型检査,这通常被称为“类型安金的向下转型”。之所以叫“向下转型”,是由于类层次结构图从来就是这么排列的。如果将Circle类型转换为Shape类型被称作向上转型,那么将Shape转型为Circle,就被称为向下转型。但是由于知道Circle肯定是一个Shape,所以编译器允许自由地做向上转型的赋值操作,而不需要任何显式的转型操作。 编译器无法知道对于给定的Shape到底是什么Shape——它可能就是shape,或者是shape的子类型,例如Circle、Square或某种其他的类型。在编译期,编译器只能知道它是shape因此,如果不使用显式的类型转换,编译器就不允许你执行向下转型赋值,以告知编译器你拥有额外的信息,这些信息使你知道该类型是某种特定类型(编译器将检査向下转型是否合理,因此它不允许向下转型到实际上不是待转型类的子类的类型上)。

RTTI在Java中还有第三种形式,就是关鍵字instanceof。它返回一个布尔值,告诉我们对象是不是某个特定类型的实例。可以用提问的方式使用它,就像这样:

if(x instanceof Dog){
    ((Dog)x).bark();
}

在将x转型成一个Dog前,上面的if话句会检査对象x是否从属于Dog类。进行向下转型前,如果没有其他信息可以告诉你这个对象是什么类型,那么使用instanceof是非常重要的,否则会得到一个ClassCastException异常。

对instanceof有比较严格的限制:只可将其与命名类型进行比较,而不能与Class对象作比较。其实这并非是一种如你想象中那般好的限制,因为渐渐地读者就会理解,如果程序中编写了许多的instanceof表达式,就说明你的设计可能存在瑕疵。

动态的instanceof

Class.isInstance()方法提供了一种动态的地测试对象的途径。例:

class Buliding{}
class House extends Buliding{}
public class Test {
	public static void main(String[] args) throws Exception {
		House h = new House();
		Class<? extends Buliding> s = h.getClass();
		if(s.isInstance(new House())){
			System.out.println("true");
		}else{
			System.out.println("false");
		}
	}
}

可以看到,isinstanceof()方法使我们不再需要instanceof表达式。此外,这意味着如果要求添加新类型的House。

注册工厂

工厂方法设计摸式可以将对象的创建工作交给类自己去完成。工厂方法可以被多态地调用,从而为你创建恰当类型的对象。在下面这个非常简单的版本中,工厂方法就是Factory接口中的create()方法:

public interface Factory{ T create();}

泛型参数T使得create()可以在每种Factory实现中返回不同的类型。这也充分利用了协变返回类型。例:

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

class Test_dog implements Factory<Test_dog>{
	@Override
	public Test_dog create() {return new Test_dog();}
	@Override
	public String toString() {return "is Dog";}
}
class Test_cat implements Factory<Test_cat>{
	@Override
	public Test_cat create() {return new Test_cat();}
	@Override
	public String toString() {return "is Cat";}
}

public class Test {
	public static void main(String[] args) throws Exception {
		List<Factory> list = new LinkedList<Factory>();
		list.add(new Test_cat());
		list.add(new Test_dog());
		System.out.println(list.get(0).create());
		System.out.println(list.get(1).create());
	}
	//输出:
	//is Cat
	//is Dog
}

instanceof与Class的等价性

在査询类型信息时,以instanceof的形式(即以instanceof的形式或isInstance()的形式,它们产生相同的结果)与直接比较Class对象有一个很重要的差别。下面的例子展示了这种差别:

class Base{}
class Derived extends Base{}

public class Test {
	public static void main(String[] args) throws Exception {
		Base b = new Base();
		System.out.println("1:"+b.getClass());
		System.out.println("2:"+(b instanceof Base));
		System.out.println("3:"+Base.class.isInstance(b));
		System.out.println("4:"+(Base.class == b.getClass()));
		System.out.println("5:"+(Base.class.equals(b.getClass())));
	}
	//输出:
	//1:class thinkinjava.Base
	//2:true
	//3:true
	//4:true
	//5:true
}

test()方法使用了两种形式的instanceof作为参数来执行类型检査。然后获取class引用,并用==和equals()来检査Class对象是否相等。使人放心的是,instanceof和islnstanceof()生成的结果完全一样,equals()和==也一样。但是要注意的是,instanceof保持了类型的概念,它指的是“你是这个类吗,或者你是这个类的派生类吗?”而如果用==比较实际的class 对象,就没有考虑继承——它或者是这个确切的类型,或者不是。

反射:运行时的类信息

如果不知道某个对象的确切类型,RTTI可以告诉你。但是有一个限制:这个类型在编译时必须已知,这样才能使用RTTI识别它,并利用这些信息做一些有用的事。换句话说,在编译时,编译器必须知道所有要通过RTTI来处理的类。

初看起来这似乎不是个限制,但是假设你获取了一个指向某个并不在你的程序空间中的对象的引用,事实上在编译时你的程序根本没法获知这个对象所属的类。例如,假设你从磁盘文件,或者网络连接中获取了一串字节,并且你被告知这些字节代表了一个类。既然这个类在编译器为你的程序生成代码之后很久才会出现,那么怎样才能使用这样的类呢?在传统的编程坏境中不太可能出现这种情况。但当我们置身于更大规模的编程世界中,在许很多重要情况下就会发生上面的事情。

人们想要在运行时获取类的信息的另一个动机,便是希望提供在跨网络的远程平台上创建和运行对象的能力。这被称为远程方法调用(RMI),它允许一个Java程序将对象分布到多台机器上。需要这种分布能力是有许多原因的,例如,你可能正在执行一项需进行大量计算的任务,为了提高运算速度,想将计算划分为许多小的计算单元,分布到空闲的机器上运行。 ’

Class类与java.lang.reflect类库一起对反射的概念进行了支持,该类库包含了Field、Method以及Constructor类(每个类都实现了Member接口)。这些类型的对象是由JVM在运行时创建的,用以表示未知类里对应的成员。这样你就可以使用Constructor创建新的对象,用get()和set()方法法取和修改与Field对象关联的字段,用invoke()方法调用与Method对象关联的方法。另外还可以调用getFields()、getMethods()和getConstructors()等很便利的方法,以返回表示字段、方法以及构造器的对象的数组(在JDK文档中,通过查找Class类可了解更多相关资料)。这样,匿名对象的类信息就能在运行时被完全确定下来,而在编译时不需要知道任何事情。

重要的是,要认识到反射机制并没有什么神奇之处。当通过反射与一个未知类型的对象打交道时,JVM只是简単地检査这个对象,看它属于哪个特定的类(就像RTTI那样)。在用它做其他事情之前必须先加载那个类的Class对象。因此,那个类的.class文件对于JVM来说必须是可获取的,要么在本地机器上,要么可以通过网络取得。所以RTTI和反射之间真正的区别只在于,对RTTI来说,编译器在编译时打开和检査.class文件。(换句话说,我们可以用“普通”方式调用对象的所有方法。)而对于反射机制来说.class文件在编译时是不可获取的,所以是在运行时打开和检査class文件。

动态代理

代理是基本的设计模式之一,它是你为了提供额外的或不同的操作,而插入的用来代替实际对象的对象。这些操作通常涉及与“实际’对象的通信,因此代理通常充当者中间人的角色。下面是一个用来展示代理结构的简单示例:

interface Interface{
	void dosomething();
}
class RealObject implements Interface{
	@Override
	public void dosomething() {
		System.out.println("RealObject do something");
	}
}
class SimpleProxy implements Interface{
	private Interface proxied;
	public SimpleProxy(Interface Interface) {
		this.proxied = Interface;
	}
	@Override
	public void dosomething() {
		System.out.println("simpleProxy dosomething");
		proxied.dosomething();
	}
}
public class Test {
	public static void consumer(Interface i){
		i.dosomething();
	}
	public static void main(String[] args) throws Exception {
		consumer(new RealObject());
		consumer(new SimpleProxy(new RealObject()));
	}
	//输出:
	//RealObject do something
	//simpleProxy dosomething
	//RealObject do something
}

因为consumer()接受的Interface,所以它无法知道正在获得的到底是RealObject还是SimpleProxy,因为这二者都实现了Interface。但是SimpleProxy已经被插入到了客户端和RealObject之间,因此它会执行操作,然后调用RealObject上相同的方法。

在任何时刻,只要你想要将额外的操作从“实际”对象中分离到不同的地方,特别是当你希望能够很容易地做出修改,从没有使用额外操作转为使用这些操作,或者反过来时,代理就显得很有用(设计模式的关键就是封装修改——因此你需要修改事务以证明这种模式的正确性)。例如,如果你希望跟踪对RealObjct中的方法的调用,或者希望度量这些调用的开销,那么你应该怎样做呢?这些代码肯定是你不希望将其合并到应用中的代码,因此代理使得你可以很容易地添加或移除它们。

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

class DynamicProxyHandler implements InvocationHandler{
	private Object proxied;
	public DynamicProxyHandler(Object obj){
		proxied = obj;
	}
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		System.out.println("*** proxy:"+proxied.getClass()+",method:"+method.getName()+" ***");
		return method.invoke(proxied, args);
	}
}
public class Test{
	public static void consumer(Interface i){
		i.dosomething();
	}
	public static void main(String[] args) throws Exception {
		RealObject r = new RealObject();
		consumer(r);
		Interface proxy = (Interface) Proxy.newProxyInstance(
				Interface.class.getClassLoader(),
				new Class[]{Interface.class},
				new DynamicProxyHandler(r)
				);
		consumer(proxy);
	}
	//输出:
	//RealObject do something
	//*** proxy:class thinkinjava.RealObject,method:dosomething ***
	//RealObject do something
}

通过调用静态方法Proxy.newProxyInstance()可以创建动态代理,这个方法需要得到一个类加载器(你通常可以从已经被加载的对象中获取其类加载器,然后传递给它),一个你希望该代理实现的接口列表(不是类或抽象类),以及InvocationHandler接口的一个实现。动态代理可以将所有调用重定向到调用处理器,因此通常会向调用处理器的构造器传递给一个“实际”对象的引用,从而使得调用处理器在执行其中介任务时,可以将请求转发。

invoke()方法中传递进来了代理对象,以防你需要区分请求的来源,但是在许多情况下,你并不关心这一点。然而在invoke()内部,在代理上调用方法时需要格外当心,因为对接口的调用将被重定向为对代理的调用。

通常,你会执行被代理的操作,然后使用Method.invoke()将请求转发给被代理对象,并传入必需的参数。这初看起来可能有些受限,就像你只能执行泛化操作一样。但是你可以通过传递其他的参数,来过滤某些方法调用。

动态代理并非是你日常使用的工具,但是它可以非常好地解决某些类型的问题。你在《Thinking in Patterns》(查看www.MindView.net)和Erich Gamma等人撰写的《Design Patterns》这两本书中了解到有关代理和其他设计模式的更多知识。

项目:使用动态代理来编写一个系统以实现事务,其中代理在被代理的调用执行成功时(不抛出任何异常)执行提交,而在其执行失败时执行回滚。你的提交和回滚都针对一个外部的文本文件,该文件不在Java异常的控制范围之内。你必须注意操作的原子性。

空对象

当你使用内置的null表示缺少对象时:在每次使用引用时都必须测试其是否为null,这显得枯燥,而且势必产生相当乏味的代码。问题在于null除了在你试图用它执行任何操作来产生NullPointerException之外,它自己没有其他任何行为有时引入空对象的思想将会很有用,它可以按受传递给它的所代表的对象的消息,但是将返回表示为实际上并不存在任何 “真实”对象的值。通过这种方式,你可以假设所有的对象都是有效的,而不必浪费编程精力去检査null(并阅读所产生的代码)。

尽管想象一种可以自动为我们创建空对象的编程语言显得很有趣,但是实际上,到处使用空对象并没有任何意义——有时检査null就可以了,有时你可以合理地假设你跟本不会遇到null,有时甚至通过NullPointerException来探测异常也可以接受的。空对象最有用之处在于更靠近数据,因为对象表示的是问题空间内的实体。有一个简单的例子,许多系统都有一个Person类,而在代码中,有很多情况是你没有一个实际的人(或者你有,但是你还没有这个人的全部信息),因此,通常你会使用一个null引用并测试它。与此不同的是,我们可以使用空对象。但是即使空对象可以响应“实际”对象可以响应的所有消息,你仍需要某种方式去测试其是否为空。要达到此目的,最简単的方式是创建一个标记接口:

public interface Null{}

这使得instanceof可以探测空对象,更重要的是,这并不要求你在所有的类中都添加isNull()方法(毕竟,这只是执行RTTI的一种不同方式一一为什么不使用内置的工具呢?)

interface Null{}
public class Person {
	private int id;
	private String name;
	public Person() {}
	public Person(int id,String name) {
		this.id = id;
		this.name = name;
	}
	
	public static class NullPerson extends Person implements Null{
		private NullPerson(){
			super(0,"None");
		}
	}
	public static final Person NULL = new NullPerson();
}

通常,空对象都是单例,因此这里将其作为静态final实例创建。这可以正常工作的,因为Person是不可变的——你只能在构造器中设置它的值,然后读取这些值,但是你不能修改它们(因为String自身具备内在的不可变性)。如果你想要修改一个NullPerson,那只能用一个新的Person对象来替换它。注意,你可以选择使用instanceof来探测泛化的Null还是更具体的NullPerson,但是由于使用了单例方式,所以你还可以只使用equals()甚至==来与Person.Null比较。

模拟对象与桩

空对象的逻辑变体是模拟对象和桩。与空对象一样,它们都表示在最终的程序中所使用的“实际”对象。但是,模拟对象和桩都只是假扮可以传递实际信息的存活对象,而不是像空对象那样可以成为null的一种更加智能化的替代物。

模拟对象和桩之间的差导在于程度不同。模拟对象往往是轻量级和自测试的,通常很多模拟对象被创建出来是为了处理各种不同的测试情况。桩只是返回桩数据它通常是重量级的,并且经常在测试之间被复用。桩可以根据它们被调用的方式,通过配置进行修改,因此桩是一种复杂对象,它要做很多事。然而对于模拟对象,如果你需要做很多事情,通常会创建大量小而简单的模拟对象。

接口与类型信息

interface关键字的一种重要目标就是运行程序员隔离构件,进而降低耦合性。如果你编写接口,那么就可以实现这一目标,但是通过类型信息,这种耦合性还是会传播出去——接口并非是对解耦的一种无懈可击的保障。下面有一个示例先是一个接口:

public interface A{
	void f();
}

然后实现这个接口,你可以看到其代码是如何围绕着实际的实现类型潜行的:

class B implements A{
	@Override
	public void f() {}
	public void g() {}
}
public class Test {
	public static void main(String[] args) {
		A a = new B();
		a.f();
		System.out.println(a.getClass().getName());
		if (a instanceof B) {
			B b = (B)a;
			b.g();
		}
	}
}

通过使用RTTI,我们发现a是被当作B实现的。通过将其转型为B,我们可以调用不在A中的方法。这是完全合法和可接受的,但是你也许并不想让客户端程序员这么做,因为这给了他们一个机会,使得他们的代码与你的代码的耦合程度超过你的期望。也就是说,你可能认为interface关键字正在保护着你,但是它并没有,在本例中使用B来实现A这一事实是公开有案可查的。

一种解决方案是直接声明,如果程序员决定使用实际的类而不是接口,他们需要自己对自己负责。这在很多情况下可能都是合理的,但“可能”还不够,你也许希望应用一些更严苛的控制。最简単的方式是对实现使用包访问权限,这样在包外部的客户端就不能看到它了。

但是如果使用包访问控制,那么在包的外部就不能使用其方法了。如果
通过使用反射,可以到达并调用所有方法,甚至是private方法如果知道方法名,你就可以在其Method对象上调用setAccessible(true)方法。

你可能会认为,可以通过只发布编译后的代码来阻止这种情况,但是这并不解决问题。因为只需运行javap,一个随JDK发布的反编译器即可突破这一限制。下面是一个使用它的命令行:

javap -private C

-private标志表示所有的成员都应该显示,甚至包括私有成员。因此任何人都可以获取你最私有的方法的名字和签名,然后调用它们。

如果你将接口实现为一个私有内部类,又会怎样呢?很抱歉对反射仍旧没有隐藏任何东西。那么如果是匿名类也是一样的。看起来没有任何方式可以阻止反射到达并调用那些非公共访问权限的方法。对于域来说的确如此,即使是private域。

但是,final域实际上在遭遇修改时是安全的。运行时系统会在不抛异常的情况下接受任何修改尝试,但是实际上不会发生任何修改。通常,所有这些违反访问权限的操作并非世上最遭之事。如果有人使用这样的技术去调用标识为private或包访问权限的方法(很明显这些访问权限表示这些人不应该调用它们),那么对他们来说如果你修改了这些方法的某些方面,他们不应该抱怨。另一方面总是在类中留下后门的这一事实,也许可以使得你能够解决某些特定类型的问题,但如果不这样做,这些问题将难以或者不可能解决,通常反射带来的好处是不可否认的。

总结

RTTI允许通过匿名基类的引用来发现类型信息。初学者极易吴用它,因为在学会使用多态调用方法之前,这么做也很有效。面向对象编程语言的目的是让我们在凡是可以使用的地方都使用多态机制,只在必需的时候使用RTTI。

然而使用多态机制的方法调用,要求我们拥有基类定义的控制权,因为在你扩展程序的时候,可能会发现基类并未包含我们想要的方法。如果基类来别人的类,或者由别人控制,这时候RTTI便是一种解决之道:可继承一个新类,然后添加你需要的方法。在代码的其他地方,可以检査你自己特定的类型,并调用你自己的方法。这样做不会破坏多态性以及程序的扩展能力,因为这样添加一个新的类并不需要在程序中搜索switch话句。但如果在程序主体中添加需要的新特性的代码,就必须使用RTTI来检査你的特定的类型。

如果只是为了某个特定类的利益,而将某个特性放进基类里,这意味着从那个基类派生出的所有其他子类都带有这些可能无意义的东西。这会使得接口更不清晰,因为我们必须覆盖由基类继成而来的所有抽象方法,这是很恼人的。例如,考虑一个表示乐器类层次结构。假设我们想清洁管弦乐队中某些乐器残留的口水,一种办法是在基类中放入一个方法。但这样做会造成混淆,因为它意味着打击乐器、弦乐器等也需要清洁口水。在这个例子中,RTTI可以提供了一种更合理的解决方案。可以将那个清洁口水的方法置于适当的特定类中,在这个例子中是“管乐器”。同时你可能会发现还有更恰当的解決方法,在这里,就是将清洁方法置于基类中,但是初次面对这个问题时读者可能看不到这样的解决方案,而误认为必须使用RTTI。

最后一点,RTTI有时能解决效率问题。也许你的程序漂亮地运用了多态,但其中某个对象是以极端缺乏效率的方式达到这个目的的。你可以挑出这个类,使用RTTI,并且为其编写一段特别的代码以提高效率。然而必须要注意,不要太早地关注程序的效率问题,这是个诱人的陷阱。最好首先让程序运作起来,然后再考虑它的速度,如果要解决效率问题可以使用profiler(査看http://MindView.net/Books/BetterJava上的补充材料)。

我们已经看到了,由于反射允许更加动态的编程风格,因此它开创了编程的新世界。对有些人来说,反射的动态特性是一种烦扰,对于已经习惯于静态类型检査的安全性的人来说,你可以执行一些只能在运行时进行的检査,并用异常来报告检査结果的行为,这本身就是一种错误的方向。有些人走的更远,他们声称引入通行时异常本身就是一种指示,说明应该避免这种代码。我发现这种意义的安全是一种错觉,因为总是有些事情是在运行时发生并抛出异常的,即使是在不包含任何try语句块或异常规格说明的程序中也是如此。因此,我认为一致的错误报告模型的存在使我们能够通过使用反射编写动态代码。当然,尽力编写能够进行静态检査的代码是值得的,只要你确实能够这么做。但是我相信动态代码是将Java与其他例如C++这样的语言区分开的重要工具之一。


  1. 本文来源《Java编程思想(第四版)》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值