プロジェクト

全般

プロフィール

Javaプログラミング 異なる型の値をキーで格納

はじめに

Javaには、キーと値の組でデータを格納する java.util.Map が標準APIで提供されています。
ただし、値の型は基本同じものを格納します。Map<String, Person>と宣言したMapは、キーにString型、値にPerson型を格納します。

Person adam = ...
Person bill = ...

Map<String, Person> roster = new HashMap<>();
roster.add(101, adam);
roster.add(102, bill);

Person x = roster.get(101);

ここで、MapにはPerson型のインスタンスあるいはPerson型のサブタイプのインスタンスが格納可能です。

何でも格納できるMap<String, Object>を宣言することは可能ですが、この宣言で作ったMapインスタンスから値を取得するときはObject型で返されるので、値の本来の型を使用するにはダウンキャストが必要となり、ジェネリックスによる型安全が導入される前のMapに戻ってしまいます。

Person adam = ...
Cat coco = ...
Map<String, Object> family = new HashMap<>();
faimly.add(1, adam);
family.add(2, coco);
Person x = (Person) family.get(1);  // <-- 運よく⁉ 実行OK
Person y = (Person) family.get(2);  // <-- 実行時例外発生

ダウンキャストを安全に実施するには余分にコードを書く必要があります。

Object o = family.get(1);
Person x = null;
if (o instanceof Person) {
    x = (Person) o;
}

このコードでは、キー1にPerson型のインスタンスが格納されていることがプログラミング上明らかであることが前提です。
もし、文脈によって型が変わるような場合、相当に難儀なことになります。

そこで、キーと値の組でデータを格納し、異なる型の値を任意に使用でき、値の取得にはダウンキャストが不要となるようなプログラミングが実現できるかどうかを探ってみます。

検討

方針を探る

最初の方針

参考文献を眺めていると、キーに型情報を持たせ、値はその型に限定させる実装が多いようです。

最初に参考にしたEffective Java第3版 項目33は、キーをClass型としています。そのため、Mapに格納するキーがすべて異なる型のときは使えますが、同じ型が2つ以上ある場合は使えません。以下がEffective Javaの実装です。

public class Favorites {  // 型安全異種コンテナ(typesafe heterogeneous container)
    private Map<Class<?>, Object> favorites = new HashMap<>();  // キーにクラス型、値にObject型を取るMapを内部に保持

    public <T> void putFavorite(Class<T> type, T instance) {
        favorites.put(Objects.requireNonNull(type), type.cast(instance));  // キーがnullのときのエラーチェックと
    }                                                                      // 値がキーの型であることを検査

    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));  // Class#castを使ってオブジェクト参照を動的にキャスト
    }
}

そこで、キーを表現するクラスを新たに定義します。

キーのクラス

キーを表現するクラスは、次の様に記述できます。

public class Key<T> {
    final String identifier;  // キーの文字列表現
    final Class<T> type;      // 値の型

    public Key(String identifier, Class<T> type) {
        this.identifier = identifier;
        this.type = type;
    }
}

型を持つキーを使うMapの骨格を定義します。

public class HeterogeneousMap {
    final Map<Key<?>, Object> internal = new HashMap<>();

    public <T> void put(Key<T> key, T value) {
        internal.put(Objects.requireNonNull(key), key.type.cast(value));
    }

    public <T> T get(Key<T> key) {
        return key.type.cast(internal.get(key));
    }
}

参考

Stephen Wong. Mixed Data Dictionaries and Inter-Module Communications. Rice University. https://www.clear.rice.edu/comp310/JavaResources/mixeddata/ , (参照 2020-05-22)

Map with different types for values ~stackexchange より(Nicolai氏の回答)

Frank Appel. How to Map Distinct Value Types Using Java Generics. Code Affine. https://www.codeaffine.com/2015/03/04/map-distinct-value-types-using-java-generics/ , (参照 2020-05-22)

ジョシュア・ブロック著, 柴田芳樹訳.『Effective Java』. 第3版. 丸善出版, 2018年, pp.153-157(項目33 型安全な異種コンテナを検討する)
Item 33: Consider typesafe heterogeneous containers (原著の該当項公開)