2つのキーの使用
多くの場合、ある型と別の型との値マッピングは、ドメインの一般的なナレッジに一致しません。例えば、時々獲物を捕ってくるハンターがいて、その「ハンターの名前」、狩猟が行われた「日付」、獲物の「大きさ」に関するデータを記録するものとします。SQLを使えばソリューションは明らかです。次のような定義を使用することになります。
CREATE TABLE Log (Hunter String, Time Timestamp, Weight Number);
しかし、Javaでこれを表現するのは困難です。Hunterというクラスがあったとしても、この3つのデータ項目をどうすればリンクできるでしょうか。1つのソリューションは、特殊なクラスを作成することです。
class Event { Hunter hunter; Timestamp time; double mammothWeight; // won't fit in float }
この場合の欠点は、特定のハンターや特定の日付に関するファクトを見つける方法がないことです。Collection<Event>を用意するとしても、すべてをスキャンする必要があります。
自然な、データベース的なソリューションでは、このようなイベントにインデックスを付けます。しかし、何がキーになるのでしょうか。J2EEでは、次のようなキーの特別クラスが考えられます。
class EventKey {
Hunter hunter;
Timestamp time;
}
このようなキーは、Entity Beanを取得するには便利ですが、現実的な意味合いを持ちません。ハンターと日付は一体ではなく、ハンターはハンターであり、日付は日付です。さらに、このような「キー」を使用しても、指定のハンターの成功(または失敗)の履歴や、一族のお祝いや不幸の歴史を追跡することはできません。つまり、ハンターと日付でそれぞれ別のインデックスを使用する必要があります。解決しようとしている問題に応じて、1つまたは2つのインデックスを用意することになります。
Map<Hunter, Map<Timestamp, Event>> hunterIndex; Map<Timestamp, Map<Hunter, Event>> timeIndex;
ここで、Collection<Event>に加えてこの2つのマップを保持する場合に、どのような処理が必要になるかを考えてみましょう。Collection<Event>にイベントを追加するたびに、hunterIndexを参照し、エントリの有無を確認することが必要です。該当するエントリがない場合は、適切なエントリを空のマップで作成し、そのマップに新しいファクトを挿入します。削除についても同様です。この場合は、空のセカンダリマップを削除した方がよいか、またはそのままにしておいてもよいかを、同僚にたずねる必要もあります。自分ではその答えがわかっているつもりでも、あなたのコードをレビューする同僚は異なる答えを出すかもしれません。このような事態がいくつも出てきます。私は1年に何度もこのようなカスケードマップを作成しているので、こうした状況については嫌というほど知っています。
実際、このようなカスケードマップを使用する人は、Collection<Event>を別に作成するという方法をめったに採用しません。なぜなら、コレクションを上から下に渡す場合や、コレクションのサイズをカウントし直す必要がある場合を除いて、時間と場所の無駄に思われるからです。このようなケースでは、現実的なプログラマは、2つの大きく異なる方法のいずれかを使用します。
- 要求が発生したら
hunterIndexをスキャンし、セカンダリマップのサイズを調整する。 - 独立したカウンタを用意し、追加または削除のたびにカウンタを更新する。この場合、プログラマはスレッドと例外を考慮する必要があり、何ヶ月にもわたって実行し続けるアプリケーションが前提となる(さらに、再カウントの必要性はないものとする)。
私の考えでは、このような事態になるのは、Javaプログラマが既存のクラスの観点で物事を考える傾向があり、java.utilやjava.langにあるものをそのまま使おうとするためです。Pythonプログラマの場合はこのような問題に出会うことはなく、JavaScriptプログラマには選択の余地がありません。JavaScriptプログラマにできることは、配列にキーとして文字列を関連付けることだけです。
SQLプログラマの場合
SQLプログラマの場合は、Collection<Event>に相当するものを使用でき、さらにあらかじめ指定しておけば、変更時に自動的に更新されるインデックスも使用できます。おそらく、この動作は真似することができるでしょう。先ほどの例で示したインターフェイスMap<X,Y>に加えて、もう1つのキー(あくまでも1つだけ)を使用する新しいインターフェイスMap<X,Y,V>を使用したらどうなるでしょうか。Map<X,Y,V>のうち、XとYはキー型、Vは値型とします。以下のコードはMap<X,Y>とほとんど同じですが、エントリには2つのキーがあります。つまり、2つのキーセットがあります。
public interface Map2<X,Y,V> { int size(); boolean containsKeyPair(Object key1, Object key2); V get(X key1, Y key2); V put(X key1, Y key2, V value); V remove(X key1, Y key2); Set<X> keySet1(); Set<Y> keySet2(); Collection<V> values(); Map<Y,V> curry1(X key1); Map<X,V> curry2(Y key2); interface Entry<X,Y,V> { public X getKey1(); public Y getKey2(); public V getValue(); V setValue(V value); } Set<Map2.Entry<X,Y,V>> entrySet(); }
curry1とcurry2という2つのメソッドに注目してください。これらは1つのキーを受け取り、もう1つのキーと値とのマッピングを返します。curry1とcurry2という名前は、この処理を表す関数型プログラミング用語のカリー化(Currying)に由来します。
デフォルトの実装のAbstractMap2<X,Y,V>は、エントリをセットに格納します。1組のキーに対応する値を検索するには、エントリセット全体をスキャンすることになります。エントリセットが小さければ、それほど問題ではないでしょう。ここでの唯一の抽象メソッドはSet<Map2.Entry<X,Y,V>> entrySet()であり、データを任意の方法で格納できます。
デフォルトの実装はあまり効率的ではないので、インデックス付きのマップIndexedMap2<X,Y,V>を使用することにしましょう。このマップはXとYの2つのインデックスを保持し、Set<Map2.Entry<X,Y,V>> entrySet()は抽象メソッドのままです。
2つのパラメータから成るマップ
カスケードマップを使用する場合にどのインデックスを最初にするか、またコレクション全体をスキャンするにはどうすればよいかという質問はよくありますが、これまで説明してきたように、これらの問題はいくつかのクラスを使って解決することができます。おなじみのMapおよびSetというクラスを適用するには、hunterを第一にしてtimeインデックス付きのイベントのコレクションを用意するか、timeを第一にしてhunterインデックス付きのイベントのコレクションを用意するかを指定します。しかし、2つのキーセットを持つマップであるMap2を適用する方が適切です。
