Search code examples
javajava-8java-streamcollectnested-map

Java8 Streams - How to modify value of keys of inner map to null object from "null" string


I have a utility method defined as below.

public static Map<String, Map<String, String>> convertRawMapToStringValues(Map<String, Map<String, Object>> cassandraRowsRawMap) {

    Map<String, Map<String, String>> cassandraStrValuesMap = cassandraRowsRawMap.entrySet()
       .stream()
       .collect(Collectors.toMap(s -> s.getKey(),
          s -> s.getValue().entrySet().stream()
             .collect(Collectors.toMap(e -> e.getKey(), 
                                            e -> String.valueOf(e.getValue())))));
    return cassandraStrValuesMap;
}

The String.valueOf(e.getValue()) returns a "null" value from the call. I would like to get the null value for the string.

When I tried the below code, I get an NPE on first .collect call.

Map<String, Map<String, String>> cassandraStrValuesMap = cassandraRowsRawMap.entrySet()
   .stream()
   .collect(Collectors.toMap(s -> s.getKey(),
                             s -> s.getValue().entrySet().stream()
                      .collect(Collectors.toMap(e -> e.getKey(), 
                                                e -> e.getValue() == null ? null : String.valueOf(e.getValue())))));
    return cassandraStrValuesMap;
}

Solution

  • The toMap collector doesn’t support null values. But it doesn’t always have to be the Stream API:

    public static <K,T,R> Map<K,R> changeValues(
           Map<? extends K, T> in, Function<? super T, ? extends R> f) {
    
        Map<K,R> result = new HashMap<>(in.size());
        in.forEach((k,t) -> result.put(k, f.apply(t)));
        return result;
    }
    
    public static Map<String, Map<String, String>> convertRawMapToStringValues(
           Map<String, Map<String, Object>> in) {
    
        return changeValues(in, inner -> changeValues(inner, v -> v==null? null: v.toString()));
    }
    

    The utility method returns a map with the same keys and transformed values and is flexible enough to allow a recursive application to do the inner map transformation.


    Alternatively, we may adapt the solution of this answer to

    public static Map<String, Map<String, String>> convertRawMapToStringValues(
           Map<String, Map<String, Object>> in) {
    
        return in.entrySet().stream()
            .collect(Collectors.toMap(Map.Entry::getKey,
                e -> e.getValue().entrySet().stream()
                .collect(
                    HashMap::new,
                    (m,e2) -> m.put(e2.getKey(),
                        e2.getValue() == null? null: e2.getValue().toString()),
                    Map::putAll)));
    }
    

    Unlike the original toMap collector, this won’t throw on duplicate keys, but for this specific case where the input is already a Map, there shouldn’t be duplicate keys anyway.