Reputation: 917
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?
Upvotes: 3
Views: 1132
Reputation: 298409
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));
Upvotes: 2
Reputation: 29710
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 Map
s:
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 Map
s, then you should create a deep copy of map1
first.
Upvotes: 4
Reputation: 159135
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
Upvotes: 6