1. 概要

HashMap は、キーと値のマッピングを格納します。 このチュートリアルでは、さまざまなタイプの値をHashMapに格納する方法について説明します。

2. 問題の紹介

Java Generics の導入以来、通常は HashMapを一般的な方法で使用してきました。例:

Map<String, Integer> numberByName = new HashMap<>();

この場合、StringIntegerのデータのみをキーと値のペアとしてマップnumberByNameに入れることができます。 それは型の安全性を保証するので、それは良いことです。 たとえば、FloatオブジェクトをMapに配置しようとすると、「互換性のないタイプ」のコンパイルエラーが発生します。

ただし、場合によっては、さまざまなタイプのデータをMapに入れたいことがあります。 たとえば、numberByNameマップにFloatおよびBigDecimalオブジェクトも値として格納する必要があります。

それを実現する方法を説明する前に、デモンストレーションと説明を簡単にするために問題の例を作成しましょう。 異なるタイプの3つのオブジェクトがあるとしましょう。

Integer intValue = 777;
int[] intArray = new int[]{2, 3, 5, 7, 11, 13};
Instant instant = Instant.now();

ご覧のとおり、3つのタイプはまったく異なります。 したがって、最初に、これら3つのオブジェクトをHashMapに配置しようとします。 簡単にするために、String値をキーとして使用します。

もちろん、ある時点で、 Map からデータを読み取り、そのデータを使用する必要があります。 したがって、 HashMap のエントリをウォークスルーし、エントリごとに、説明付きの値を出力します。

では、それをどのように達成できるか見てみましょう。

3. 使用する地図

Javaでは、Objectはすべてのタイプのスーパータイプであることがわかっています。 したがって、 地図なので地図 、任意のタイプの値を受け入れる必要があります。

次に、このアプローチが要件を満たしているかどうかを確認しましょう。

3.1. マップにデータを入れる

先に述べたように、 地図それに任意のタイプの値を入れることができます:

Map<String, Object> rawMap = new HashMap<>();
rawMap.put("E1 (Integer)", intValue);
rawMap.put("E2 (IntArray)", intArray);
rawMap.put("E3 (Instant)", instant);

それはかなり簡単です。 次に、 Map のエントリにアクセスして、値と説明を出力してみましょう。

3.2. データの使用

に値を入れた後地図 、値の具体的なタイプを失いました。 したがって、 data を使用する前に、値を確認して適切なタイプにキャストする必要があります。 たとえば、 instanceof演算子を使用して、値のタイプを確認できます。

rawMap.forEach((k, v) -> {
    if (v instanceof Integer) {
        Integer theV = (Integer) v;
        System.out.println(k + " -> "
          + String.format("The value is a %s integer: %d", theV > 0 ? "positive" : "negative", theV));
    } else if (v instanceof int[]) {
        int[] theV = (int[]) v;
        System.out.println(k + " -> "
          + String.format("The value is an array of %d integers: %s", theV.length, Arrays.toString(theV)));
    } else if (v instanceof Instant) {
        Instant theV = (Instant) v;
        System.out.println(k + " -> "
          + String.format("The value is an instant: %s", FORMATTER.format(theV)));
    } else {
        throw new IllegalStateException("Unknown Type Found.");
    }
});

上記のコードを実行すると、次の出力が表示されます。

E1 (Integer) -> The value is a positive integer: 777
E2 (IntArray) -> The value is an array of 6 integers: [2, 3, 5, 7, 11, 13]
E3 (Instant) -> The value is an instant: 2021-11-23 21:48:02

このアプローチは、期待どおりに機能します。

ただし、いくつかの欠点があります。 次に、それらを詳しく見てみましょう。

3.3. 短所

まず、マップが比較的多くの異なるタイプをサポートするように計画している場合、複数のif-elseステートメントは大きなコードブロックになり、コードを読みにくくします

さらに、使用する型に継承関係が含まれている場合、instanceofチェックが失敗する可能性があります

たとえば、 java.lang.IntegerintValuejava.lang.NumbernumberValue をマップに配置した場合、instanceofを使用してそれらを区別することはできません。オペレーター。 これは、(intValue instanceof Integer)(intValue instanceof Number)の両方がtrueを返すためです。

したがって、値の具体的なタイプを判別するために、追加のチェックを追加する必要があります。 しかし、もちろん、これはコードを読みにくくします。

最後に、マップは任意のタイプの値を受け入れるため、タイプsafetyを失いました。 つまり、予期しないタイプが発生した場合の例外を処理する必要があります。

質問が出てくるかもしれません:異なるタイプのデータを受け入れ、タイプの安全性を維持する方法はありますか?

次に、問題を解決するための別のアプローチについて説明します。

4. 必要なすべてのタイプのスーパータイプを作成する

このセクションでは、型の安全性を維持するためのスーパータイプを紹介します。

4.1. データ・モデル

まず、インターフェースDynamicTypeValueを作成します。

public interface DynamicTypeValue {
    String valueDescription();
}

このインターフェースは、マップがサポートすると予想されるすべてのタイプのスーパータイプになります。 また、いくつかの一般的な操作を含めることもできます。 たとえば、メソッドvalueDescriptionを定義しました。

次に、具体的なタイプごとにクラスを作成して値をラップし、作成したインターフェイスを実装します。 たとえば、IntegerタイプのIntegerTypeValueクラスを作成できます。

public class IntegerTypeValue implements DynamicTypeValue {
    private Integer value;

    public IntegerTypeValue(Integer value) {
        this.value = value;
    }

    @Override
    public String valueDescription() {
        if(value == null){
            return "The value is null.";
        }
        return String.format("The value is a %s integer: %d", value > 0 ? "positive" : "negative", value);
    }
}

同様に、他の2つのタイプのクラスを作成しましょう。

public class IntArrayTypeValue implements DynamicTypeValue {
    private int[] value;

    public IntArrayTypeValue(int[] value) { ... }

    @Override
    public String valueDescription() {
        // null handling omitted
        return String.format("The value is an array of %d integers: %s", value.length, Arrays.toString(value));
    }
}
public class InstantTypeValue implements DynamicTypeValue {
    private static DateTimeFormatter FORMATTER = ...

    private Instant value;

    public InstantTypeValue(Instant value) { ... }

    @Override
    public String valueDescription() {
        // null handling omitted
        return String.format("The value is an instant: %s", FORMATTER.format(value));
    }
}

より多くのタイプをサポートする必要がある場合は、対応するクラスを追加するだけです。

次に、上記のデータモデルを使用して、さまざまなタイプの値をマップに保存して使用する方法を見てみましょう。

4.2. マップでのデータの配置と使用

まず、 Map を宣言し、それにさまざまなタイプのデータを入れる方法を見てみましょう。

Map<String, DynamicTypeValue> theMap = new HashMap<>();
theMap.put("E1 (Integer)", new IntegerTypeValue(intValue));
theMap.put("E2 (IntArray)", new IntArrayTypeValue(intArray));
theMap.put("E3 (Instant)", new InstantTypeValue(instant));

ご覧のとおり、マップを次のように宣言しました。 地図そのため型安全性が保証されています :データのみ DynamicTypeValue タイプはマップに入れることができます。

マップにデータを追加するときに、作成した対応するクラスをインスタンス化します

データを使用する場合、タイプのチェックとキャストは必要ありません

theMap.forEach((k, v) -> System.out.println(k + " -> " + v.valueDescription()));

コードを実行すると、次のように出力されます。

E1 (Integer) -> The value is a positive integer: 777
E2 (IntArray) -> The value is an array of 5 integers: [2, 3, 5, 7, 11]
E3 (Instant) -> The value is an instant: 2021-11-23 22:32:43

ご覧のとおり、このアプローチのコードはクリーンで、はるかに読みやすくなっています

さらに、サポートする必要のあるすべての型に対してラッパークラスを作成するため、継承関係を持つ型は問題になりません。

型安全性のおかげで、予期しない型のデータに直面するというエラーケースを処理する必要がありません。

5. 結論

この記事では、Java HashMapがさまざまなタイプの値データをサポートするようにする方法について説明しました。

また、例を通してそれを達成するための2つのアプローチに取り組みました。

いつものように、記事に付属するソースコードは、GitHubから入手できます。