概要
複数の型を入れることのできる、型安全なMap風コンテナ実装のアイデアを整理する。
目的とすること
- 言語は Java で実装
- できるだけコーディング時に誤りを検出したい
- (Qiitaのポストの練習をしたい。内容は何でも良かったので、どうでもよいネタで…)
目的としないこと
- 悪意あるコーディングからの防御
- フールプルーフ(アホなコーディングからの防御)
- 実行時の型チェック
現状
機能間のデータのやり取りを Map<String,Object> のようなコンテナを用いて実現したり、あるいは HttpSession などのように実質的に Map<String,Object> 同様のコンテナは広く使われていると思われる。
しかしいくつかのプロジェクトではこのようなコンテナの使い方に問題があり、結合テスト以降にバグが多発するところを見たことがある。
問題点
主に、以下の2つの問題があると思われる。
- ほとんどの場合、key としてString型が使われる。 そのため、使用する箇所でそれぞれに文字列リテラルを指定することが可能になっている。 key に使用する文字列リテラルに誤字があった場合、実行時に NullPointerException が発生することになる。
- value が Object 型で定義されているため、設定時にどんな型のデータでも入れることができ、また取得時にキャストが必要となる。 設定するデータ、あるいはキャストする型を間違えた場合には ClassCastException が発生することになる。
対策案
そこで、以下のような実装を考えてみた。
TypesafeMap.java
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* 複数の型を入れることのできる、型安全なMap風コンテナ.<br>
*
* ほぼ java.util.Map インタフェースと同等のメソッドを持っていますが
* get(), put(), remove() が型安全になっているため、
* Mapインタフェースの実装にはなっていません.<br>
*
* この実装は同期しません.<br>
*/
public class TypesafeMap implements Serializable {
/** シリアライズバージョン */
private static final long serialVersionUID = 585258775919601476L;
/** 情報を保持するMap */
private final Map<Key<?>, Object> map;
/** 変更不能Map */
private final Map<Key<?>, Object> unmodifiableMap;
/**
* TypesafeMapのキーのためのインタフェース.<br>
* このインタフェースを実装したenumクラスをTypesafeMapのキーとする.<br>
* @param <T> 値の型
*/
public interface Key<T> {}
/** コンストラクタ */
public TypesafeMap() {
// キーは enum 型だが、複数のenum型を格納するために
// EnumSet は使用できない
map = new LinkedHashMap<>();
unmodifiableMap = Collections.unmodifiableMap(map);
}
/**
* 指定された値と指定されたキーをこのマップに関連付ける.<br>
* マップに既にこのキーに対応する値がある場合、古い値は置き換えられます.<br>
* @param <E> 値のクラス
* @param <K> キーのクラス
* @param key キー
* @param value 値
* @return 古い値
*/
@SuppressWarnings("unchecked")
public <E, K extends Enum<?> & Key<E>> E put(K key, E value) {
// 以前の put() で型安全にしているので、無検査キャストしてもOK
return (E)map.put(key, value);
}
/**
* 指定されたキーに対応された値を返す.<br>
* 対応する値がない場合にはnullを返します.<br>
* @param <E> 値のクラス
* @param <K> キーのクラス
* @param key キー
* @return 値
*/
@SuppressWarnings("unchecked")
public <E, K extends Enum<?> & Key<E>> E get(K key) {
// 以前の put() で型安全にしているので、無検査キャストしてもOK
return (E)map.get(key);
}
/**
* 指定されたキーに対応された値の削除.<br>
* @param <E> 値のクラス
* @param <K> キーのクラス
* @param key キー
* @return 削除された値
*/
@SuppressWarnings("unchecked")
public <E, K extends Enum<?> & Key<E>> E remove(K key) {
// 以前の put() で型安全にしているので、無検査キャストしてもOK
return (E)map.remove(key);
}
/**
* 指定されたマップのすべてをこのマップにコピー.<br>
* @param other コピー元のマップ
*/
public void putAll(TypesafeMap other) {
map.putAll(other.map);
}
/** マップをクリア */
public void clear() {
map.clear();
}
/**
* 指定されたキーのマッピングが含まれている場合に真を返す.<br>
* @param key キー
* @return 含まれている場合に真
*/
public boolean containsKey(Object key) {
return map.containsKey(key);
}
/**
* 指定された値のマッピングが含まれている場合に真を返す.<br>
* @param value 値
* @return 含まれている場合に真
*/
public boolean containsValue(Object value) {
return map.containsValue(value);
}
/**
* マップが空の場合に真を返す.<br>
* @return マップが空の場合に真
*/
public boolean isEmpty() {
return map.isEmpty();
}
/**
* マップ内のキーと値のマッピングの数を返す.<br>
* @return マッピングの数
*/
public int size() {
return map.size();
}
/**
* キーのSetを返す.<br>
* このSetは変更不可です.<br>
* @return キーのSet
*/
public Set<Key<?>> keySet() {
return unmodifiableMap.keySet();
}
/**
* キーと値のSetを返す.<br>
* このSetは変更不可です.<br>
* @return キーと値のSet
*/
public Set<Map.Entry<Key<?>, Object>> entrySet() {
return unmodifiableMap.entrySet();
}
/**
* マップに含まれている値のコレクションビューを返す.<br>
* このコレクションは変更不可です.<br>
* @return 値のコレクションビュー
*/
public Collection<?> values() {
return unmodifiableMap.values();
}
}
gist: TypesafeMap.java
具体的な使い方は以下のような感じになります。
// TypesafeMapのためのキーを作成する
// キーはenumであり、TypesafeMap.Key<T>を実装する必要がある
public enum StringKey implements TypesafeMap.Key<String> {
KEY_FOO, KEY_BAR, KEY_BAZ
}
public enum ListKey implements TypesafeMap.Key<List<String>> {
KEY_STRINGS
}
// ...
TypesafeMap map = new TypesafeMap();
map.put(StringKey.KEY_FOO, "value");
map.put(ListKey.KEY_STRINGS, list1);
String name = map.get(StringKey.KEY_BAZ);
List<String> list = map.get(ListKey.KEY_STRINGS);
メリット
- key はenum型のため、typo があればコーディング時にエラーとなる。
- key となる enum 型は任意の場所で好きに登録できる(集中管理は不要)
- value は Generics 対応のため、間違えたデータ型を入れることはできない。 またキャストも不要。
デメリット
- Key ごとに enum 型を作らないといけない。面倒?
- Key と文字列とをマッピングさせる必要がある場合に面倒(プロパティや環境変数などからKeyを組み立てる場合など)
- Java8から追加されているような、Map内のすべてのEntryに対して処理を行うような場合には型安全にならない(と思われる)。
- キーを範囲指定して削除などが難しい(キーが文字列であれば、キーの先頭などでグルーピング可能)
- (このクラスを実際に使ったことはないので、いろいろありそう…)
まとめ(ていない)
似たことはどこかに書いてありそうな気がするね。