Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

We often work with collections such as maps to store key-value pairs in Java.

In this quick tutorial, we’ll explore converting a Map<String, Object> to a Map<String, String>.

2. Introduction to the Problem

First, let’s create a 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");
}

So, if we convert it to a Map<String, String>, the result should look like this:

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

Using the HashMap(Map<? extends K, ? extends V> m) constructor might be the first idea to complete the conversion. So, let’s give it a try:

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

Unfortunately, the line above doesn’t compile:

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

In this tutorial, we’ll discuss different approaches to solving the problem.

Also, as we can see, although the type of MAP1 is Map<String, Object>, all entries’ values are strings. Since the value’s type parameter is Object, our input map may contain entries whose values aren’t strings:

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
}

We’ll cover this conversion scenario too.

For simplicity, let’s take MAP1 and MAP2 as inputs and use unit test assertions to verify whether each solution works as expected.

3. Casting to Map (Unsafe)

Generic types are a compile-time feature. At runtime, all type parameters are erased, and all maps have the same type Map<Object, Object>. Therefore, we can cast MAP1 to a raw Map and assign it to a Map<String, String> variable:

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

This trick works, although there is an “Unchecked Conversion” warning at compile-time.

However, it’s not safe. For example, if some entry’s value isn’t a string, this approach hides the problem:

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

As the test above shows, although result2 is in type Map<String, String>, one value in the map isn’t a string. This could lead to unexpected issues in further processes.

So next, let’s see how to achieve a safe conversion.

4. Creating the checkAndTransform() Method

We can build a checkAndTransform() method to perform a safe conversion:

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

As the method above shows, we go through the entries in the input map, cast each entry’s value to String, and put the key-value pair to a new Map<String, String>.

Let’s test it with our MAP1:

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

Moreover, once the input map holds some values that cannot be cast to String, the ClassCastException will be thrown. Let’s pass MAP2 to the method to test:

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

For simplicity, in the catch block, we throw the exception. Depending on the requirement, we can employ a proper error-handling process after we catch the ClassCastException.

5. Converting All Values to Strings

Sometimes, we don’t want to catch any exception even if the map holds values that cannot be cast to String. Instead, for those “non-string” values, we’d like to take the object’s String representation as the value. For example, we want to convert MAP2 to this map:

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
}

Let’s implement it using the Stream 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);

It’s worth mentioning that we used String.valueOf() instead of e.toString() to avoid the potential NullPointerException.

6. Conclusion

In this article, we learned different ways to convert a Map<String, Object> to Map<String, String>. 

Also, we discussed the scenario when the input map contains non-string values.

As usual, all code snippets presented here are available over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are closed on this article!