Java8: HashMap<X, Y> to HashMap<X, Z> using Stream / Map-Reduce / Collector

JavaMapreduceJava 8Java StreamCollectors

Java Problem Overview


I know how to "transform" a simple Java List from Y -> Z, i.e.:

List<String> x;
List<Integer> y = x.stream()
        .map(s -> Integer.parseInt(s))
        .collect(Collectors.toList());

Now I'd like to do basically the same with a Map, i.e.:

INPUT:
{
  "key1" -> "41",    // "41" and "42"
  "key2" -> "42"      // are Strings
}

OUTPUT:
{
  "key1" -> 41,      // 41 and 42
  "key2" -> 42       // are Integers
}

The solution should not be limited to String -> Integer. Just like in the List example above, I'd like to call any method (or constructor).

Java Solutions


Solution 1 - Java

Map<String, String> x;
Map<String, Integer> y =
    x.entrySet().stream()
        .collect(Collectors.toMap(
            e -> e.getKey(),
            e -> Integer.parseInt(e.getValue())
        ));

It's not quite as nice as the list code. You can't construct new Map.Entrys in a map() call so the work is mixed into the collect() call.

Solution 2 - Java

Here are some variations on Sotirios Delimanolis' answer, which was pretty good to begin with (+1). Consider the following:

static <X, Y, Z> Map<X, Z> transform(Map<? extends X, ? extends Y> input,
                                     Function<Y, Z> function) {
    return input.keySet().stream()
        .collect(Collectors.toMap(Function.identity(),
                                  key -> function.apply(input.get(key))));
}

A couple points here. First is the use of wildcards in the generics; this makes the function somewhat more flexible. A wildcard would be necessary if, for example, you wanted the output map to have a key that's a superclass of the input map's key:

Map<String, String> input = new HashMap<String, String>();
input.put("string1", "42");
input.put("string2", "41");
Map<CharSequence, Integer> output = transform(input, Integer::parseInt);

(There is also an example for the map's values, but it's really contrived, and I admit that having the bounded wildcard for Y only helps in edge cases.)

A second point is that instead of running the stream over the input map's entrySet, I ran it over the keySet. This makes the code a little cleaner, I think, at the cost of having to fetch values out of the map instead of from the map entry. Incidentally, I initially had key -> key as the first argument to toMap() and this failed with a type inference error for some reason. Changing it to (X key) -> key worked, as did Function.identity().

Still another variation is as follows:

static <X, Y, Z> Map<X, Z> transform1(Map<? extends X, ? extends Y> input,
                                      Function<Y, Z> function) {
    Map<X, Z> result = new HashMap<>();
    input.forEach((k, v) -> result.put(k, function.apply(v)));
    return result;
}

This uses Map.forEach() instead of streams. This is even simpler, I think, because it dispenses with the collectors, which are somewhat clumsy to use with maps. The reason is that Map.forEach() gives the key and value as separate parameters, whereas the stream has only one value -- and you have to choose whether to use the key or the map entry as that value. On the minus side, this lacks the rich, streamy goodness of the other approaches. :-)

Solution 3 - Java

A generic solution like so

public static <X, Y, Z> Map<X, Z> transform(Map<X, Y> input,
		Function<Y, Z> function) {
	return input
			.entrySet()
			.stream()
			.collect(
					Collectors.toMap((entry) -> entry.getKey(),
							(entry) -> function.apply(entry.getValue())));
}

Example

Map<String, String> input = new HashMap<String, String>();
input.put("string1", "42");
input.put("string2", "41");
Map<String, Integer> output = transform(input,
			(val) -> Integer.parseInt(val));

Solution 4 - Java

Guava's function Maps.transformValues is what you are looking for, and it works nicely with lambda expressions:

Maps.transformValues(originalMap, val -> ...)

Solution 5 - Java

Does it absolutely have to be 100% functional and fluent? If not, how about this, which is about as short as it gets:

Map<String, Integer> output = new HashMap<>();
input.forEach((k, v) -> output.put(k, Integer.valueOf(v));

(if you can live with the shame and guilt of combining streams with side-effects)

Solution 6 - Java

My StreamEx library which enhances standard stream API provides an EntryStream class which suits better for transforming maps:

Map<String, Integer> output = EntryStream.of(input).mapValues(Integer::valueOf).toMap();

Solution 7 - Java

An alternative that always exists for learning purpose is to build your custom collector through Collector.of() though toMap() JDK collector here is succinct (+1 here) .

Map<String,Integer> newMap = givenMap.
                entrySet().
                stream().collect(Collector.of
               ( ()-> new HashMap<String,Integer>(),
                       (mutableMap,entryItem)-> mutableMap.put(entryItem.getKey(),Integer.parseInt(entryItem.getValue())),
                       (map1,map2)->{ map1.putAll(map2); return map1;}
               ));

Solution 8 - Java

If you don't mind using 3rd party libraries, my cyclops-react lib has extensions for all JDK Collection types, including Map. We can just transform the map directly using the 'map' operator (by default map acts on the values in the map).

   MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                                .map(Integer::parseInt);

bimap can be used to transform the keys and values at the same time

  MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                               .bimap(this::newKey,Integer::parseInt);

Solution 9 - Java

The declarative and simpler solution would be :

map.replaceAll((key, val) -> getUpdatedListFor(key, val));

yourMutableMap.replaceAll((key, val) return_value_of_bi_your_function); Nb. be aware your modifying your map state. So this may not be what you want.

Cheers to : http://www.deadcoderising.com/2017-02-14-java-8-declarative-ways-of-modifying-a-map-using-compute-merge-and-replace/

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBenjamin MView Question on Stackoverflow
Solution 1 - JavaJohn KugelmanView Answer on Stackoverflow
Solution 2 - JavaStuart MarksView Answer on Stackoverflow
Solution 3 - JavaSotirios DelimanolisView Answer on Stackoverflow
Solution 4 - JavaAlex KraussView Answer on Stackoverflow
Solution 5 - JavaLukas EderView Answer on Stackoverflow
Solution 6 - JavaTagir ValeevView Answer on Stackoverflow
Solution 7 - JavaIraView Answer on Stackoverflow
Solution 8 - JavaJohn McCleanView Answer on Stackoverflow
Solution 9 - JavaBreton F.View Answer on Stackoverflow