Effective Javaを読むチャレンジ-項目29その3-

型安全な異種コンテナーパターン

3回目にしてようやく辿りついた本項目の本筋、異種コンテナーパターン(typesafe heterogeneous container)の話です。

このパターンは、SetやMapのようなコンテナーごとに固定数の型パラメータに制限されるような単一要素コンテナーに対し、そういった型パラメータの固定数の制限がない異種のコンテナーを、型トークンを使って実現します。それは下記のようなAPIです。

一見Mapを改変しただけのようなAPIですが、Mapのキーに型トークン(引数のClass<T>)を使用しています。これが「値を挿入したり取り出したりするためにコンテナーに対してパラメータ化されたキーを渡す」という方法です。そしてそのキーは値の型がそのキーと一致することを保証するために使用されるとあります。

このFavoritesクラスのクライアントは「好み」の型についてのインスタンスを保持したり取り出したりできます。クライアントコードは次のとおり。

このコードを実行すると「Java cafebabe」という文字列が表示されます。このFavoritesインスタンスは型安全です。putFavoriteメソッドの第1引数がString.classなら、値もStringインスタンスでないとコンパイルされません。Objectインスタンスに文字列を入れて渡そうとしても、String.classとObject.classは別の型なのでやはりコンパイルエラーになります。

getFavoriteメソッドの引数がString.classなら、その戻り値の型もString.classです。他の型が戻り値になることはありません。また、Favoritesインスタンスは異なる型を格納できることから、異種でもあります。これが型安全な異種コンテナーパターンです。

型安全でなくてもいいなら、それはMapインタフェースで十分です。上記のコードをMapを使ったコードに置き換えても同じ結果になります。ですが、その場合は「map.put(String.class, 0xcafebabe);」のコンパイルが通るので型安全ではありません。

次に、Favoriteクラスの実装はこうなります。

この実装のポイントを箇条書きにします。

  • 内部で保持しているMapのキーがClass<?>と非境界ワイルドカード型を使用している。マップのキーではなく、キーの型が非境界ワイルドカード型なのでMapにキーを挿入することができている(キー自体が非境界ワイルドカード型だとnull以外のキーが挿入できなくなる)。また、そのワイルドカードが異種性を生み出している
  • 内部で保持しているMapの値がObjectクラスとなっていて、キーと値の間で型の関係性がなくなっている(putFavoriteのときにキーと値の型の関係、すなわち型関連が破棄される)。しかし、getFavoriteのときに型関連を復元することができる
  • getFavoriteは普通に内部で保持しているMapの値を取り出すとObjectを返してしまうので、型Tにキャストしなくてはいけない。その値の型はputFavoriteメソッドによりTだと分かっているので、引数のClassインスタンスに対してcastメソッドを呼び出してTに変換している

このFavoritesクラス内部のマップはキーがClass<T>なので、型に対してユニークな値(そして値の型はキーと同じ)を持つコンテナーということになります。String.classに対応する値は最大1つで、同じキーに対するputFavoriteメソッドの結果、値は上書きされます。本書において「好み」の、と何でもなさそうな言葉を強調しているのはそういうことだと思います。つまり「好みのインスタンスをどれか1つ」という意味になります。

この型安全な異種コンテナーパターンの使い道ってどんなものがあるでしょうか。型ごとにdistinctされてもいいインスタンスがまず浮かばないんですよね。データベースのテーブルの行へのアクセッサ・・・は本項目の内容そのままですね・・・。

このパターンの制限

次にこのパターンには制限事項が2つあるという話。その1つ目は、Classオブジェクトを原型で使用すると型安全性を容易に破壊するとあります。それはこういったコードです。

このコードを実行すると、getFavoriteメソッドを呼び出した箇所でClassCastExceptionをスローします。つまり、悪意のあるデータがコンテナーに格納されてしまいます。下記のようにputFavoriteメソッドを変更すると、この悪意のあるデータの混入を防ぐことができます。

上記の修正後、さっきのクライアントコードを実行するとputFavoriteメソッドを呼び出した箇所でClassCastExceptionをスローします。コンパイル時に型安全性を守ることはできませんが、より手前で危険なコード実行を防ぐことができます。

もう1つの制約は、具象化不可能型に対してこのパターンが使えないことです。具象化不可能型とは実行時に型情報が分からない型、例えばList<String>などです。Javaの文法上、List<String>.classといったクラスリテラルは書けません。

この問題の完全に満足のいく回避策はどうもないみたいです。スーパー型トークンと呼ばれる技法がありますが、この技法は例えばList<String>を異種コンテナーに格納して取り出すことができるとはいえ、同じコンテナーにList<String>とList<Integer>のような異なる型パラメータをもった同じClassのキーを同梱することができないという制約があります。結局のところ、スーパー型トークンパターンでもList<String>とList<Integer>が同じキー(同じ型)になってしまうことは避けられないということです。

境界型トークン

今までの例は使用する型に制限はなく、可能な限りどの型でもキーにできました。今度はそのClass<T>に制限するような型トークンの使用法を紹介しています。それは境界型トークンと呼ばれます。境界型トークンは、Class<T>の型パラメータが境界型パラメータ(T extends 型名)や境界型ワイルドカード(? extends 型名)を使用して、型トークンに使用する型を制限した形になります。

この境界型トークンの実例としてアノテーションAPIが挙げられています。例えばjava.lang.reflect.AnnotatedElementインタフェースのgetAnnotationメソッドの宣言はこういった感じです。

アノテーションを表すAnnotationのサブタイプだけを許容した型トークンですね。このインタフェースはClassクラスなどで実装されています。

これでようやくジェネリックスの章が読み終わりました・・・だいぶ時間が掛かりましたが。次の章が終わればようやく折り返し地点です。

広告
  • LINEで送る