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

10

291

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).

Moresque answered 18/9, 2014 at 2:14 Comment(0)
A
514
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.

Accustomed answered 18/9, 2014 at 2:21 Comment(2)
You can replace e -> e.getKey() with Map.Entry::getKey. But that’s a matter of taste/programming style.Astrology
Actually it is a matter of performance, your suggesting being slightly superior to the lambda 'style'Quid
L
38

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. :-)

Loom answered 18/9, 2014 at 6:0 Comment(1)
Function.identity() might look cool but since the first solution requires a map/hash lookup for every entry whereas all other solution don’t, I would not recommend it.Astrology
Q
19

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

Used like so

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));
Quilmes answered 18/9, 2014 at 2:23 Comment(0)
J
16

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

Maps.transformValues(originalMap, val -> ...)
Jubilant answered 5/7, 2017 at 22:19 Comment(3)
I like this approach, but be careful not to pass it a java.util.Function. Since it expects com.google.common.base.Function, Eclipse gives an unhelpful error - it says Function is not applicable for Function, which can be confusing: "The method transformValues(Map<K,V1>, Function<? super V1,V2>) in the type Maps is not applicable for the arguments (Map<Foo,Bar>, Function<Bar,Baz>)"Wheels
If you must pass a java.util.Function, you have two options. 1. Avoid the issue by using a lambda to let Java type inference figure it out. 2. Use a method reference like javaFunction::apply to produce a new lambda that type inference can figure out.Everywhere
Also note that, unlike other solutions on this page, this solution returns a view to the underlying map, not a copy.Hautesalpes
H
13

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));
Hogen answered 6/1, 2016 at 21:38 Comment(0)
O
4

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();
Omnipresent answered 29/1, 2016 at 4:52 Comment(0)
H
4

Although it is possible to remap the key or/and value in the collect part of the stream as shown in other answers, I think it should belong in the map part as that function is designed to transform the data within the stream. Next to that it should be easily repeatable without introducing additional complexity. The SimpleEntry object can be used which is already available since Java 6.

With Java 8

import java.util.AbstractMap.SimpleEntry;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;

public class App {

    public static void main(String[] args) {
        Map<String, String> x;
        Map<String, Integer> y = x.entrySet().stream()
                .map(entry -> new SimpleEntry<>(entry.getKey(), Integer.parseInt(entry.getValue())))
                .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
    }

}

With Java 9+

With the release of Java 9 a static method within the Map interface was introduced to make it easier to just create an entry without to instantiate a new SimpleEntry as shown in the previous example.

import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;

public class App {

    public static void main(String[] args) {
        Map<String, String> x;
        Map<String, Integer> y = x.entrySet().stream()
                .map(entry -> Map.entry((entry.getKey(), Integer.parseInt(entry.getValue())))
                .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
    }

}
Howlond answered 4/1, 2023 at 11:12 Comment(0)
C
3

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;}
               ));
Coverdale answered 26/6, 2016 at 2:39 Comment(1)
I started with this custom collector as a base and wanted to add that, at least when using parallelStream() instead of stream(), the binaryOperator should be rewritten to something more akin to map2.entrySet().forEach(entry -> { if (map1.containsKey(entry.getKey())) { map1.get(entry.getKey()).merge(entry.getValue()); } else { map1.put(entry.getKey(),entry.getValue()); } }); return map1 or values will be lost when reducing.Splendent
L
2

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);
Lief answered 23/2, 2016 at 11:58 Comment(0)
C
-1

A declarative and simpler Java8+ solution would be :

yourMap.replaceAll((key, val) -> computeNewVal);

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

Culbreth answered 10/2, 2018 at 13:2 Comment(1)
1. please wrap code in code block. 2. it's usually not good practice to modify input. always better to create a new objectPaba

© 2022 - 2025 — McMap. All rights reserved.