図4 多重継承の優先順位<BR>メソッドを呼び出す優先順位がはっきりしない。
図4 多重継承の優先順位<BR>メソッドを呼び出す優先順位がはっきりしない。
[画像のクリックで拡大表示]
図5 親子関係にあるクラスの例&lt;BR&gt;変数からはサブクラスにしかないメソッドDを呼び出せない。
図5 親子関係にあるクラスの例<BR>変数からはサブクラスにしかないメソッドDを呼び出せない。
[画像のクリックで拡大表示]

多重継承から生まれる3つの問題

 もう少し,細かく見ていくと,多重継承の問題は以下の3点にまとまります。

●構成の複雑化

 単一継承では,あるクラスのスーパークラスは簡単に決まります。直接上のスーパークラス,そのスーパークラス,そのまたスーパークラス,…と一列に並ぶ単純な関係です。多重継承では,あるクラスに複数のスーパークラスがあり,その複数のスーパークラスそれぞれにさらに複数のスーパークラスがあるので関係が複雑になってしまいます。

●優先順位

 複雑な関係を持つスーパークラスがあるということは,クラス群の優先順位が一目で分からないということです。例えば図4[拡大表示]のようなクラス階層があるとします。Dがあるメソッドを受け継ぐ順番は,D,B,A,C,Objectなのか,D,B,C,A,Objectなのか,あるいは全く違う順序なのかが分かりません。一つに決まらないのです。クラスの優先順位がはっきり定まる単一継承とは対照的です。

●機能の衝突

 多重継承では複数のスーパークラスからメソッドなどの機能を受け継ぐことから,受け継いだメソッドの名称が衝突することもあります。図4の例では,クラスBとクラスCに同じ名前のメソッドがあった場合,どちらが有効になるのでしょうか。一意に定めることはできません。

多重継承の問題を解決するには

 ここまで多重継承の欠点を説明しましたが,SmalltalkのStreamの例をはじめとして多重継承がなければ解決できない問題が残っています。

 さらに,継承を抽象化の手段として考えるときには,なんからの形で多重継承の役割を果たす機能が必要なのです。共通するクラスの機能をくくり出すときに,1つのクラスにつき1つだけしか抽出できないと言う制限は厳しすぎるからです。

 多重継承のメリットを享受しつつ,問題を避けたいというのであれば,なんとか問題を解決する機能を考えるしかないでしょう。構造化プログラミングがgoto問題を解決した際の原則は,自由度の高いgotoの代わりに,gotoより制約された3種類の制御構造を導入するというものでした。この3種類の制御構造は制約されてはいますが,これらを組み合わせることで任意のアルゴリズムを記述できます。これに従えば,より制約がきつい多重継承を導入すれよさそうです。

 そこで,これらの問題を解決あるいは軽減するために登場した「制約された多重継承」とでも呼ぶべき機能が,Javaにおけるインタフェースであり,LispやRubyにおけるMix-inです。ここからは,これらがどのような機能であり,どのようにこれらの欠点を改善するのか見てみましょう。

静的型言語と動的型言語の違い

 最初に,Javaのインタフェースを調べてみましょう。

 インタフェースの仕組みを説明する前に,まずJavaのようなタイプのオブジェクト指向言語と多重継承について説明しておきます。

 オブジェクト指向言語は,大きく分けて「静的型言語」と「動的型言語」の2つに分かれます。変数や式に型情報が付けられているJavaのような言語のことを静的型言語と呼びます。

 静的型言語では型が異なる値を変数に代入できません。代入するとコンパイル・エラーになります。型の不整合はコンパイル時に見つかりますから,実行時になって「型が合わない」というエラーは発生しません。実行しなくてもエラーを見付けられる,これが静的型言語のメリットの一つです。

String str;
str = "abc"; // 問題なし
str = 2; // コンパイル・エラー

 オブジェクト指向言語では変数などの型をクラスで指定することが多いでしょう。上の例ではStringクラスです。しかし,オブジェクト指向言語を使う際,この例のように変数に特定のクラスのオブジェクト(そのクラスのインスタンス)しか代入できないという制限は厳しすぎます。なぜなら,ポリモーフィズムが働く余地がありません。ある変数に,必ず変数と同じクラスのオブジェクトが入っているのなら,オブジェクトのクラスによってふさわしい挙動を選ぶ(ポリモーフィズム)ことはあり得ないからです。

静的型言語の特徴

 そこで,静的型を持つオブジェクト指向言語では,あるクラスの変数にはそのクラスのオブジェクトに加えて,サブクラスのオブジェクトが代入できるように設計されています。これによってポリモーフィズムが実現できるわけです。

 図5[拡大表示]のプログラムを見てください。末尾に出てくる変数polyの型はPolygonですから,polyを介してPolygonクラスのメソッド(例えばメソッドC)を呼び出すことができます。しかし,実際に代入されているのはPolygonクラスのサブクラス*2であるRectangeクラスです。従って,呼び出されるのはRectangeクラスで定義されているメソッドです。RectangeメソッドでPolygonクラスのメソッドが再定義されていない場合は,そのままPolygonクラスのメソッドが呼び出されます。つまり,メソッドA',B',Cを呼び出せます。

 しかしながら,polyはあくまでもPolygonクラスとしてプログラムに登場していますから,たとえRectangeクラスのオブジェクトが代入されていると(人間には)明らかでも,poly変数を介する限りRectangeクラス固有のメソッド(メソッドD)を呼び出すことができません。

 言い換えれば,変数は実際に代入されているオブジェクトをのぞき見る窓のようなものと言えます。変数に代入されているオブジェクトが持つメソッドがどのようなものであっても,ある変数を介してメソッドを呼び出すときには,その変数の型が「知っている」メソッドしか呼び出すことはできません。

 メソッドDを呼び出してみると,静的型言語では無情にもコンパイル・エラーになってしまいます。

 これはRubyのような変数や式に型のない動的型の言語とは対照的です。これらの言語は変数を介して実際にオブジェクトのメソッドを呼び出してみて,見つからなければはじめてエラーにしています。

動的型言語の特徴

 動的型言語では継承関係に関係なくメソッドを呼び出せます。例えばRubyでは要素を順番に取り出すメソッドeachが用意されていて,配列,ハッシュ,文字列などにeachが備わっています。

obj.each {|x|
print x
}

 静的型言語では継承関係のあるメソッドしか呼べませんから,配列,ハッシュ,文字列のすべてに対して呼び出しをかけられるのは,これらに共通するスーパークラス(恐らくはObject)に所属するメソッドだけです。

 これが,後ほど説明するとした静的型言語における単一継承の欠点です。

 静的型言語では,クラス階層の木を横断してメソッドを呼び出したい場合,それらのオブジェクトすべてを表現できる「型」が必要です。そのような型がなければ,メソッドが呼び出しできる範囲が非常に狭くなるのです。静的型のオブジェクト指向言語ではなんらかの形の多重継承が欠かせないことが分かります。

静的型と動的型の比較

 静的型と動的型は対照的です。両者の手法の違いは一長一短なのです。静的型言語では,実際に実行しなくても漏れなく型の不整合が見付かりますから,プログラムの論理エラーのうち,ある程度の割合を自動的に検出できます。

 しかし,式や変数の一つひとつに型を指定する必要があるので,プログラムが冗長になりがちですし,なんらかの継承関係があるものだけしか,ポリモーフィズムの対象になりません。このような仕組みは,動的言語よりも制約が厳しく柔軟性が低いと言えます。

 動的型言語はちょうど反対です。いくつかのエラーは実行してみないと分かりませんから,プログラムの信頼性という観点で若干の不安があります。プログラムに型の情報がないということは,プログラムが簡潔になる半面,他人が書いたプログラムを解釈する際にヒントが少なくなります。

 しかし,とりあえず同じ名前のメソッドを持っているオブジェクトを同じように扱えます。つまり,型の階層について深く検討しなくてもプログラムを開発できるということになります。生産性という面からは大変ありがたいことです*3