Перейти к основному содержимому

Java HashMap с разными типами значений

· 6 мин. чтения

1. Обзор

HashMap хранит сопоставления ключ-значение . В этом руководстве мы обсудим, как хранить значения разных типов в HashMap .

2. Введение в проблему

С момента появления Java Generics мы обычно использовали HashMap в общем виде, например:

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

В этом случае мы можем поместить в карту numberByName только данные String и Integer в виде пар ключ-значение . Это хорошо, так как обеспечивает безопасность типов. Например, если мы попытаемся поместить объект Float в Map , мы получим ошибку компиляции «несовместимые типы». ``

Однако иногда мы хотели бы поместить данные разных типов в карту . Например, мы хотим, чтобы карта numberByName также хранила объекты Float и BigDecimal в качестве значений.

Прежде чем обсуждать, как этого добиться, давайте создадим пример задачи, чтобы упростить демонстрацию и объяснение. Допустим, у нас есть три объекта разных типов:

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

Как мы видим, эти три типа совершенно разные. Итак, сначала мы попробуем поместить эти три объекта в HashMap . Для простоты мы будем использовать строковые значения в качестве ключей.

Конечно, в какой-то момент нам нужно будет считать данные с карты и использовать их. Поэтому мы пройдемся по записям в HashMap и для каждой записи выведем значение с некоторым описанием.

Итак, давайте посмотрим, как мы можем этого добиться.

3. Использование Map<String, Object>

Мы знаем, что в Java Object является супертипом всех типов . Следовательно, если мы объявим Map как Map<String, Object> , она должна принимать значения любого типа.

Далее давайте посмотрим, соответствует ли этот подход нашим требованиям.

3.1. Размещение данных на карте

Как мы упоминали ранее, Map<String, Object> позволяет нам помещать в него значения любого типа:

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

Это довольно просто. Затем давайте посетим записи на карте и напечатаем значение и описание.

3.2. Использование данных

После того, как мы поместили значение в Map<String, Object> , мы потеряли конкретный тип значения. Поэтому нам нужно проверить и привести значение к правильному типу перед использованием данных . Например, мы можем использовать оператор 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.Integer intValue и java.lang.Number numberValue , мы не сможем различить их с помощью оператора instanceof . Это связано с тем, что и (intValue instanceof Integer), и (intValue instanceof Number) возвращают true .

Следовательно, мы должны добавить дополнительные проверки для определения конкретного типа значения. Но, конечно, это затруднит чтение кода.

Наконец, поскольку наша карта принимает значения любого типа, мы потеряли безопасность типов . То есть мы должны обрабатывать исключение, когда встречаются непредвиденные типы.

Может возникнуть вопрос: есть ли способ принимать данные разных типов и сохранять безопасность типов?

Итак, далее мы рассмотрим другой подход к решению проблемы.

4. Создание супертипа для всех необходимых типов

В этом разделе мы введем супертип для сохранения безопасности типов.

4.1. Модель данных

Во-первых, мы создаем интерфейс DynamicTypeValue :

public interface DynamicTypeValue {
String valueDescription();
}

Этот интерфейс будет супертипом всех типов, которые, как мы ожидаем, будет поддерживать карта . Он также может содержать некоторые общие операции. Например, мы определили метод valueDescription .

Затем мы создаем класс для каждого конкретного типа, чтобы обернуть значение и реализовать созданный нами интерфейс . Например, мы можем создать класс IntegerTypeValue для типа Integer :

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);
}
}

Аналогично создадим классы для двух других типов:

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<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));

Как мы видим, мы объявили карту как Map<String, DynamicTypeValue> , чтобы гарантировать безопасность типов : в карту можно помещать только данные с типом 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 поддерживать данные значений разных типов.

Кроме того, мы рассмотрели два подхода для достижения этого на примерах.

Как всегда, исходный код, прилагаемый к статье, доступен на GitHub .