Anthony
Anthony

Reputation: 917

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?

Upvotes: 3

Views: 1132

Answers (3)

Holger
Holger

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

Jacob G.
Jacob G.

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

Upvotes: 4

Andreas
Andreas

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

Related Questions