Adding the value parts of multiple HashMap into a new one - java

I have three HashMap<String, Integer> where some keys may be present in two/three Maps.
I'm looking for a new HashMap that will combine all three in a new one where the value part will be a simple summation of all the values (where key is same).
I did that with merge() method,
Map<String, Integer> combine = new HashMap<>(map1);
map2.forEach((k, v) -> combine.merge(k, v, Integer::sum));
map3.forEach((k, v) -> combine.merge(k, v, Integer::sum));
Wondering, How can I do it using Stream and/or Collection classes?

I suppose:
Map<String, Integer> combine = Stream.of(map1, map2, map3)
.map(Map::entrySet)
.flatMap(Set::stream)
.collect(toMap(Entry::getKey, Entry::getValue, Math::addExact));
This does have the advantage of not modifying any of the maps. You approach would be a problem if map1 happens to be an unmodifiable map.
In general, avoid modification for achieving results that don’t require it, especially when such a side effect is unexpected.

Related

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

How to group values of map into lists based on key with java streams?

Say I have mappings from Strings to a Mapping from Strings to int, such as
Map<String, Map<String, Integer>> myMap1 = new HashMap<>();
myMap1.put("A", Map.of("X", 1))
myMap1.put("B", Map.of("Y", 1))
Map<String, Map<String, Integer>> myMap2 = new HashMap<>();
myMap2.put("B", Map.of("Y", 3))
I would like to merge these mappings such that we get a mapping where the key is the inner map's key, and the value would be the average of the inner maps values of the same keys.
So the output to the example above would be
{"X" : 1, "Y", 2}
We can discard the outer map's key altogether.
What is the nicest way to do this with java. I thought there might be some nice way to do it with Collectors.groupBy method but I am quite inexperienced with this.
I’m going to assume there might be more than two maps, so let’s make a List out of them:
Collection<Map<String, Map<String, Integer>>> myMaps =
List.of(myMap1, myMap2);
Then we can use flatMap on the values() of each Map, which gives us a stream of Map<String, Integer> maps.
We can obtain the entrySet() of each of those, then apply flatMap to the streams of those entry sets, to give us a single Stream of Map.Entry<String, Integer> objects, which we can then group.
There is a groupingBy method which takes a second Collector for customizing the values of the groups, by collecting all of the grouped values seen. We can use that to get our averages, using an averaging collector.
Map<String, Double> averages =
myMaps.stream().flatMap(map -> map.values().stream()) // stream of Map<String, Integer>
.flatMap(innerMap -> innerMap.entrySet().stream()) // stream of Map.Entry<String, Integer>
.collect(Collectors.groupingBy(Map.Entry::getKey, // group by String key
Collectors.averagingInt(Map.Entry::getValue))); // value for each key = average of its Integers

Convert Map<String, Object> to Map<String, Set<Object>> with filter and streams

I would like to convert my map which looks like this:
{
key="someKey1", value=Apple(id="1", color="green"),
key="someKey2", value=Apple(id="2", color="red"),
key="someKey3", value=Apple(id="3", color="green"),
key="someKey4", value=Apple(id="4", color="red"),
}
to another map which puts all apples of the same color into the same list:
{
key="red", value=list={apple1, apple3},
key="green", value=list={apple2, apple4},
}
I tried the following:
Map<String, Set<Apple>> sortedApples = appleMap.entrySet()
.stream()
.collect(Collectors.toMap(l -> l.getColour, ???));
Am I on the right track? Should I use filters for this task? Is there an easier way?
Collectors.groupingBy is more suitable than Collectors.toMap for this task (though both can be used).
Map<String, List<Apple>> sortedApples =
appleMap.values()
.stream()
.collect(Collectors.groupingBy(Apple::getColour));
Or, to group them into Sets use:
Map<String, Set<Apple>> sortedApples =
appleMap.values()
.stream()
.collect(Collectors.groupingBy(Apple::getColour,
Collectors.mapping(Function.identity(),
Collectors.toSet())));
or (as Aomine commented):
Map<String, Set<Apple>> sortedApples =
appleMap.values()
.stream()
.collect(Collectors.groupingBy(Apple::getColour, Collectors.toSet()));
if you want to proceed with toMap you can get the result as follows:
map.values() // get the apples
.stream() // Stream<Apple>
.collect(toMap(Apple::getColour, // group by colour
v -> new HashSet<>(singleton(v)), // have values as set of apples
(l, r) -> {l.addAll(r); return l;})); // merge colliding apples by colour
stream over the map values instead of entrySet because we're not concerned with the map keys.
Apple::getColour is the keyMapper function used to extract the "thing" we wish to group by, in this case, the Apples colour.
v -> new HashSet<>(singleton(v)) is the valueMapper function used for the resulting map values
(l, r) -> {l.addAll(r); return l;} is the merge function used to combine two HashSet's when there is a key collision on the Apple's colour.
finally, the resulting map is a Map<String, Set<Apple>>
but this is better with groupingBy and toSet as downstream:
map.values().stream().collect(groupingBy(Apple::getColour, toSet()));
stream over the map values instead of entrySet because we're not concerned with the map keys.
groups the Apple's by the provided classification function i.e. Apple::getColour and then collect the values in a Set hence the toSet downstream collector.
finally, the resulting map is a Map<String, Set<Apple>>
short, readable and the idiomatic approach.
You could also do it without a stream:
Map<String, Set<Apple>> res = new HashMap<>();
map.values().forEach(a -> res.computeIfAbsent(a.getColour(), e -> new HashSet<>()).add(a));
iterate over the map values instead of entrySet because we're not concerned with the map keys.
if the specified key a.getColour() is not already associated with a value, attempts to compute its value using the given mapping function e -> new HashSet<>() and enters it into the map. we then add the Apple to the resulting set.
if the specified key a.getColour() is already associated with a value computeIfAbsent returns the existing value associated with it and then we call add(a) on the HashSet to enter the Apple into the set.
finally, the resulting map is a Map<String, Set<Apple>>
You can use Collectors.groupingBy and Collectors.toSet()
Map<String, Set<Apple>> sortedApples = appleMap.values() // Collection<Apple>
.stream() // Stream<Apple>
.collect(Collectors.groupingBy(Apple::getColour, // groupBy colour
Collectors.mapping(a -> a, Collectors.toSet()))); // collect to Set
You've asked how to do it with streams, yet here's another way:
Map<String, Set<Apple>> result = new LinkedHashMap<>();
appleMap.values().forEach(apple ->
result.computeIfAbsent(apple.getColor(), k -> new LinkedHashSet<>()).add(apple));
This uses Map.computeIfAbsent, which either returns the set mapped to that color or puts an empty LinkedHashSet into the map if there's nothing mapped to that color yet, then adds the apple to the set.
EDIT: I'm using LinkedHashMap and LinkedHashSet to preserve insertion order, but could have used HashMap and HashSet, respectively.

How can I concatenate two Java streams while specifying specific logic for items that don't contain duplicates?

I have two maps that use the same object as keys. I want to merge these two streams by key. When a key exists in both maps, I want the resulting map to run a formula. When a key exists in a single map I want the value to be 0.
Map<MyKey, Integer> map1;
Map<MyKey, Integer> map2;
<Map<MyKey, Double> result =
Stream.concat(map1.entrySet().stream(), map2.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey, Map.Entry::getValue,
(val1, val2) -> (val1 / (double)val2) * 12D));
This will use the formula if the key exists in both maps, but I need an easy way to set the values for keys that only existed in one of the two maps to 0D.
I can do this by doing set math and trying to calculate the inner-join of the two keySets, and then subtracting the inner-join result from the full outer join of them... but this is a lot of work that feels unnecessary.
Is there a better approach to this, or something I can easily do using the Streaming API?
Here is a simple way, only stream the keys, and then looking up the values, and leaving the original maps unchanged.
Map<String, Double> result =
Stream.concat(map1.keySet().stream(), map2.keySet().stream())
.distinct()
.collect(Collectors.toMap(k -> k, k -> map1.containsKey(k) && map2.containsKey(k)
? map1.get(k) * 12d / map2.get(k) : 0d));
Test
Map<String, Integer> map1 = new HashMap<>();
Map<String, Integer> map2 = new HashMap<>();
map1.put("A", 1);
map1.put("B", 2);
map2.put("A", 3);
map2.put("C", 4);
// code above here
result.entrySet().forEach(System.out::println);
Output
A=4.0
B=0.0
C=0.0
For this solution to work, your initial maps should be Map<MyKey, Double>. I'll try to find another solution that will work if the values are initially Integer.
You don't even need streams for this! You should simply be able to use Map#replaceAll to modify one of the Maps:
map1.replaceAll((k, v) -> map2.containsKey(k) ? 12D * v / map2.get(k) : 0D);
Now, you just need to add every key to map1 that is in map2, but not map1:
map2.forEach((k, v) -> map1.putIfAbsent(k, 0D));
If you don't want to modify either of the Maps, then you should create a deep copy of map1 first.
Stream.concat is not the right approach here, as you are throwing the elements of the two map together, creating the need to separate them afterward.
You can simplify this by directly doing the intended task of processing the intersection of the keys by applying your function and processing the other keys differently. E.g. when you stream over one map instead of the concatenation of two maps, you only have to check for the presence in the other map to either, apply the function or use zero. Then, the keys only present in the second map need to be put with zero in a second step:
Map<MyKey, Double> result = map1.entrySet().stream()
.collect(Collectors.collectingAndThen(
Collectors.toMap(Map.Entry::getKey, e -> {
Integer val2 = map2.get(e.getKey());
return val2==null? 0.0: e.getValue()*12.0/val2;
}),
m -> {
Map<MyKey, Double> rMap = m.getClass()==HashMap.class? m: new HashMap<>(m);
map2.keySet().forEach(key -> rMap.putIfAbsent(key, 0.0));
return rMap;
}));
This clearly suffers from the fact that Streams don’t offer convenience methods for processing map entries. Also, we have to deal with the unspecified map type for the second processing step. If we provided a map supplier, we also had to provide a merge function, making the code even more verbose.
The simpler solution is to use the Collection API rather than the Stream API:
Map<MyKey, Double> result = new HashMap<>(Math.max(map1.size(),map2.size()));
map2.forEach((key, value) -> result.put(key, map1.getOrDefault(key, 0)*12D/value));
map1.keySet().forEach(key -> result.putIfAbsent(key, 0.0));
This is clearly less verbose and potentially more efficient as it omits some of the Stream solution’s processing steps and provides the right initial capacity to the map. It utilizes the fact that the formula evaluates to the desired zero result if we use zero as default for the first map’s value for absent keys. If you want to use a different formula which doesn’t have this property or want to avoid the calculation for absent mappings, you’d have to use
Map<MyKey, Double> result = new HashMap<>(Math.max(map1.size(),map2.size()));
map2.forEach((key, value2) -> {
Integer value1 = map1.get(key);
result.put(key, value1 != null? value1*12D/value2: 0.0);
});
map1.keySet().forEach(key -> result.putIfAbsent(key, 0.0));

Reducing/Collecting stream of a flattened map in particular traversing order (not Depth-first search)

I'm checking everything for reducing/collecting a flattened map in my particular traverse order (not Depth-first search) and can't find an example/collectors signature for this kind of operation. I'll try to explain it with a simplified example:
I got 3 maps
Map1: {1,"a1"; 2, "b1"; 3, "c1"}
Map2: {1,"a2", 2, "b2"; 3, "c2"}
Map3: {1,"a3"; 2, "b3"; 3, "c3"}
I want to end up collecting it as following:
LinkedHashSet() {new LinkedHashSet("a1","a2","a3"),
new LinkedHashSet("b1","b2","b3"),
new LinkedHashSet("c1","c2","c3")}
If I use flatMap I would have something like 1,2,3,1,2,3,1,2,3 instead of 1,1,1,2,2,2,3,3,3, so I am trying some kind of dirty grouping by, but cannot survive to all the signatures. I am trying things like this but I frankly get lost, it must be an easier approach:
...flatMap( map-> map.stream() )
.collect( Collectors.groupingBy(Map.Entry::getKey, Collectors.reducing(Map.Entry::getValue, Collectors.toCollection(LinkedHashSet::new)))
(This is just a simplification)
Your original approach isn’t so bad, but you have small errors within it. Unfortunately, current compilers have a tendency to flood the error messages with reports about generic signature mismatches, making it easy to overlook the simpler errors like methods or types that haven’t been found, which are more than often the actual cause of the problem.
First, there is no stream() method in Map. You have to decide which collection view to stream on, keySet(), entrySet() or values(). Obviously, you want to use map-> map.entrySet().stream() instead of map-> map.stream().
Second, you used Collectors.reducing instead of the obviously intended Collectors.mapping.
Fixing these errors should be sufficient to let the generic signature errors go away as well; the result of the groupingBy collector is a Map<Integer, LinkedHashSet<String>> then, but unfortunately, it’s not an order maintaining map by default. Therefore, you have to add a map Supplier, i.e. LinkedHashMap::new to enforce the use of an order maintaining Map.
Then, you have to copy the result map’s values() to a LinkedHashSet to get a result of the desired type, i.e. new LinkedHashSet<>(resultMap.values()).
Putting it all together, we get:
Map<Integer,String> map1 = map(1,"a1", 2,"b1", 3,"c1");
Map<Integer,String> map2 = map(1,"a2", 2,"b2", 3,"c2");
Map<Integer,String> map3 = map(1,"a3", 2,"b3", 3,"c3");
Map<Integer, LinkedHashSet<String>> m = Stream.of(map1, map2, map3)
.flatMap( map -> map.entrySet().stream() )
.collect( Collectors.groupingBy(Map.Entry::getKey,
LinkedHashMap::new,
Collectors.mapping(Map.Entry::getValue,
Collectors.toCollection(LinkedHashSet::new))));
LinkedHashSet<LinkedHashSet<String>> result=new LinkedHashSet<>(m.values());
// [[a1, a2, a3], [b1, b2, b3], [c1, c2, c3]]
System.out.println(result);
 
static <K,V> Map<K,V> map(K k1, V v1, K k2, V v2, K k3, V v3) {
LinkedHashMap<K,V> m=new LinkedHashMap<>();
m.put(k1, v1);
m.put(k2, v2);
m.put(k3, v3);
return m;
}
I hope, you still recognize your original approach within the changed code.
You can also integrate the post-processing step into the Collector via collectingAndThen:
LinkedHashSet<LinkedHashSet<String>> result = Stream.of(map1, map2, map3)
.flatMap( map -> map.entrySet().stream() )
.collect( Collectors.collectingAndThen(
Collectors.groupingBy(Map.Entry::getKey,
LinkedHashMap::new,
Collectors.mapping(Map.Entry::getValue,
Collectors.toCollection(LinkedHashSet::new))),
m -> new LinkedHashSet<>(m.values())));
It should be mentioned that this result will only look rectangular, like a transposed table of the original maps, if the values are all unique. LinkedHashSet, like any other Set, will eliminate duplicate elements. This does not only apply to the sub-sets, but also to the result set that will eliminate identical sub-sets.
As correctly noticed by Holger I initially missed your point about traverse order.
But traverse order only makes sense if your initial Maps (Map1, Map2, Map3) are some instances of a Map that are ordered, like LinkedHashMap or a SortedMap.
I'll try to explain. Suppose this example (using jdk-9 new Immutable Maps - that internally use a randomization pattern, meaning that the order in which you get the entries is not guaranteed).
Map<Integer, String> map1 = Map.of(3, "a1", 2, "b1", 1, "c1");
Map<Integer, String> map2 = Map.of(3, "a2", 2, "b2", 1, "c2");
Map<Integer, String> map3 = Map.of(3, "a3", 2, "b3", 1, "c3");
System.out.println(Stream.of(map1, map2, map3)
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toList()));
The result of this operation could be of of these two for example.
[2=b1, 3=a1, 1=c1, 2=b2, 3=a2, 1=c2, 2=b3, 3=a3, 1=c3]
[1=c1, 3=a1, 2=b1, 1=c2, 3=a2, 2=b2, 1=c3, 3=a3, 2=b3]
At this point in time HashMap does not have any sort of randomization done internally or any other functionality that could alter the order of the entries when iterating over it, but it could happen on a future release, so you can't rely on that.
Thus, assuming your maps are LinkedHashMaps:
Set<Set<String>> result = Stream.of(map1, map2, map3).flatMap(e -> e.entrySet().stream())
.collect(Collectors.collectingAndThen(Collectors.toMap(
e -> e.getKey(),
e -> {
Set<String> l = new LinkedHashSet<>();
l.add(e.getValue());
return l;
},
(left, right) -> {
left.addAll(right);
return left;
},
LinkedHashMap::new),
map -> map.values().stream().collect(Collectors.toCollection(LinkedHashSet::new))));

Categories

Resources