具有不同值类型的Java HashMap

2023/06/07

1. 概述

HashMap存储键值映射。在本教程中,我们将讨论如何在HashMap中存储不同类型的值。

2. 问题简介

自从引入Java泛型以来,我们通常以泛型方式使用HashMap-例如:

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

在这种情况下,我们只能将String和Integer数据作为键值对放入Map numberByName中。这很好,因为它确保了类型安全。例如,如果我们试图将一个Float对象放入Map中,我们将得到“不兼容类型”的编译错误。

然而,有时,我们希望将不同类型的数据放入一个Map中。例如,我们希望numberByNameMap也能将Float和BigDecimal对象存储为值。

在讨论如何实现之前,让我们创建一个示例问题来简化演示和解释。假设我们有三个不同类型的对象:

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

正如我们所看到的,这三种类型是完全不同的。因此首先,我们将尝试将这三个对象放入HashMap中。为简单起见,我们将使用String值作为键。

当然,在某些时候,我们需要从Map中读取数据并使用数据。因此,我们将遍历HashMap中的条目,并为每个条目打印带有一些描述的值。

那么,让我们看看如何实现这一目标。

3. 使用Map<String, Object>

我们知道,在Java中,Object是所有类型的超类型。因此,如果我们将Map声明为Map<String, Object>,它应该接受任何类型的值。

接下来让我们看看这种方法是否满足我们的要求。

3.1 将数据放入Map

正如我们之前提到的,Map<String, Object>允许我们将任何类型的值放入其中:

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

这很简单。接下来,让我们访问Map中的条目并打印值和描述。

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: 2022-11-23 21:48:02

这种方法按我们预期的那样工作。

但是,它有一些缺点。接下来,让我们仔细看看它们。

3.3 缺点

首先,如果我们打算让Map支持相对更多的不同类型,那么多个if-else语句将成为一个大代码块,导致代码难以阅读。

而且,如果我们要使用的类型包含继承关系,instanceof检查可能会失败

例如,如果我们在Map中放置一个java.lang.Integer intValue和一个java.lang.Number numberValue,我们无法使用instanceof运算符区分它们。这是因为(int Value instanceof Integer)和(int Value instanceof Number)都返回true。

因此,我们必须添加额外的检查来确定值的具体类型。当然,这会使代码难以阅读。

最后,由于我们的Map接受任何类型的值,因此我们失去了类型安全。也就是说,当遇到非预期的类型时,我们要处理异常。

可能会出现一个问题:有没有办法接收不同类型的数据并保持类型安全?

因此接下来,我们将讨论另一种解决问题的方法。

4. 为所有需要的类型创建超类

在本节中,我们将引出一个超类型来保持类型安全。

4.1 数据模型

首先,我们创建一个接口DynamicTypeValue:

public interface DynamicTypeValue {
    String valueDescription();
}

该接口将是我们希望Map支持的所有类型的超类型。它还可以包含一些常用操作。例如,我们定义了一个方法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);
    }
}

同样,让我们为其他两种类型创建类:

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

如果我们需要支持更多的类型,我们只需要添加相应的类。

接下来,让我们看看如何使用上面的数据模型来存储和使用Map中不同类型的值。

4.2 在Map中添加和使用数据

首先,让我们看看如何声明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));

如我们所见,我们已将Map声明为Map<String, DynamicTypeValue>以保证类型安全:只允许将DynamicTypeValue类型的数据放入Map中。

当我们向Map添加数据时,我们会实例化创建的相应类

当我们使用数据时,不需要类型检查和强制转换

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: 2022-11-23 22:32:43

正如我们所看到的,这种方法的代码很干净并且更容易阅读

此外,由于我们为每个需要支持的类型都创建了一个包装类,因此具有继承关系的类型不会导致任何问题。

多亏了类型安全,我们不需要去处理面对非预期类型数据的错误情况。

5. 总结

在本文中,我们讨论了如何使Java HashMap支持不同类型的值数据。

此外,我们还通过示例介绍了两种实现它的方法。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章