victorio
victorio

Reputation: 6656

How to combine Map values from parent Map with java 8 stream

I have a map inside a map which looks like this:

Map<String, Map<Integer, BigDecimal>> mapInMap; //--> with values like: 
/*
"a": (1: BigDecimal.ONE),
"b": (2: BigDecimal.TEN),
"c": (1: BigDecimal.ZERO)
*/

And I would like to combine the inner maps by expecting the following result:

Map<Integer, BigDecimal> innerMapCombined; //--> with values:
/*
1: BigDecimal.ZERO,
2: BigDecimal.TEN
*/

This is my solution with predefining the combined map and using forEach:

Map<Integer, BigDecimal> combined = new HashMap<>();
mapInMap.forEach((str, innerMap) -> {
  innerMap.forEach(combined::putIfAbsent);
});

But this will ignore (1: BigDecimal.ZERO).

Could you provide a 1-line solution with java 8 stream?

Upvotes: 3

Views: 1334

Answers (2)

dreamcrash
dreamcrash

Reputation: 51483

The issue with your problem is that as soon as you initialize your maps, and add the duplicate keys on the inner maps, you will rewrite those keys, since those maps do not accept duplicated keys. Therefore, you need to first change this:

Map<String, Map<Integer, BigDecimal>> mapInMap;

to a Map that allows duplicated keys, for instance Multimap from Google Guava:

Map<String, Multimap<Integer, BigDecimal>> mapInMap = new HashMap<>();

where the inner maps are created like this:

 Multimap<Integer, BigDecimal> x1 = ArrayListMultimap.create();
 x1.put(1, BigDecimal.ONE);
 mapInMap.put("a", x1);

Only now you can try to solve your problem using Java 8 Streams API. For instance:

Map<Integer, BigDecimal> map = multiMap.values()
                                       .stream()
                                       .flatMap(map -> map.entries().stream())
                                       .collect(Collectors.toMap(Map.Entry::getKey,
                                                                 Map.Entry::getValue, 
                                                                 (v1, v2) -> v2));

The duplicate keys conflicts are solved using mergeFunction parameter of the toMap method. We explicitly express to take the second value (v1, v2) -> v2 in case of duplicates.

Upvotes: 1

Nikolas
Nikolas

Reputation: 44456

Problem:

To address why your current solution doesn't work is because Map#putIfAbsent method only adds and doesn't replace a value in a map if is already present.


Solution using for-each:

Map#put is a way to go, however its limitation is that you cannot decide whether you want to keep always the first value for such key, calculate a new one or use always the last value. For such reason I recommend to use either a combination of Map#computeIfPresent and Map#putIfAbsent or better a method that does all that at once which is Map#merge(K, V, BiFunction) with a BiFunction remappingFunction:

remappingFunction - the function to recompute a value if present

Map<Integer, BigDecimal> resultMap = new HashMap<>();
for (Map<Integer, BigDecimal> map: mapInMap.values()) {
    for (Map.Entry<Integer, BigDecimal> entry: map.entrySet()) {
         resultMap.merge(entry.getKey(), entry.getValue(), (l, r) -> r);
    }
}

Solution using Stream API:

To rewrite it in the Stream-alike solution, the approach would be identical. The only difference is the declarative syntax of Stream API, however, the idea is very same.

Just flatMap the structure and collect to a map with a Collector.toMap(Function, Function, BinaryOperator using BinaryOperator mergeFunction to merge duplicated keys.

mergeFunction - a merge function, used to resolve collisions between values associated with the same key, as supplied to Map.merge(Object, Object, BiFunction)

Map<Integer, BigDecimal> resultMap = mapInMap.values().stream()
    .flatMap(entries -> entries.entrySet().stream())
    .collect(Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue, (l, r) -> r));

Note: @dreamcrash also deserves a credit for his good Stream API answer in terms of speed.


Result:

{1=1, 2=10} is the result when you pring out such map (note that BigDecimal is printed as a number). This output matches your expected output.

1=BigDecimal.ZERO
2=BigDecimal.TEN

Notice the similarities between Map#merge(K, V, BiFunction) and Collector.toMap(Function, Function, BinaryOperator that use a very similar approach to the same result.

Upvotes: 1

Related Questions