全部博文(556)
分类: Java
2011-05-05 19:08:01
两个引用之所以调用同一个行为,是因为Derived2对象并不知道去调用哪个方法。对 象只知道什么时候调用,它随着继承实现的顺序去执行。这样的顺序决定了Derived2对象调用Derived里的m1()方法,并调用Derived2 里的m2(String)方法。这种结果取决于对象本身的类型,而不是引用的类型。
尽管如此,但不意味着你用derived2和 base引用的效果是完全一样的。如图3所示,Base的引用只能看到Base类型拥有的操作。所以,虽然Derived2有对方法m3()和m4()的 映射,但是变量base不能访问这些方法。
运行期的Derived2对象保持了接受m3()和m4()方法的能力。类型的限制使 Base的引用不能在编译期调用这些方法。编译期的类型检查像一套铠甲,保证了运行期对象只能和正确的操作进行相互作用。换句话说,类型定义了对象间相互 作用的边界。
多态的依附性
类型的一致性是多态的核心。对象上的每一个引用,静态的类型检查器都要确认这样的依附和其对象的层次是一致的。当一个引用成功的依附于另一个不同的对象 时,有趣的多态现象就产生了。(严格的说,对象类型是指类的定义。)你也可以把几个不同的引用依附于同一个对象。在开始更有趣的场景前,我们先来看一下下 面的情况为什么不会产生多态。
多个引用依附于一个对象
图2和图3描述的例子是把两个及两个以上的 引用依附于一个对象。虽然Derived2对象在被依附之后仍保持了变量的类型,但是,图3中的Base类型的引用依附之后,其功能减少了。结论很明显: 把一个基类的引用依附于派生类的对象之上会减少其能力。
一个开发这怎么会选择减少对象能力的方案呢?这种选择是间接的。假设有一个名 为ref的引用依附于一个包含如下方法的类的对象:
用一个Derived2的参数调用poly(Base)是符合参数类型检查的:
方法调用把一个本地Base类型的变量依附在一个引入的对象上。所以,虽然这个方法只接 受Base类型的参数,但Derived2对象仍是允许的。开发这就不必选择丢失功能的方案。从人眼在通过Derived2对象时所看到的情况,Base 类型引用的依附导致了功能的丧失。但从执行的观点看,每一个传入poly1(Base)的参数都认为是Base的对象。执行机并不在乎有多个引用指向同一 个对象,它只注重把指向另一个对象的引用传给方法。这些对象的类型不一致并不是主要问题。执行器只关心给运行时的对象找到适当的实现。面向类型的观点展示 了多态的巨大能力。
附于多个对象的引用
让我们来看一下发生在poly1(Base)中的多态行 为。下面的代码创建了三个对象,并通过引用传给poly1(Base):
poly1(Base)的实现代码是调用传进来的参数的m1()方法。图3和图4展示了 把三个类的对象传给方法时,面向类型的所使用的体系结构。
请注意每个图中方法m1()的映射。图3中,m1()调用了Derived类的代码;上面代码中的注释标明了ploy1(Base)调用 Derived.m1()。图4中Derived对象调用的仍然是Derived类的m1()方法。最后,图4中,Base对象调用的m1()是Base 类中定义的代码。
多态的魅力何在?再来看一下poly1(Base)的代码,它可以接受任何属于Base类范畴的参数。然而,当他收 到一个Derived2的对象时,它实际上却调用了Derived版本的方法。当你根据Base类派生出其他类时,如 Derived,Derived2,poly1(Base)都可以接受这些参数,并作出选择调用合适的方法。多态允许你在完成poly1(Base)后扩 展它的用途。
这看起来当然很神奇。基本的理解展示了多态的内部工作原理。在面向类型的观点中,底层的对象所实现的代码是非实质性的。 重要的是,类型检查器会在编译期间为每个引用选择合适的代码以实现其方法。多态使开发者运用面向类型的观点,不考虑实现的细节。这样有助于把类型和实现分 离(实际用处是把接口和实现分离)。
对象接口
多态依赖于类型和实现的分离,多用来把接口和实现分离。但下面的观点好像把Java的关键字 interface搞得很糊涂。
更为重要的使开发者们怎样理解短语“the interface to an object",典型地,根据上下文,这个短语的意思是指一切对象类中所定义的方法,至一切对象公开的方法。这种倾向于以实现为中心的观点较之于面向类型 的观点来说,使我们更加注重于对象在运行期的能力。图3中,引用面板的对象表面被标志成"Derived2 Object"。这个面板上列出了Derived2对象的所有可用的方法。但是要理解多态,我们必须从实现这一层次上解放出来,并注意面向类型的透视图中 被标为"Base Reference"的面板。在这一层意思上,引用变量的类型指明了一个对象的表面。这只是一个表面,不是接口。在类型一致的原则下,我们可以用面向类型 的观点,为一个对象依附多个引用。对interface to an object这个短语的理解没有确定的理解。
在类型概念 中,the interface to an object refers 引用了面向类型观点的最大可能----如图2的情形。把一个基类的引用指向相同的对象缩小了这样的观点----如图3所示。类型概念能使人获得把对象间的 相互作用同实现细节分离的要领。相对于一个对象的接口,面向类型的观点更鼓励人们去使用一个对象的引用。引用类型规定了对象间的相互作用。当你考虑一个对 象能做什么的时候,只需搞明白他的类型,而不需要去考虑他的实现细节。
Java接口
以上所谈到的 多态行为用到了类的继承关系所建立起来的子类型关系。Java接口同样支持用户定义的类型,相对地,Java的接口机制启动了建立在类型层次结构上的多态 行为。假设一个名为ref的引用变量,并使其指向一个包含一下方法的类对象:
为了弄明白poly2(IType)中的多态,以下的代码从不同的类创建两个对象,并分别把他们传给 poly2(IType):
上面的代码类似于关于poly1(Base)中的多态的讨论。poly2(IType)的实现代码是调 用每个对象的 本地版本的m3()方法。如同以前,代码的注释表明了每次调用所返回的CString类型的结果。图5表明了两次调用poly2(IType)的概念结构:
图5:指向Derived2和Separate对象的IType引用
方法poly1(Base)和poly2(IType)中所表现的多态行为的相似之处可以从透视图中直接看出来。把我们在实现在一层上的理解再提高一 层,就可以看到这两段代码的技巧。基类的引用指向了作为参数传进的类,并且按照类型的限制调用对象的方法。引用既不知道也不关心执行哪一段代码。编译期间 的子类型关系检查保证了通过的对象有能力在被调用的时候选择合适的实现代码。
然而,他们在实现层上有一个重要的差别。在 poly1(Base)的例子中(图3和图4),Base-Derived-Derived2的类继承结构为子类型关系的建立提供了条件,并决定了方法去 调用哪段代码。在poly2(IType)的例子中(如图5),则是完全不同的动态发生的。Derived2和Separate不共享任何实现的层次,但 是他们还是通过IType的引用展示了多态的行为。
这样的多态行为使Java的接口的功能的重大意义显得很明显。图1中的UML类图 说明了Derived是Base和IType的子类型。通过完全脱离实现细节的类型的定义方法,Java实现了多类型继承,并且不存在Java所禁止的多 继承所带来的烦人的问题。完全脱离实现层次的类可以按照Java接口实现分组。在图1中,接口IType和Derived,Separate以及这类型的 其他子类型应该划为一组。
按照这种完全不同于实现层次的分类方法,Java的接口机制是多态变得很方便,哪怕不存在任何共享的实现或 者复写的方法。如图5所示,一个IType的引用,用多态的方法访问到了Derived2和Separate对象的m3()方法。
再次探讨对象的接口
注意图5中的Derived2和Separate对象的对m1()的映射方法。如前所述,每一个对象的接 口都包含方法m1()。但却没有办法用这两个对象使方法m1()表现出多态的行为。每一个对象占有一个m1()方法是不够的。必须存在一个可以操作 m1()方法的类型,通过这个类型可以看到对象。这些对象似乎是共享了m1()方法,但在没有共同基类的条件下,多态是不可能的。通过对象的接口来看多 态,会把这个概念搞混。
结论
从全文所述的面向对象多态所建立起来的子类型多态,你可以清楚地认识到这种面向类型的观点。如果你想理解子类型多态的思想,就应该把注意力从实现的细节转移到类型的上。类型把对象分成组,并且管理着这些对象的接口。类型的 继承层次结构决定了实现多态所需的类型关系。
有趣的是,实现的细节并不影响子类型多态的层次结构。类型决定了对象调用什么方法,而实 现则决定了对象怎么执行这个方法。也就是说,类型表明了责任,而负责实施的则是具体的实现。将实现和类型分离后,我们好像看到了这两个部分在一起跳舞,类型决定了他的舞伴和舞蹈的名字,而实现则是舞蹈动作的设计师。