C++ vs. Java (1)
きょうびのソフト屋さんはマルチリンガルであることが以前より増して求められるようになってきました。特に昨今のinternetブームに乗って、Javaの必要性が日増しに増大しています。
幸いにもJavaはC++と非常に似ていますから、僕たちC++屋には比較的楽に習得できます。
とはいえやはりJavaはJava。C++と異なる部分も多く見受けられますし、その違いがプログラムの構造やスタイルまで左右することも少なくありません。
JavaとC++はどこがどう違うのでしょう。そしてどちらが優れているといえるのでしょうか…
多重継承 vs. 単一継承
Javaの設計者はmultiple inheritance:多重継承を許しませんでした。その代わり、Javaでは'interface'と呼ばれる、インタフェースを宣言する構文を用意し、そして複数のインタフェースをクラスにサポートさせることにしました。
Javaにおけるinterfaceは純粋仮想関数だけで構成されたC++クラスとほとんど同一に見えます。Javaは複数の基底クラスから導出することを許しません。たとえその基底クラスが抽象メソッド(純粋仮想関数)だけで構成されていてもです。多重継承の代替手段として複数のinterfaceをimplementすることが可能です。
例をあげましょう。Javaでは次のようなインタフェース:Stackが作れます:
public interface Stack { public void Push(Object o); public Object Pop(); }
この構造はおおむね次のC++コードと等価です:
class Stack { public: virtual void Push(Object& o) =0; virtual Object& Pop() =0; }
しかし、この二つは同じものではありません。Javaのinterfaceはclassではないのです。interface内の関数は実装することができません。さらに、メンバ変数を持つことも許されません。
interfaceがメンバ関数の実装やメンバ変数を持つことができないのは、そうすることによってC++での仮想継承によって引き起こされる問題を回避するためです。
仮想継承を避けることにより、ひとつのメンバ変数をたどるパスをただひとつに限定できます。
C++では多重継承によって引き起こされる"死のダイヤモンド"と呼ばれるやっかいなシチュエーションが起こります。
上の図はその"死のダイヤモンド"のオブジェクト図です。クラス B と C の両方が A から導出され、そして D が B と C から多重継承しています。
ここで二つの問題が発生します。
ひとつめ: クラス D が継承するのメンバ関数 f はどれでしょう? B::f() でしょうか、それとも (D->C->Aとたどって) A::f() でしょうか?
答えはどちらでもないです。C++では D::f()の呼び出しはコンパイルエラーとなります。
Javaではメソッドをたどる道が一本しかないのでこのような状況は起こりえません。
ふたつめ: クラス A はメンバ変数 i をもっており、B と C は基底クラス A の持つ i を継承します。
クラス D は B とC の双方から導出されているために、ここで'あいまいさ'が生じます。
D において B::i と C::i を別のものとして扱うなら、D は ふたつの Aを内包することになりますし、
同一のものとして扱うなら D は Aをひとつだけ内包しなければなりません。
C++ではこのあいまいさを解消するため、継承に'オプション'を設けてあります。
D が Aをふたつ内包したいとき、通常の継承:
class A { public: int i; virtual void f(); }; class B : public A { public: int j; virtual void f(); }; class C : public A { public: int k; }; class D : public B, public C { };
を行い、D が A を一つだけ内包するのなら 仮想継承 を利用します。
class A { public: int i; virtual void f(); }; class B : virtual public A { public: int j; virtual void f(); }; class C : virtual public A { public: int k; }; class D : public B, public C { };
仮想継承は複雑な機能であり、コンパイラ実装者とアプリケーションプログラマの双方を混乱させます。
Javaは仮想継承が引き起こす複雑さを避けるため、多重継承を禁止し、その代わりに複数のインタフェースを実装すること許しました。
これによって"死のダイヤモンド"があいまいさを引き起こすことを防いだのです。
これはJavaにとって適切なトレードオフであったといえるでしょう。多重継承を避けることで言語仕様をシンプルにできました。
しかしその代償として、複数のクラスから実装を継承できなくなりました。
複数のクラスから実装を継承したくなることは決して珍しいことではないのですが、Javaはそれを許さないのです。
Observerパターンを使った次の例(C++コード)を考えましょう:
class Clock { public: virtual void Tick() { itsTime++; } private: int itsTime; };
Clockは定期的にメソッドTickを呼び出すことで経過時間を保持することができます。
さて、そこでClockの監視バージョンを作るとします。すなわち一定期間が経過するごとに他のクラスにその旨を通知させようというものです。
このような、状態の変化を他のオブジェクトに通知するからくりとして、Observerパターンを用いるのが一般的です。
Observerパターンでは、ふたつの基底クラスを用意します。
ひとつは Observerで、純粋仮想関数 Update()が宣言されています。
状態変化を通知して欲しいクラスは、このObserverから導出させなくてはなりません:
class Observer { public: virtual void Update() =0; };
そしてもうひとつはObserverに状態の変化を通知するSubjectです。
SubjectはObserverの集合を持っており、ふたつのメソッドを持っています。
ひとつは通知先を登録するRegister、もうひとつが登録されているObserverに状態の変化を通知する Notify です。
class Subject { public: void Register(Observer& o) { itsObservers.push_back(&o); } void Notify() { for ( int i = 0; i < itsObservers.size(); ++i ) { itsObservers[i]->Update(); } } private: std::vector<observer> itsObservers; };
他のオブジェクトの状態の変化を通知してもらいたいクラスはObserverから導出し、オブジェクトの状態の変化を他のオブジェクトに通知したいクラスはSubjectから導出します。
ObserverはあらかじめSubjectに登録(Register)しておかなくてはなりませんし、Subjectは状態変化が発生したらその旨を通知(Notify)しなければなりません。
さて、Clockに話を戻しましょう。
すべてのアプリケーションにおいてClockを監視しなければならないわけではありませんから、ClockをSubjectの派生クラスとしたくはありません。そんなことをすれば必要もないのにSubjectをincludeすることになるからです
ここで多重継承を利用します。
他のオブジェクトに時間の経過を通知するClockが必要となったら、Subject と Clock を継承した ObservedClock を作りましょう:
class ObservecClock : public Clock, public Subject { public: virtual void Tick() { Clock::Tick(); Notify(); } };
多重継承はこの例では非常にシンプルかつエレガントな解決をもたらしてくれます。
ClockはSubjectから切り離されているし、必要ならば ObservedClockを使うこともできます。
さて、一方Javaでは多重継承による解決が許されていません。以下のような実装が必要となります:
public class Clock { public void Tick() { itsTime++; } private int itsTime; } public interface Observer { public void Update(); } public interface Subject { public void Register(Observer o); public void Notify(); }
クラス Clock はC++版とほとんど同じです。ObserverもC++と同様です。単にclassがinterfaceとなっているだけです。
しかしSubjectはC++と大きく異なります。JavaではSubjectもinterfaceとなります。
C++ではSubjectにメンバ変数とメソッドの実装を内包していますが、JavaではSubjectを実装するクラス SubjectImpl を定義します:
public class SubjectImpl implements Subject { { public void Register(Observer o) { itsObservers.addElement(o); } public void Notify() { Enumeration i = itsObservers,elements(); while ( i.hasmoreElements() ) { Observer o = (Observer)(i.nextElement()); o.Update(); } } private Vector itsObservers; }
SubjectImplもまた、C++でのSubjectととても類似しています。
さて、JavaはC++のような多重継承を許しません。Clock と SubjectImpl の両方を継承することができないのです。
その代替策として、ObservecCloskは Clock から導出し、そして Subjectをimplementsします。
public class ObservedClock extends Clock implements Subject { { public void Tick() { super.Tick(); Notify(); } public void Notify() { itsSubjectImpl.Notify(); } public void Register(Observer o) { itsSubjectImpl.Register(o); } private SubjectImpl itsSubjectImpl; }
Subjectをimplementsするために、SubjectImplをメンバとして持ち、Subjectの機能はすべてSubjectImplに委譲しています。多重継承を許さないなら、こうするしかないのです
C++ | Java |
---|---|
Clock.h | Clock.java |
Observer.h | Observer.java |
Subject.h | Subject.java SubjectImpl.java |
Application.cpp | Application.java |