作者:egreenworld
依据DDJ的C/C++专栏作家Al Steven表示:他虽然不是很懂得Java╋但是看到这些书中对于C++的物件导向概念的阐释╋有些地方明显错误╋真是令人担心。本文假设读者您已熟悉一些C/C++语言的概念╋对Java也有初步的认识。而谈论Java的interface与C++的多重继承之主要异同处。
interface与多重继承的观念
不管是Java的interface或是C++的多重继承╋在物件导向的理论里╋都算是蛮新颖的概念。所以这里我们谈的╋是以程式语言的角度╋看看Java interface的所有意义与功能╋是否C++的多重继承能全部诠释?或是相反地以Java的来诠释C++的。
首先让我们来复习一下什么是C++的多重继承。 「继承」通常在物件导向程式语言中╋扮演着程式码的重复利用的重责大任╋而C++的多重继承则让某一个子类别可以继承许多分属于不同资料型别的父类别如下:
#include
class Test1 {
public:
virtual void f1() {puts("Test1::f1()"); }
virtual void g1() {puts("Test1::g1()"); }
};
class Test2 {
public:
virtual void f2() { puts("Test2::f2()"); }
virtual void g2() { puts("Test2::g2()"); }
};
class Test3 : public Test1, public Test2 {
public:
virtual void gg() { puts("Test3::gg()"); }
};
void main() {
Test3 t3; t3.f1(); t3.f2();
t3.g1(); t3.g2(); t3.gg();
}
// 程式输出:
Test1::f1() Test2::f2() Test1::g1()
Test2::g2() Test3::gg()
程式1?C++的多重继承
根据[Rie96]╋认为正确使用物件导向技术中之「多重继承」观念╋应该如下面的例子:
假设有一个木造门╋则:
1. 此木造门是门的一种(a kind of)。
2. 但门不是木造门的一部份(a part of)。
3. 木造门是木制品的一种。
4. 但木制品不是木造门的一部份。
5. 木制品不是门的一种。
6. 门也不是木制品的一种。
所以您可以发现╋多重继承在使用时╋必须非常小心╋而且在许多时候╋其实我们并不需要多重继承的。
Java也提供继承机制╋但还另外提供一个叫interface的概念。由于Java的继承机制只能提供单一继承(就是只能继承一种父类别)╋所以就以Java的interface来代替C++的多重继承。interface就是一种介面╋规定欲沟通的两物件╋其通讯该有的规范有哪些。如以Java程式语言的角度来看╋Java的interface则表示:一些函数或资料成员╋为另一些属于不同类别的物件所需共同拥有╋则将这些函数与资料成员╋定义在一个interface中╋然后让所有不同类别的Java物件可以共同操作使用之。
所以╋对于Java的继承与interface╋我们总结如下:
1.Java的class只能继承一个父类别(用extends关键字)╋但可以拥有(或称实作)许多interface(用implements关键字)。
2.Java的interface可以继承许多别的interface(也是用extends关键字)╋但不可以实作任何interface。
因此╋我们可以利用Java的interface来模拟C++的多重继承。如上面的例子可以转化如下:
interface Test1 {
public void f1();
public void g1();
}
interface Test2 {
public void f2();
public void g2();
}
interface Test3 extends Test1, Test2 {
public void gg();
}
class CTest implements Test3 {
public void f1() { System.out.println("Test1::f1()"); }
public void g1() { System.out.println("Test1::g1()"); }
public void f2() { System.out.println("Test2::f2()"); }
public void g2() { System.out.println("Test2::g2()"); }
public void gg() { System.out.println("Test3::gg()"); }
}
class Run {
public void run() {
CTest ct=new CTest(); ct.f1();ct.f2();
ct.g1();ct.g2(); ct.gg();
}}
class Main {
public static void main (String args[]) {
Run rr=new Run();
rr.run();
}}
// 程式输出:
Test1::f1() Test2::f2() Test1::g1()
Test2::g2() Test3::gg()
程式2?利用Java的interface完成C++的多重继承功能
然而╋根据[Ait96]的文章显示╋他认为Java的interface比C++的多重继承好学很多╋也较容易懂╋但是有其限制。对于Java interface的易懂╋在文章中╋并没有说明。其主要即为「介面继承」与「实作继承」概念的差异。
「介面继承」就是只继承父类别的函数名称╋然后子类别一定会实作取代之。所以当我们以父类别的指标「多型」于各子类别时╋由于子类别一定会实作父类别的多型函数╋所以每个子类别的实作都不一样╋此时我们(使用父类别指标的人)并不知道此多型函数到底怎么完成╋因之称为「黑箱设计」。
「实作继承」就是继承父类别的函数名称╋子类别在实作时╋也会用到父类别的函数实作。所以我们(使用父类别指标的人)知道此多型函数怎么完成工作╋因为大概也跟父类别的函数实作差不多╋因之称为「白箱设计」。
套用的Java的interface上╋我们发现╋Java的interface就是介面继承╋因为Java interface只能定义函数名称╋无法定义函数实作╋所以子类别必须用「implements」关键字来实作之╋且每个实作同一介面的子类别当然彼此不知道对方如何实作╋因此为一个黑箱设计。
Java的类别继承则为实作继承╋子类别会用到父类别的实作(更正确地说应该是父类别有定义实作╋所以子类别可能会使用到╋即使不使用到也会遵循父类别实作的演算法)╋所以父类别与子类别有一定程度的相关性?不像介面继承╋彼此只有函数名字刚好一样而已。
介面继承与实作继承╋应对至Java的interface?class?extends与implements关键字╋很容易了解其含意。但是C++的继承机制╋似乎就没有那么容易解释清楚的!所以这就是[Ait86]文章中所表示的意思:C++多重机制比较复杂。
所以接下来我们将讨论:
C++的多重继承有什么功能╋是Java的interface所达不到的?
在C++的ARM中╋或是[Str94]的多重继承章节里╋皆提到了下述著名的例子:
#include
class t1 {
public:
virtual void f() { puts("t1::f()"); }
virtual void g() { puts("t1::g()"); }
};
class t2 : public virtual t1 {
public:
virtual void g() { puts("t2::g()"); }
};
class t3 : public virtual t1 {
public:
virtual void f() { puts("t3::f()"); }
};
class t4 : public t2, public t3, public virtual t1 { ...};
void main() {
t4 *tt4=new t4; t2 *tt2=tt4; t3 *tt3=tt4;
tt4-f(); tt4-g(); tt2-f(); tt3-g();
}
// 程式输出:
t3::f() t2::g() t3::f() t2::g()
程式3?C++著名的环状继承
由上例╋我们发现╋C++的多重继承具有下列两个特质是Java的interface所不能达到的功能。
图1?C++的环状多重继承
C++的多重继承可以形成环状继承关系╋如图1。但是不管是Java的继承机制或是interface╋都不容许有环状的情况发生。换句话说╋因为C++有virtual base的属性的父类别╋所有在多重继承时╋允许父类别被继承两次以上。但Java则完全不行。
本题中的tt4指标╋转成tt2指标后╋执行f()函数时╋仍然会正确地执行tt4中的f()函数╋也就是t3::f()。我们可以发现╋这种找函数的方式╋是先找函数的正确名称╋再找函数所属的类别的正确名称。与Java的虚拟函数(或称为abstract函数)不同。Java的是先找指标(或参考)所属的正确类别名称╋再继续找类别名称下的正确函数名称。
图2?对于虚拟函数C++与Java的各别作法
对于第二点参考图2。C++的虚拟函数╋可以参考[Sou94]╋C++编译器对于每一个虚拟函数╋均建立一个虚拟函数表与之应对╋因为每一个虚拟函数在一个继承树可能有许多子类别实作之。因