依據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 <stdio.h>;
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 <stdio.h>;
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++編譯器對於每一個虛擬函數﹐均建立一個虛擬函數表與之應對﹐因爲每一個虛擬函數在一個繼承樹可能有許多子類別實作之。因此在實際執行時﹐是先找虛擬函數表﹐然後再尋找與自己類別階層等級最靠近的那個函數實作。所以我們可以將一個父類別指標轉換成子類別後﹐反而仍執行母親那輩的虛擬函數。
而Java則不能透過interface執行上述功能。Java的抽象函數(abstract function)事實上就是C++的純虛擬函數(virtual function()=0)﹐沒有像C++可以有非純虛擬函數(就是子類別不一定要定義的虛擬函數)﹐所以很難執行上面複雜的例子。並且﹐Java的interface裏的函數預設爲抽象函數﹐也就是如果某類別實作此interface的話﹐interface裏的所有函數都必須全部實作。因此在實際執行時﹐先決定interface物件轉型成某類別的物件﹐於是再執行該類別內的函數實作。
乍看之下﹐似乎C++的多重繼承功能較完整﹐那麼:
Java的interface概念﹐是否可用C++的多重繼承模擬出來呢?
目前﹐在微軟的OLE技術中﹐確實是用C++的多重繼承模擬OLE中的interface概念﹐可以參考MFC中關於OLE COM的寫作。如下面的程式:
class CoSomeObject : public Iunknown, public Ipersiet {
// IUnknown methods
virtual DWORD AddRef(void);
virtual DWORD Release(void);
virtual HRESULT QueryInterface(REFILD, LPVOID FAR*);
//IPersist methods
virtual HRESULT GetCLASSID(LPCLSID pclsid);
};
(注:參見MFC Internals p.p 442)
然而﹐考慮以下的問題:Java interface的特色爲﹐interface間可以繼承。但這裏所謂的繼承﹐事實上只是子interface包括全部的父interface內的成員﹐無法像Java的類別或是C++的類別那樣﹐可以用子類別的新定義將父類別取代之。這也就是所謂的「介面繼承」與「實作繼承」的差別。換言之﹐Java interface繼承(注意﹐不是子類別implements父interface)只是一種「包含關係」而已﹐甚至包含不可以重複。所以﹐interface概念用在軟體模組間的介面定義便非常厲害﹐如CORBA的IDL便是。因此﹐Java interface仍然擁有許多優點﹐但如果我們要以C++的多重繼承模擬(或稱之爲藉此學習Java interface概念)時﹐在C++這邊該如何映對呢?
我們以程式2的Java程式爲主﹐觀察C++的模擬interface版﹐該如何映對。
#include <stdio.h>;
class Test1 {
public:
virtual void f1()=0;
virtual void g1()=0;
};
class Test2 {
public:
virtual void f2()=0;
virtual void g2()=0;
};
class Test3 : public Test1, public Test2 {
public:
virtual void gg()=0;
};
class CTest : public Test3 {
public:
void f1() { puts("Test1::f1()"); }
void g1() { puts("Test1::g1()"); }
void f2() { puts("Test2::f2()"); }
void g2() { puts("Test2::g2()"); }
void gg() { puts("Test3::gg()"); }
};
void main() {
CTest ct; ct.f1();ct.f2();
ct.g1();ct.g2(); ct.gg();
}
// 程式輸出:
Test1::f1() Test2::f2() Test1::g1()
Test2::g2() Test3::gg()
程式4﹑C++對應的Java interface概念
由程式4可知﹐其實只要在每個類別中的每一個函數都宣告成純虛擬函數﹐則C++類別就變成Java interface概念了。
因此我們下一結論:C++的多重繼承功能較廣﹐Java的interface功能只是其中的一個子集。因爲C++的虛擬函數可以有純虛擬函數﹐也可有非純虛擬函數﹐而Java只有抽象函數﹐所以功能模式少一種﹐自然能達到的效果較少一些。
但這並不代表Java的interface就比較差﹐因爲interface的觀念較簡單﹐全部動態的抽象函數也正代表着Java爲一純物件導向語言。與C++不同的是﹐C++考慮許多執行效率的問題﹐所以語言本身就變的較複雜化﹐同時C++的編譯器也是公認難寫的﹐多重繼承更是一大挑戰。Java的interface雖然好象少了一些功能﹐但其實那些功能在一般的程式設計中也很少碰到﹔並且觀念簡潔的Java interface﹐在設計軟體程式時更可以避免許多陷阱。總之﹐兩種程式語言機制是很難比較誰好誰壞的。
順帶一提的是:最近有一股新趨勢﹐就是物件導向程式語言所引導出來的新的概念﹐反過來引響物件導向分析與設計的基礎理論。如Java的interface或是微軟的OLE Interface﹐使得Booch﹑OMT整合方法&shy;&shy;UML﹐對軟體模組間的介面有了新的定義﹔或是C++的STL與template﹐促進許多學者對靜態的物件關係又有了新解等。■
參考資料
[Ait96]Aitken, Gary. (1996) Moving from C++ to Java, Dr.Dobb’s Journal, Vol.21, Issue 3, A Miller Freeman Publication.
[Fla96]Flanagan, David. (1996) Java in a Nutshell, O’Reilly.
[Mor+96]Morrison, Michael. et al. (1996) Java Unleashed., Sams.
[Rie96]Reil, Arthur. (1996) Object-Oriented Design Heuristics, Addison-Wesley.
[[Str94]Stroustrup, Bjarne. (1994) The Design and Evolution of C++, Addison-Wesley.
文章來自 http://www.umlchina.com
|