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 .