咀嚼Java子范例多态的魅力
副标题#e#
摘要:
Java措施员常常运用工具的多态性使其在适当的处所挪用适当的要领,显得很神奇。这种要领通过担任机制来实现。然而,一个严谨的尝试可以使其变得很大白,并展现了,把多态性领略为与范例相关的观念更为符合,比担任机制的表明更好。这种领略可以辅佐措施员更好的运用多态。
——WM.保罗 罗格斯
“polymorphism(多态)”一词来自希腊语,意为“多种形式”。大都Java措施员把多态看作工具的一种本领,使其能挪用正确的要领版本。尽量如此,这种面向实现的概念导致了多态的神奇成果,胜于仅仅把多态当作纯粹的观念。
Java中的多态老是子范例的多态。险些是机器式发生了一些多态的行为,使我们不去思量个中涉及的范例问题。本文研究了一种面向范例的工具概念,阐明白如何将工具可以或许表示的行为和工具即将表示的行为分分开来。抛开Java中的多态都是来自担任的观念,我们仍然可以感想,Java中的接口是一组没有民众代码的工具共享实现。
多态的分类
多态在面向工具语言中是个很普遍的观念.固然我们常常把多态等量齐观,但实际上有四种差异范例的多态。在开始正式的子范例多态的细节接头前,然我们先来看看普通面向工具中的多态。
Luca Cardelli和Peter Wegner("On Understanding Types, Data Abstraction, and Polymorphism"一文的作者, 文章参考资源链接)把多态分为两大类—-特定的和通用的—-四小类:强制的,重载的,参数的和包括的。他们的布局如下:
在这样一个别系中,多态表示出多种形式的本领。通用多态引用有沟通布局范例的大量工具,他们有着配合的特征。特定的多态涉及的是小部门没有沟通特征的工具。四种多态可做以下描写:
强制的:一种隐式做范例转换的要领。
重载的:将一个符号符用作多个意义。
参数的:为差异范例的参数提供沟通的操纵。
包括的:类包括干系的抽象操纵。
我将在报告子范例多态前简朴先容一下这几种多态。
#p#副标题#e#
强制的多态
强制多态隐式的将参数按某种要领,转换成编译器认为正确的范例以制止错误。在以下的表达式中,编译器必需抉择二元运算符‘+’所应做的事情:
2.0 + 2.0
2.0 + 2
2.0 + "2"
第一个表达式将两个double的操纵数相加;Java中出格声明白这种用法。
第二个表达式将double型和int相加。Java中没有明晰界说这种运算。不外,编译器隐式的将第二个操纵数转换为double型,并作double型的加法。做对措施员来说十分利便,不然将会抛出一个编译错误,可能强制措施员显式的将int转换为double。
第三个表达式将double与一个String相加。Java中同样没有界说这样的操纵。所以,编译器将double转换成String范例,并将他们做串联。
强制多态也会产生在要领挪用中。假设类Derived担任了类Base,类C有一个要领,原型为m(Base),在下面的代码中,编译器隐式的将Derived类的工具derived转化为Base类的工具。这种隐式的转换使m(Base)要领利用所有能转换成Base类的所有参数。
C c = new C();
Derived derived = new Derived();
c.m( derived );
而且,隐式的强制转换,可以制止范例转换的贫苦,淘汰编译错误。虽然,编译器仍然会优先验证切合界说的工具范例。
重载的多态
重载答允用沟通的运算符或要领,去暗示截然差异的意义。‘+’在上面的措施中有两个意思:两个double型的数相加;两个串相连。别的尚有整型相加,长整型,等等。这些运算符的重载,依赖于编译器按照上下文做出的选择。以往的编译器会把操纵数隐式转换为完全切合操纵符的范例。固然Java明晰支持重载,但不支持用户界说的操纵符重载。
Java支持用户界说的函数重载。一个类中可以有沟通名字的要领,这些要领可以有差异的意义。这些重载的要领中,必需满意参数数目差异,沟通位置上的参数范例差异。这些差异可以辅佐编译器区分差异版本的要领。
编译器以这种独一暗示的特征来暗示差异的要领,比用名字暗示更为有效。据此,所有的多态行为都能编译通过。
强制和重载的多态都被分类为特定的多态,因为这些多态都是在特定的意义上的。这些被划入多态的特性给措施员带来了很大的利便。强制多态解除了贫苦的范例和编译错误。重载多态像一块糖,答允措施员用沟通的名字暗示差异的要领,很利便。
参数的多态
#p#分页标题#e#
参数多态答允把很多范例抽象成单一的暗示。譬喻,List抽象类中,描写了一组具有同样特征的工具,提供了一个通用的模板。你可以通过指定一种范例以重用这个抽象类。这些参数可以是任何用户界说的范例,大量的用户可以利用这个抽象类,因此参数多态毫无疑问的成为最强大的多态。
乍一看,上面抽象类仿佛是java.util.List的成果。然而,Java实际上并不支持真正的安详范例气势气魄的参数多态,这也是java.util.List和java.util的其他荟萃类是用原始的java.lang.Object写的原因(参考我的文章"A Primordial Interface?" 以得到更多细节)。Java的单根担任方法办理了部门问题,但没有发挥出参数多态的全部成果。Eric Allen有一篇出色的文章“Behold the Power of Parametric Polymorphism”,描写了Java通用范例的需求,并发起给Sun的Java规格需求#000014号文档"Add Generic Types to the Java Programming Language."(参考资源链接)
包括的多态
包括多态通过值的范例和荟萃的包括干系实现了多态的行为.在包罗Java在内的浩瀚面向工具语言中,包括干系是子范例的。所以,Java的包括多态是子范例的多态。
在早期,Java开拓者们所提及的多态就特指子范例的多态。通过一种面向范例的概念,我们可以看到子范例多态的强大成果。以下的文章中我们将仔细探讨这个问题。为简明起见,下文中的多态均指包括多态。
面向范例概念
图1的UML类图给出了类和范例的简朴担任干系,以便于表明多态机制。模子中包括5种范例,4个类和一个接口。固然UML中称为类图,我把它当作范例图。如"Thanks Type and Gentle Class," 一文中所述,每个类和接口都是一种用户界说的范例。按独立实现的概念(如面向范例的概念),下图中的每个矩形代表一种范例。从实现要领看,四种范例运用了类的布局,一种运用了接口的布局。
图1:示范代码的UML类图
以下的代码实现了每个用户界说的数据范例,我把实现写得很简朴。
/* Base.java */
public class Base
{
public String m1()
{
return "Base.m1()";
}
public String m2( String s )
{
return "Base.m2( " + s + " )";
}
}
/* IType.java */
interface IType
{
String m2( String s );
String m3();
}
/* Derived.java */
public class Derived
extends Base
implements IType
{
public String m1()
{
return "Derived.m1()";
}
public String m3()
{
return "Derived.m3()";
}
}
/* Derived2.java */
public class Derived2
extends Derived
{
public String m2( String s )
{
return "Derived2.m2( " + s + " )";
}
public String m4()
{
return "Derived2.m4()";
}
}
/* Separate.java */
public class Separate
implements IType
{
public String m1()
{
return "Separate.m1()";
}
public String m2( String s )
{
return "Separate.m2( " + s + " )";
}
public String m3()
{
return "Separate.m3()";
}
}
用这样的范例声明和类的界说,图2从观念的概念描写了Java指令。
Derived2 derived2 = new Derived2();
图2 :Derived2 工具上的引用
上文中声明白derived2这个工具,它是Derived2类的。图2种的最顶层把Derived2引用描写成一个荟萃的窗口,固然其下的Derived2工具是可见的。这里为每个Derived2范例的操纵留了一个孔。Derived2工具的每个操纵都去映射适当的代码,凭据上面的代码所描写的那样。譬喻,Derived2工具映射了在Derived中界说的m1()要领。并且还重载了Base类的m1()要领。一个Derived2的引用变量无权会见Base类中被重载的m1()要领。但这并不料味着不行以用super.m1()的要领挪用去利用这个要领。干系到derived2这个引用的变量,这个代码是不符合的。Derived2的其他的操纵映射同样表白了每种范例操纵的代码执行。
既然你有一个Derived2工具,可以用任何一个Derived2范例的变量去引用它。如图1所示,Derived, Base和IType都是Derived2的基类。所以,Base类的引用是很有用的。图3描写了以下语句的观念概念。
Base base = derived2;
图3:Base类引用附于Derived2工具之上
#p#分页标题#e#
固然Base类的引用不消再会见m3()和m4(),可是却不会改变它Derived2工具的任何特征及操纵映射。无论是变量derived2照旧base,其挪用m1()或m2(String)所执行的代码都是一样的。
String tmp;
// Derived2 reference (Figure 2)
tmp = derived2.m1(); // tmp is "Derived.m1()"
tmp = derived2.m2( "Hello" ); // tmp is "Derived2.m2( Hello )"
// Base reference (Figure 3)
tmp = base.m1(); // tmp is "Derived.m1()"
tmp = base.m2( "Hello" ); // tmp is "Derived2.m2( Hello )"
两个引用之所以挪用同一个行为,是因为Derived2工具并不知道去挪用哪个要领。工具只知道什么时候挪用,它跟着担任实现的顺序去执行。这样的顺序抉择了Derived2工具挪用Derived里的m1()要领,并挪用Derived2里的m2(String)要领。这种功效取决于工具自己的范例,而不是引用的范例。
尽量如此,但不料味着你用derived2和base引用的结果是完全一样的。如图3所示,Base的引用只能看到Base范例拥有的操纵。所以,固然Derived2有对要领m3()和m4()的映射,可是变量base不能会见这些要领。
String tmp;
// Derived2 reference (Figure 2)
tmp = derived2.m3(); // tmp is "Derived.m3()"
tmp = derived2.m4(); // tmp is "Derived2.m4()"
// Base reference (Figure 3)
tmp = base.m3(); // Compile-time error
tmp = base.m4(); // Compile-time error
运行期的Derived2工具保持了接管m3()和m4()要领的本领。范例的限制使Base的引用不能在编译期挪用这些要领。编译期的范例查抄像一套铠甲,担保了运行期工具只能和正确的操纵举办彼此浸染。换句话说,范例界说了工具间彼此浸染的界线。
多态的依附性
范例的一致性是多态的焦点。工具上的每一个引用,静态的范例查抄器都要确认这样的依赞同其工具的条理是一致的。当一个引用乐成的依附于另一个差异的工具时,有趣的多态现象就发生了。(严格的说,工具范例是指类的界说。)你也可以把几个差异的引用依附于同一个工具。在开始更有趣的场景前,我们先来看一下下面的环境为什么不会发生多态。
多个引用依附于一个工具
图2和图3描写的例子是把两个及两个以上的引用依附于一个工具。固然Derived2工具在被依附之后仍保持了变量的范例,可是,图3中的Base范例的引用依附之后,其成果淘汰了。结论很明明:把一个基类的引用依附于派生类的工具之上会淘汰其本领。
一个开拓这怎么会选择淘汰工具本领的方案呢?这种选择是间接的。假设有一个名为ref的引用依附于一个包括如下要领的类的工具:
public String poly1( Base base )
{
return base.m1();
}
用一个Derived2的参数挪用poly(Base)是切合参数范例查抄的:
ref.poly1( derived2 );
要领挪用把一个当地Base范例的变量依附在一个引入的工具上。所以,固然这个要领只接管Base范例的参数,但Derived2工具仍是答允的。开拓这就不必选择丢失成果的方案。从人眼在通过Derived2工具时所看到的环境,Base范例引用的依附导致了成果的丧失。但从执行的概念看,每一个传入poly1(Base)的参数都认为是Base的工具。执行机并不在乎有多个引用指向同一个工具,它只注重把指向另一个工具的引用传给要领。这些工具的范例纷歧致并不是主要问题。执行器只体贴给运行时的工具找到适当的实现。面向范例的概念展示了多态的庞大本领。
附于多个工具的引用
让我们来看一下产生在poly1(Base)中的多态行为。下面的代码建设了三个工具,并通过引用传给poly1(Base):
Derived2 derived2 = new Derived2();
Derived derived = new Derived();
Base base = new Base();
String tmp;
tmp = ref.poly1( derived2 ); // tmp is "Derived.m1()"
tmp = ref.poly1( derived ); // tmp is "Derived.m1()"
tmp = ref.poly1( base ); // tmp is "Base.m1()"
poly1(Base)的实现代码是挪用传进来的参数的m1()要领。图3和图4展示了把三个类的工具传给要领时,面向范例的所利用的体系布局。
图4:将Base引用指向Derived类,以及Base工具
请留意每个图中要领m1()的映射。图3中,m1()挪用了Derived类的代码;上面代码中的注释标明白ploy1(Base)挪用Derived.m1()。图4中Derived工具挪用的仍然是Derived类的m1()要领。最后,图4中,Base工具挪用的m1()是Base类中界说的代码。
#p#分页标题#e#
多态的魅力安在?再来看一下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的引用变量,并使其指向一个包括一下要领的类工具:
public String poly2( IType iType )
{
return iType.m3();
}
为了弄大白poly2(IType)中的多态,以下的代码从差异的类建设两个工具,并别离把他们传给poly2(IType):
Derived2 derived2 = new Derived2();
Separate separate = new Separate();
String tmp;
tmp = ref.poly2( derived2 ); // tmp is "Derived.m3()"
tmp = ref.poly2( separate ); // tmp is "Separate.m3()"
上面的代码雷同于关于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以及这范例的其他子范例应该划为一组。
#p#分页标题#e#
凭据这种完全差异于实现条理的分类要领,Java的接口机制是多态变得很利便,哪怕不存在任何共享的实现可能复写的要领。如图5所示,一个IType的引用,用多态的要了解见到了Derived2和Separate工具的m3()要领。
再次探讨工具的接口
留意图5中的Derived2和Separate工具的对m1()的映射要领。如前所述,每一个工具的接口都包括要领m1()。但却没有步伐用这两个工具使要领m1()表示出多态的行为。每一个工具占有一个m1()要领是不足的。必需存在一个可以操纵m1()要领的范例,通过这个范例可以看到工具。这些工具好像是共享了m1()要领,但在没有配合基类的条件下,多态是不行能的。通过工具的接口来看多态,会把这个观念搞混。
结论
从全文所述的面向工具多态所成立起来的子范例多态,你可以清楚地认识到这种面向范例的概念。假如你想领略子范例多态的思想,就应该把留意力从实现的细节转移到范例的上。范例把工具分成组,而且打点着这些工具的接口。范例的担任条理布局抉择了实现多态所需的范例干系。
有趣的是,实现的细节并不影响子范例多态的条理布局。范例抉择了工具挪用什么要领,而实现则抉择了工具怎么执行这个要领。也就是说,范例表白了责任,而认真实施的则是详细的实现。将实现和范例疏散后,我们仿佛看到了这两个部门在一起跳舞,范例抉择了他的舞伴和舞蹈的名字,而实现则是舞蹈行动的设计师。