1. Introduction

Many developers decide to store application parameters outside the source code. One of the ways to do so in Java is to use an external configuration file and read them via the java.util.Properties class.

In this tutorial, we'll focus on various approaches to convert java.util.Properties into a HashMap<String, String>. We'll implement different methods to achieve our goal, using plain Java, lambdas, or external libraries. Through examples, we'll discuss the pros and cons of each solution.

2. HashMap Constructor

Before we implement our first code, let's check the Javadoc for java.util.Properties. As we see, this utility class inherits from Hashtable<Object, Object>, which also implements the Map interface. Moreover, Java wraps its Reader and Writer classes to work directly on String values.

According to that information, we can convert Properties into HashMap<String, String> using typecasting and constructor calls.

Assuming that we've loaded our Properties correctly, we can implement:

public static HashMap<String, String> typeCastConvert(Properties prop) {
    Map step1 = prop;
    Map<String, String> step2 = (Map<String, String>) step1;
    return new HashMap<>(step2);
}

Here, we implement our conversion in three simple steps.

First, according to the inheritance graph, we need to cast our Properties into a raw Map. This action will force the first compiler warning, which can be disabled by using the @SuppressWarnings(“rawtypes”) annotation.

After that, we cast our raw Map into Map<String, String>, causing another compiler warning, which can be omitted by using @SupressWarnings(“unchecked”).

Finally, we build our HashMap using the copy constructor. This is the fastest way to convert our Properties, but this solution also has a big disadvantage related to type safety: Our Properties might be compromised and modified before the conversion.

According to the documentation, the Properties class has the setProperty() and getProperty() methods that force the use of String values. But also there are put() and putAll() methods inherited from Hashtable that allow using any type as keys or values in our Properties:

properties.put("property4", 456);
properties.put(5, 10.11);

HashMap<String, String> hMap = typeCastConvert(properties);
assertThrows(ClassCastException.class, () -> {
    String s = hMap.get("property4");
});
assertEquals(Integer.class, ((Object) hMap.get("property4")).getClass());

assertThrows(ClassCastException.class, () -> {
    String s = hMap.get(5);
});
assertEquals(Double.class, ((Object) hMap.get(5)).getClass());

As we can see, our conversion executes without any error, but not all elements in the HashMap are strings. So, even if this method looks the easiest, we must keep in mind some safety-related checks in the future.

3. The Guava API

If we can use third-party libraries, the Google Guava API comes in handy. This library delivers a static Maps.fromProperties() method, which does almost everything for us. According to the documentation, this call returns an ImmutableMap, so if we want to have the HashMap, we can use:

public HashMap<String, String> guavaConvert(Properties prop) {
    return Maps.newHashMap(Maps.fromProperties(prop));
}

As previously, this method works fine when we're completely sure that the Properties contain only String values. Having some non-conforming values will lead to unexpected behavior:

properties.put("property4", 456);
assertThrows(NullPointerException.class, 
    () -> PropertiesToHashMapConverter.guavaConvert(properties));

properties.put(5, 10.11);
assertThrows(ClassCastException.class, 
    () -> PropertiesToHashMapConverter.guavaConvert(properties));

The Guava API doesn't perform any additional mappings. As a result, it doesn't allow us to convert those Properties, throwing exceptions.

In the first case, the NullPointerException is thrown due to the Integer value, which cannot be retrieved by the Properties.getProperty() method and, as a result, is interpreted as null. The second example throws the ClassCastException related to the non-string key occurring on the input property map.

This solution gives us better type control and also informs us of violations that occur during the conversion process.

4. Custom Type Safety Implementation

It's now time to resolve our type of safety problems from the previous examples. As we know, the Properties class implements methods inherited from the Map interface. We'll use one of the possible ways of iterating over a Map to implement a proper solution and enrich it with type checks.

4.1. for-Loop Iteration

Let's implement a simple for-loop:

public HashMap<String, String> loopConvert(Properties prop) {
    HashMap<String, String> retMap = new HashMap<>();
    for (Map.Entry<Object, Object> entry : prop.entrySet()) {
        retMap.put(String.valueOf(entry.getKey()), String.valueOf(entry.getValue()));
    }
    return retMap;
}

In this method, we iterate over the Properties in the same way as we do for a typical Map. As a result, we have one-by-one access to every single key-pair value represented by the Map.Entry class.

Before putting values in a returned HashMap, we can perform additional checks, so we decide to use the String.valueOf() method.

4.2. Stream and Collectors API

We can even refactor our method using the modern Java 8 way:

public HashMap<String, String> streamConvert(Properties prop) {
    return prop.entrySet().stream().collect(
      Collectors.toMap(
        e -> String.valueOf(e.getKey()),
        e -> String.valueOf(e.getValue()),
        prev, next) -> next, HashMap::new
    ));
}

In this case, we're using Java 8 Stream Collectors without explicit HashMap construction. This method implements exactly the same logic introduced in the previous example.

Both solutions are slightly more complex because they require some custom implementation that the typecasting and Guava examples don't.

However, we have access to the values before putting them on the resulting HashMap, so we can implement additional checks or mappings:

properties.put("property4", 456);
properties.put(5, 10.11);

HashMap<String, String> hMap1 = loopConvert(properties);
HashMap<String, String> hMap2 = streamConvert(properties);

assertDoesNotThrow(() -> {
    String s1 = hMap1.get("property4");
    String s2 = hMap2.get("property4");
});
assertEquals("456", hMap1.get("property4"));
assertEquals("456", hMap2.get("property4"));

assertDoesNotThrow(() -> {
    String s1 = hMap1.get("property4");
    String s2 = hMap2.get("property4");
});
assertEquals("10.11", hMap1.get("5"));
assertEquals("10.11", hMap2.get("5"));

assertEquals(hMap2, hMap1);

As we can see, we solved our problems related to non-string values. Using this approach, we can manually adjust the mapping logic to achieve proper implementation.

5. Conclusion

In this tutorial, we checked different approaches to convert java.util.Properties into a HashMap<String, String>.

We started with a typecasting solution that is perhaps the fastest conversion but also brings compiler warnings and potential type safety errors.

Then we took a look at a solution using Guava API, which resolves compiler warnings and brings some improvements for handling errors.

Finally, we implemented our custom methods, which deal with type safety errors and give us the most control.

All code snippets from this tutorial are available over on GitHub.

Generic bottom

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

>> CHECK OUT THE COURSE
Generic footer banner
Comments are closed on this article!