Java コレクション API の設計に関する FAQ |
これは、API 全体でもっとも議論の余地のある設計上の決定事項です。 静的な (コンパイル時の) 型チェックが望ましいのは明らかで、Java の標準でもあります。 可能ならばサポートしたかったのですが、これをサポートするとインタフェース階層のサイズが非常に大きくなり、実行時の例外の必要性を (かなり減らすことはできても) なくすことができません。
Doug Lea は、インタフェース階層に可変性の特徴を反映したポピュラーな Java コレクションを作成しました。 しかし、そのコレクションパッケージのユーザとしての経験から、このアプローチはもはや実行可能ではないと考えています。 彼は、私信の中で「これを言うのはつらいが、強力な静的型設定は Java のコレクションインタフェースでは機能しない」と述べています。
問題を詳しく説明するために、階層に修正可能性という概念を追加しようとする場合、ModifiableCollection、ModifiableSet、ModifiableList、ModifiableMap の 4 つの新しいインタフェースが必要になります。 以前は単純だった階層が複雑になります。 また、削除オペレーションを持たない修正不可能なコレクションで使用するための新しい反復子が必要になります。 この状態で、UnsupportedOperationException を排除することはできません。
配列について考えます。 配列にはほとんどの List オペレーションが備わっていますが、削除と追加のオペレーションはありません。配列は「固定サイズ」の List です。 階層にこの概念を与えようとすると、VariableSizeList と VariableSizeMap の 2 つの新しいインタフェースが必要になります。 VariableSizeCollection と VariableSizeSet は、ModifiableCollection と ModifiableSet に等しいので追加する必要はありませんが、一貫性のために追加しなければならない場合があります。また、変更不可能な List に対処するため、追加と削除のオペレーションをサポートしない新種の ListIterator が必要です。 最初に必要だったのは 4 つのインタフェースだけでしたが、このように 10 〜 12 個のインタフェースに加えて 2 個の新しい反復子インタフェースが必要になります。 しかし、それでも十分ではありません。
ログ (エラーログ、監査ログ、修復可能なデータオブジェクト用のジャーナル) について考えます。 これらは、本質的に追加専用シーケンスで、削除と設定 (置換) 以外のすべての List オペレーションをサポートします。 ログには、新しいコアインタフェースと新しい反復子が必要になります。
修正不可能なコレクションとは対照的な、不変性のあるコレクション (つまり、クライアントによる変更が不可能で、かつ他のどんな理由によっても変化しないコレクション) についてはどうでしょうか。 多くの人が、これがもっとも重要な特徴であると主張しています。 その理由は、複数のスレッドが (同期の必要なく) 同時に 1 つのコレクションにアクセスできるからです。 型の階層にこれを追加するには、さらに 4 つのインタフェースが必要です。
これで、インタフェースの数は最大 20 個程度、反復子は 5 個になりました。 実際には、明らかにどのインタフェースにも適合しないコレクションが増えているのはほぼ確実です。 たとえば、Map から返されるコレクションビューは本質的に削除専用コレクションです。 また、値に基づいて特定の要素を拒否するコレクションもあるので、実行時の例外を排除することはできません。
議論と試行の末、実行時例外を発行できるコアインタフェースのごく小さなセットを提供することで問題全体を回避することが、道理にかなった技術的妥協案であるとの結論に達しました。
プログラムでこれらの例外をキャッチする必要はないと考えています。 これらの例外がチェックされない例外 (実行時例外) なのは、そのためです。 これらの例外は、プログラミングエラーの結果としてだけ発生するため、この場合は、例外がキャッチされないためにプログラムが機能停止します。
Collection インタフェースにこの機能があります。 このインタフェースが他のインタフェースのように頻繁に利用されるとは考えられないので、public の実装はしていません。 AbstractCollection (たとえば、Map.values から返されるコレクション) の上に簡単に実装されるようなコレクションを随時返します。
型保証されるコレクションの要望には大いに賛成します。 場当たり的な型の安全性を得るための応急処置をフレームワークに追加するのではなく、現在議論されているパラメータ化された型の提案すべてに適応するようにフレームワークは設計されています。 パラメータ化された型が言語に追加されたときには、すべてのコレクションのフレームワークが、明示的なキャストの必要がない「コンパイル時の」型安全性の確保をサポートします。 1.2 リリースではこれは実現しません。 それまでの間、実行時の型保証が必要な場合は、JDK コレクションを囲む「ラッパー」コレクションに独自のゲート機能を実装することができます。
新しいコレクションメソッドの名前は「Beans の命名規約」には固執していませんが、妥当で一貫性があり、目的に適した名前だと考えています。 Beans の命名規約は、JDK 全体に適用されているわけではありません。 AWT はこれらの規約を採用しましたが、この決断は議論を呼ぶものでした。 コレクション API は、極めて広範に使用されるので、1 行のコードで複数のメソッドが呼び出されることが多く、名前が短いことが重要だと考えます。 たとえば、Iterator メソッドについて考えてみましょう。 現在、1 つのコレクションに関するループは次のようなものです。
for (Iterator i = c.iterator(); i.hasNext(); ) System.out.println(i.next());コレクションの名前が長い場合でも、すべてが 1 行に収まります。 メソッドの名前を getIterator、hasNextElement、getNextElement とした場合、1 行には収まりません。 このような理由から、Beans 方式ではなく「従来の」JDK 方式を採用しました。
多くの Collection 実装 (JDK が供給するものもすべて含め) には public の clone メソッドがありますが、すべての Collection にこのメソッドを要求するのは誤りです。 たとえば、1 テラバイトの SQL データベースを基にする Collection の複製を作るとはどういうことでしょうか。 メソッドを呼び出すことにより、会社が新しいディスク装置ファームを請求するようなことになっても良いのでしょうか。 同様な議論は、いくらでもあります。
クライアントが Collection の実際の型を知らない場合、クライアントがどの型の Collection が必要なのかを決定し、その型の空の Collection を作成し、addAll メソッドを使って元のコレクションの要素を新しいコレクションにコピーする方が、より柔軟性が高くエラーが起こりにくくなります。
これは、『Design Patterns』(Gamma ほか著) という本で「Internal Iterator」と呼ばれているものです。 これを提供することについては考慮しましたが、内部および外部反復子をサポートするのは冗長であると思われ、また Java にはすでに外部反復子用の機能 (Enumeration に対して) があるので、提供しないことに決めました。 アップコールを記述するために public のインタフェースが必要であるため、この機能の「投射重量」は増大してしまうのです。
Iterator の上にこの機能を実装するのは簡単で、ユーザが述語を直列に並べることができるため、結果のコードはより簡潔になります。 しかし、この簡潔さが十分役に立つかどうかは明確ではありません。 役立つと思われる場合は、Collection クラスにあとからこれを追加 (Iterator の上に実装) することができます。
「代用のコレクション」として Enumeration (または Iterator) を使用しない方が良いと考えるからです。 この使用方法は、以前のリリースではしばしば行われていましたが、今回のリリースでは Collention インタフェースがあるので、オブジェクトの抽象コレクションを次々にまわす方が良い方法です。
これも、「代用のコレクション」としての Enumeration の使用方法であり、私たちはこの使用方法の防止に努めています。 逆に、すべての固定実装には Collection をとるコンストラクタを持たせることを (また、同じ要素で新しい Collection を作成することを) 強くお勧めします。
Iterator の規約が反復の順序に関して何の保証も行わないとすれば、意味があいまいになります。 ただし、ListIterator では反復の順序が保証されるので、追加オペレーションが利用できます。
List がリンクリストを示唆するかどうかの意見は二分しています。 実装命名規約を <Implementation><Interface> と仮定すると、コアインタフェースの名前は短いままにしたいという強い要望があります。 また、List を Sequence に変更すると、既存の名前のいくつか (AbstractSequentialList, LinkedList) は、不適切なものになります。 命名の矛盾は、次の「形式」で対処できます。
import java.util.*; import Java.awt.*; import java.util.List; // Dictates interpretation of "List"
この言語では、set/get 命名規約が正式であると決まっているので、それに従いました。
これは設計によるものです。 マッピングはコレクションではなく、コレクションはマッピングではないと考えています。 そのため、Map が Collection インタフェースを継承する (その逆も) ということにはほとんど意味がありません。
Map が Collection だとすれば、要素は何でしょうか。 唯一の妥当な答えは「キーと値のペア」ですが、これでは非常に限定された (しかも特に役に立つわけでもない) Map の抽象化しかできません。 与えられたキーがどんな値にマップするかを問い合わせることも、与えられたキーがどんな値にマップするかを知らずにそのキーのエントリを削除することもできません。
Collection が Map を継承するようにはできますが、「キーとは何か」という問題が発生します。 これには満足できる答えはなく、無理に答えを探しても、結果的には不自然なインタフェースになります。
Map は (キー、値、またはキーと値のペアの) Collection として「表示」できます。 このことは、Map の 3 つの「コレクション表示オペレーション」 (keySet、entrySet、values) に反映されています。 原理的には、List を 要素への Map マッピングインデックスとして表示することはできますが、List から要素を削除すると、削除された要素の前のすべての要素に関連する Key が変更されるという厄介な性質があります。 List にマップ表示オペレーションがないのは、この理由からです。
Enumeration のメソッド名は不適切なものだと考えています。 これらは非常に長く、頻繁に使用されます。 メソッドを追加して新しいフレームワーク全体を作成する立場から、名前を改善する機会は利用すべきであると考えました。 もちろん、Iterator で新しい名前と古い名前の両方をサポートすることもできますが、それは価値のあることとは思われませんでした。
そのメソッドは、java.io.PushbackInputStream と似た方法で、現在の Iterator の上に実装できます。 このメソッドを使用することは滅多にないので、誰もが実装する必要のあるインタフェース内に含める価値はないと考えます。
Collections Framework の設計目標 (「Collections Framework の概要」にある) を読むと、JGL とまったく同じ立場では作業していないことが理解できると思います。Java コレクション概要の「設計目標」から引用すると、「設計上の主要な目標は、実際のサイズにおいて、またより重要性の高い「概念の重さ」においても妥当な小ささの API を作成することでした」という記述があります。
JGL は約 130 のクラスとインタフェースで構成され、主な目標は C++ STL (Standard Template Library) との整合性でした。 これは、Collections Framework の目標ではありませんでした。 Java は従来、C++ の持つ複雑な特徴 (多重継承、演算子のオーバーロードなど) を避けてきました。すべてのインフラストラクチャを含め、Collections Framework には全体で約 25 のクラスとインタフェースがあります。
C++ のプログラマの中にはこのことを不快に感じる人がいるかもしれませんが、長期的には Java にとって良いことだと思います。 Java のライブラリが成熟するにつれて増大するのは避けられませんが、Java が容易に学習できて楽しく使える言語でありつづけられるよう、小ささと管理しやすさをできるだけ維持するように努力しています。
プログラマが自分の実装を「隠す」ことのできるコアコレクションインタフェースを提供すると、別名のコレクションが (それを JDK が提供するしないにかかわらず) 存在することになります。 JDK からのすべてのビューを廃止すると、たとえば配列から Collection を作成するような共通のオペレーションのコストが増大し、多くの有用な機能 (同期ラッパーなど) が廃止されるでしょう。 特に有用だと考えられているビューのひとつは、List.subList です。 このメソッドの存在は、入力に List をとるメソッドを書く場合に、(配列の場合のように) オフセットと長さをとる第 2 の形式を書く必要がないということを意味します。
主に、リソースの制約によります。 そのような API に取り組むのであれば、万人に役立つものでなければならず、また長期の使用に耐えるものでなければなりません。 このような機能も提供していく予定です。 それまでの間、public の API の上にそのような機能を実装するのは難しいことではありません。
Copyright © 1997-1999 Sun Microsystems, Inc., 901 San Antonia Ave., Palo Alto, CA 94303 USA.
All rights reserved.
コメントの送付先: collections-comments@java.sun.com |
Java ソフトウェア |