目次


Javaコードの診断:痛みを伴わないJava総称クラス 第4回

総称型でどのようにしていたずら好きなミックスインを克服するか

JSR-14とTigerの総称型について考察するこのミニ・シリーズでは、これまでに次のようなことを学習しました。

  • 総称型、および総称型をサポートするために計画されている今後の機能
  • プリミティブ型に対する制限、制約付きの総称クラス、および多義的メソッド
  • これらのJavaの拡張に課せられたいくつかの制限
  • これらの拡張言語のコンパイラーで採用されている実装ストラテジーによって、必然的に制限が発生する経緯
  • 総称型の「ネイキッドな」型パラメーターにnew 操作のサポートを追加することによる効果

今月の記事では、総称型がもたらす最も強力な機能だと考えられるミックスインを処理するために、前もって対処しておかなければならない問題を取り上げ、Java言語の総称型に関する解説を締めくくります。

ミックスイン対ラッピング

ミックスインは、自身の親クラスによってパラメーター化されるクラスです。たとえば、次の総称クラスの例をご覧ください。このクラスは、自身の型パラメーターを継承しています。

class Scrollable<T> extends T {...}

クラスScrollable の目的は、GUIウィジェットにスクロール機能を追加するために必要な機能を埋め込むことです。この総称クラスを使用すると、作成されるインスタンスでは、そのたびにそれぞれ個別の親クラスを継承します。たとえば、Scrollable<JTextPane>JTextPane のサブクラスとなり、Scrollable<JEditorPane>JEditorPane のサブクラスになります。Java Swingライブラリーでこのような機能の埋め込みを行う場合と対比してみてください。Swingライブラリーの現在の機能では、JComponent をスクロール可能にするには、これをJScrollPane でラッピングする必要があります。

ラッピングでは、ラップされているクラスの機能にアクセスするために転送メソッドを追加する必要があるだけでなく、ラップされているオブジェクトのインスタンスが必要な場合に、ラッピング結果であるスクロール可能なオブジェクトを使用できません(たとえば、JTextPane のインスタンスを要求するメソッドにJScrollPane を渡すことはできません)。Scrollable をその親クラスでパラメーター化することにより、複数のスーパークラスを継承しながら、スクロールに関連する機能を1箇所で管理することができます。このように、ミックスインを使用できることによって、バグやエラーに悩まされることなく多重継承の持ついくつかの強力な機能を利用できるようになります。

前述の例では、型パラメーターに制約を設定して、このクラスが不適切な状況で使用されないようにすることもできます。たとえば、型パラメーターをJComponent のサブクラスに限定したい場合は、次のようになります。

class Scrollable<T extends JComponent> extends T {...}

こうすることにより、この例のミックスインではGUIコンポーネントの継承だけが可能になります。

ミックスインと総称クラス: 完璧な組み合わせ

多くの場合、ミックスインは独立した言語機能として言語に追加されます。たとえば、Jamなどがそうです。しかし、ミックスインを総称型システムの一部として組み込むことは、魅力的であり、そうしないではいられないほどです。それは、ミックスインと総称クラスを、どちらも既存のクラスを新しいクラスにマッピングする関数と考えることができるからです。

総称クラスは、自身の引数を新しいインスタンス生成にマッピングする関数ととらえることができます。一方、ミックスインは、既存のクラスを新しいサブクラスにマッピングする関数と考えることができます。総称型を使用してミックスインを組み込むことにより、他の方法によるミックスインの主な制限の多くを回避することができます。

Java言語のJamによる拡張では、ミックスインのスーパークラスの型に名前がなく、ミックスインの本体内でその型を参照することができません。この制限は、雪だるま式に他のあらゆる種類の問題を併発します。たとえば、Jamでは、プログラマーはthis を引数としてメソッドに渡すことはできません。それは、このような呼び出しで型チェックを行う方法がないからです。一般に広く使用されているデザイン・パターンの多くが、引数としてthis を渡すことができることに依存しているため、この制限は大きなダメージになります。

たとえば、複合階層内の各クラスにfor メソッドを使用してビジター・クラスが定義されているビジター・パターンを考えてみてください。一般に、ビジターを受け入れる側のクラスにはaccept メソッドが含まれています。このメソッドは、ビジターを受け取り、this を渡してそのビジターのメソッドを呼び出します。したがって、Jamでは、ミックスインでビジター・パターンを使用することはできません。

総称クラスとして作成されたミックスインの場合は、必ず親クラスへのハンドル、つまり、そのクラスが継承する型パラメーターが存在します。たとえば、Scrollable の親クラスは、型T として参照できます。そのため、型引数としてthis を渡せるようにすることは、根本的には問題になりません。

しかし、ミックスインを総称型として作成することについては、これ以外に大きな問題がいくつか存在します。考えられる問題の一部について、その概要をつかんでいただくために、ここでは特に顕著な問題と、その問題に対する有望な解決策について説明したいと思います。

ミックスインと型消去

他の問題について述べる前に、まず指摘しておかなければならないことがあります。それは、先月の記事で考察した総称型の機能拡張と同じで、JSR-14やTigerで採用されている単純な型消去ストラテジーを使用するだけでは、Java言語にミックスインのサポートを追加することはできないということです。

その理由を知るために、ある型パラメーターを継承するクラスが消去された場合に何が起こるかを考えてみましょう。このクラスは、その型パラメーターの上位の型を継承することになります。たとえば、前出のScrollable では、インスタンス化のたびにJComponent が継承されることになります。これは、明らかに意図した動作と違います。

総称型でミックスインをサポートするためには、総称型のインスタンス化を実行時に表現できる方法が必要です。幸い、実際にはTigerと下位互換でこの情報をエンコードする方法がいくつか存在します。このような下位互換のエンコード方式は、NextGenに記述されたGenericJava (参考文献のセクションを参照) の優れた特徴です。

スーパークラスの使用可能なコンストラクター

型パラメーターを継承するクラスを考慮するにあたってすぐに直面する差し迫った問題は、呼び出し可能なスーパーコンストラクターの決定です。すべてのJavaクラスのコンストラクターは、スーパークラスのコンストラクターを呼び出す必要があることを思い出してください。通常は、型チェッカーがスーパークラスを検索し、対応するスーパーコンストラクターが存在することを確認することにより、このようなスーパーコンストラクターの呼び出しが確実に成功するようになっています。

しかし、スーパークラスがある型パラメーターから生成される何らかのインスタンスであることしかわからない場合には、任意のインスタンス化にどのようなコンストラクターが使用できるのか知ることは不可能です。また、型チェッカーは、ミックスインのすべてのインスタンス化において、有効なスーパーコンストラクター呼び出しが行われるかどうかをチェックすることもできません。なぜなら、ミックスインのパラメーターが、型パラメーターの上位の型でインスタンス化される場合もあるからです。

たとえば、総称クラスJSplitPane<T> は、Scrollable<T> のインスタンスを生成する可能性があります。型パラメーターTJSplitPanes でインスタンス化されるすべての方法を把握していない限り、Scrollable<T> で呼び出されるスーパーコンストラクターが有効であるかどうかを知ることはできません。しかし、Javaのコーディングではクラスを個別にコンパイルできるため、型チェックの時点でJSplitPane のすべてのインスタンス化の種類を知ることはできません。

この問題に対するさまざまな解決策は、先月の第3回の記事で取り上げた、型パラメーターのnew 式をチェックするために提案された解決策と一致します。これは、スーパーコンストラクター呼び出しとnew 式が、両方とも任意のクラスの同じクラス・コンストラクターを指しているためです。それでは、その解決策を復習してみましょう。

  • すべての型パラメーターのインスタンス化でゼロ引数 (zeroary) コンストラクターを必須化する。
  • 一致するコンストラクターがない場合は、実行時に例外をスローする。
  • 型パラメーターに追加の注釈を組み込み、このようなインスタンス化に含まれるべきコンストラクターを指示できるようにする。

new 式の場合と同様に、最初の2つの解決先には重大な欠点があります。多くの場合、クラス定義にゼロ引数コンストラクターを組み込むことには意味がありません。また、一致するコンストラクターが存在しない場合に単に例外をスローするというのも、理想的ではありません。結局、静的型チェックの意義は、このような例外を防止することに他ならないからです。

3番目の解決策は冗長になる可能性がありますが、多くの長所があります。型パラメーターに、すべてのインスタンス化で使用可能にするべきコンストラクターのセットを示す注釈を付ける方法です。このような注釈により、その型パラメーターで安心して呼び出すことのできるコンストラクターを知ることができます。したがって、型パラメーターT が総称クラスのスーパークラスとして使用されている場合は、T の注釈からどのスーパーコンストラクターを呼び出すことができるかを正確に知ることができます。T に注釈が含まれていない場合は、型チェッカーで、その型パラメーターのスーパークラスとしての使用を許可しないようにします。

メソッドの偶発的なオーバーライド

どのような方法のミックスインでも発生する大きな問題の1つは、特定のミックスインのメソッド名が、そのスーパークラスで生成される可能性のあるインスタンスのメソッド名と重複する場合があることです。たとえば、前出のクラスScrollable に、引数を取らず、縦と横の寸法をエンコードしたSize オブジェクトを返すメソッド、getSize が含まれていたとします。ここで、クラスMyTextPane (JComponent のサブクラス) にも、引数を取らず、呼び出し元オブジェクトの画面領域を表すint を返すメソッド、getSize が含まれていたとします。

結果のクラスは、次のようになります。

リスト1. メソッドの偶発的なオーバーライドの例
class Scrollable<T extends JComponent> extends T { ... Size getSize() {...}
}
class MyTextPane extends JComponent { ... int getSize() {...}
}
new Scrollable<MyTextPane>()

ミックスインのインスタンス化であるScrollable<MyTextPane> には、同じ (空の) パラメーターを持ちながら、戻り値の型に互換性のない2つのgetSize メソッドが含まれることになります。この問題のあるgetSize のオーバーライドについて予測することは、クラスScrollable のプログラマーにもMyTextPane のプログラマーにも期待できなかったと考えられることから (結局この2人のプログラマーは同じ開発チームですらなかったかもしれません)、このような状況は偶発的なオーバーライドと呼ばれます。

ミックスインが総称クラスとして作成されている場合、この偶発的なオーバーライドの問題は特に厄介です。ミックスインの親は型パラメーターでインスタンス化されている可能性があるため、型チェッカーにはメソッドの偶発的なオーバーライドのすべてのケースを判別する手段がありません。さらに、偶発的なオーバーライドが発生したときに実行時例外をスローするという方法も、クライアントのプログラマーがそのような例外がスローされる場合を予測できないことから、許容できる方法ではありません。信頼できるプログラムを作成するのであれば、実行時に予測不可能なエラーが発生する可能性を残すことはできません。

もう1つの解決策として、名前の重複するこれらのメソッドの一方を隠蔽し、その名前に一致するすべてのメソッド呼び出しが、隠蔽されていないメソッドを参照するように解決するという方法があります。この解決策の問題点は、Scrollable<MyTextPane> のようなミックスインのインスタンス化を、Scrollable オブジェクトを必要とするコンテキストとMyTextPane オブジェクトを必要とするコンテキストの両方で使用できるようにしたいということです。どちらか一方のgetSize メソッドを隠蔽すれば、Scrollable<MyTextPane>s をこのような両方のコンテキストで使用することはできなくなります。

総称型以外のミックスインの場合は、Felleisen氏、Flatt氏、およびKrishnamurthi氏が、1998年のACMSIGPLAN-SIGACT Symposium on Principles of Programming Languagesで、この問題に対する優れた解決策を提案しています(参考文献を参照)。その内容は、ミックスインのインスタンス化が使用されているコンテキストに基づいて、名前の重複するメソッドへの参照を解決するというものです。この解決策では、ミックスインは、メソッド名が重複した場合に呼び出すメソッドを決定するためのビューに関連付けられています。

総称型として作成されたミックスインの場合にも、同じ解決策の適用が可能です。そのためには、総称型のコンテキストで有効に使用でき、JVMと下位互換性のある、ビューにあたるものを考案する必要があるだけです。Rice大学のJavaPLT研究室では、"AFirst-Class Approach to Genericity" (参考文献を参照) という論文の中で既にそのような解決策を提案しています。

高い機能には付随する問題がある

さまざまな例、問題、および有望な解決策が示しているように、Javaプログラミングの総称型を拡張してミックスインのサポートを組み込めば、強力な言語ができます。しかし、それは同時に、克服しなければならない問題が生まれるということでもあります。多くの既存の機能を複雑化することによってしか望ましい機能を追加できないということは、プログラミング言語設計の特徴でもあります。プログラミング言語の世界では、ただでランチが食べられることなどありません。


ダウンロード可能なリソース


関連トピック

  • Allen氏、Bannet氏、およびCartwright氏による「 A First-Class Approach to Genericity」(PDF) では、Javaの総称クラスを拡張してミックスインを組み込むことに伴う多くの問題が取り上げられ、その解決策が提案されています。
  • 同じ著者による2002年4月のコラム「「付け足し初期化コード」バグ・パターン」で、異質なゼロ引数コンストラクターの組み込みがどのように問題につながるかを学んでください。
  • JSR-14プロトタイプ・コンパイラーをダウンロードして、Javaプログラミングの総称クラスを体験してください。このコンパイラーには、拡張言語で記述されたプロトタイプ・コンパイラーのソース、コンパイラーの実行および起動用のクラス・ファイルを含むJARファイル、およびコレクション・クラス用のスタブを含むJARファイルが含まれています。
  • NextGen プロトタイプ・コンパイラーを今すぐダウンロードできます。
  • Javaのステートメントと式を対話的に評価し、総称Javaの構文とコンパイルをサポートする無料のJavaIDE、DrJava もお試しください。
  • Javaへの総称型の追加に関するディスカッションについては、Javaコミュニティー・プロセスの提案JSR-14 を参照してください。
  • developerWorks のJava technology zone に掲載されている、Javaテクノロジーに関するその他の記事やチュートリアルもお読みください。
static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=219319
ArticleTitle=Javaコードの診断:痛みを伴わないJava総称クラス 第4回
publish-date=05132003