1. 概述

在Java中,我们经常使用如map这样的集合来存储键值对。

在这篇简短教程中,我们将探讨如何将Map<String, Object>转换为Map<String, String>

2. 问题介绍

首先,我们创建一个Map<String, Object>

static final Map<String, Object> MAP1 = Maps.newHashMap();

static {
    MAP1.put("K01", "GNU Linux");
    MAP1.put("K02", "Mac OS");
    MAP1.put("K03", "MS Windows");
}

如果转换为Map<String, String>,结果应该如下所示:

static final Map<String, String> EXPECTED_MAP1 = Maps.newHashMap();

static {
    EXPECTED_MAP1.put("K01", "GNU Linux");
    EXPECTED_MAP1.put("K02", "Mac OS");
    EXPECTED_MAP1.put("K03", "MS Windows");
}

我们可能会首先尝试使用HashMap(Map<? extends K, ? extends V> m)构造函数完成转换。让我们试一试:

Map<String,String> result = new HashMap<String,String>(MAP1);

不幸的是,上面的代码无法编译

no suitable constructor found for HashMap(java.util.Map<java.lang.String,java.lang.Object>)

在这篇教程中,我们将讨论解决这个问题的不同方法。

另外,虽然MAP1的类型是Map<String, Object>,但所有条目的值都是字符串。由于值的类型参数是Object输入映射可能包含值非字符串的条目

static final Map<String, Object> MAP2 = Maps.newHashMap();

static {
    MAP2.put("K01", "GNU Linux");
    MAP2.put("K02", "Mac OS");
    MAP2.put("K03", BigDecimal.ONE); // value is not a string
}

我们也会涵盖这种转换场景。

为了简化,我们将MAP1MAP2作为输入,并使用单元测试断言来验证每个解决方案是否按预期工作。

3. 转换为Map(不安全)

泛型类型是编译时特性。**在运行时,所有类型参数都会被擦除,所有的映射都具有相同的类型Map<Object, Object>**。因此,我们可以将MAP1转换为原始的Map并将其赋值给Map<String, String>变量:

Map<String, String> result = (Map) MAP1;
assertEquals(EXPECTED_MAP1, result);

这个技巧可行,尽管编译时会有一个“不安全转换”警告

然而,这并不安全。例如,如果某些条目的值不是字符串,这种方法会隐藏问题

Map<String, String> result2 = (Map) MAP2;
assertFalse(result2.get("K03") instanceof String);

如上测试所示,尽管result2Map<String, String>类型,但映射中的一个值不是字符串。这可能导致进一步处理中的意外问题。

接下来,我们将看到如何实现安全转换。

4. 创建checkAndTransform()方法

我们可以构建一个checkAndTransform()方法来进行安全转换:

Map<String, String> checkAndTransform(Map<String, Object> inputMap) {
    Map<String, String> result = new HashMap<>();
    for (Map.Entry<String, Object> entry : inputMap.entrySet()) {
        try {
            result.put(entry.getKey(), (String) entry.getValue());
        } catch (ClassCastException e) {
            throw e; // or a required error handling
        }
    }
    return result;
}

如上述方法所示,我们遍历输入映射中的条目,将每个条目的值转换为String,并将键值对放入新的Map<String, String>中。

让我们用MAP1进行测试:

Map<String, String> result = checkAndTransform(MAP1);
assertEquals(EXPECTED_MAP1, result);

此外,一旦输入映射中包含无法转换为String的值,ClassCastException将被抛出。让我们将MAP2传递给方法进行测试:

assertThrows(ClassCastException.class, () -> checkAndTransform(MAP2));

为了简洁,我们在catch块中重新抛出异常。根据需求,我们可以在捕获ClassCastException后执行适当的错误处理过程。

5. 将所有值转换为字符串

有时,即使映射中包含无法转换为String的值,我们也不希望捕获任何异常。相反,对于这些“非字符串”值,我们希望取对象的String表示形式作为值。例如,我们想将MAP2转换为此映射:

static final Map<String, String> EXPECTED_MAP2_STRING_VALUES = Maps.newHashMap();

static {
    EXPECTED_MAP2_STRING_VALUES.put("K01", "GNU Linux");
    EXPECTED_MAP2_STRING_VALUES.put("K02", "Mac OS");
    EXPECTED_MAP2_STRING_VALUES.put("K03", "1"); // string representation of BigDecimal.ONE
}

让我们使用流API来实现它:

Map<String, String> result = MAP1.entrySet()
  .stream()
  .collect(toMap(Map.Entry::getKey, e -> String.valueOf(e.getValue())));

assertEquals(EXPECTED_MAP1, result);

Map<String, String> result2 = MAP2.entrySet()
  .stream()
  .collect(toMap(Map.Entry::getKey, e -> String.valueOf(e.getValue())));

assertEquals(EXPECTED_MAP2_STRING_VALUES, result2);

值得一提的是,**我们使用String.valueOf()而不是e.toString()来避免潜在的NullPointerException**。

6. 总结

在这篇文章中,我们学习了将Map<String, Object>转换为Map<String, String>的不同方式。

我们也讨论了当输入映射包含非字符串值的情况。

如往常一样,这里展示的所有代码片段都可以在GitHub上找到。