Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to efficiently filter and collect to a resulting map with differently derived map values from a few maps?

I have a few Map objects that are keyed by the same type K with differently typed values V1...VN, which for the purpose of this question do not share a supertype*:

Map<K, V1> kv1
Map<K, V2> kv2
Map<K, V3> kv3
...
Map<K, VN> kvN

I need to create a resulting map of type Map<K, V>, by filtering each of these maps differently, and then use a 'value mapper' to map the V1...VN values to a commonly-typed V new values (i.e. a Function<? super Entry<K, VN>, ? extends V>) on these maps. As such, I have the following static helper method to perform the first two steps:

public static <K, VN, V> Map<K, V> filterAndMapValue(final Map<K, VN> map,
        final Predicate<? super Entry<K, VN>> predicate,
        final Function<? super Entry<K, VN>, ? extends V> mapper) {
    return map.entrySet().stream().filter(predicate)
            .collect(Collectors.toMap(Entry::getKey, mapper));
}

My current use cases make it safe to assume that only after the filtering on each map will give me distinct keys for the final Map object (there can be the same keys used in each map), but in the event that does not hold true in the future, I know I can provide a supplementary mergeFunction expression to Collectors.toMap(Function, Function, BinaryOperator) to handle this properly.

The final code now reads something like the following:

Map<K,V> result = filterAndMapValue(kv1, predicateForKV1, mapV1toV);
result.putAll(filterAndMapValue(kv2, predicateForKV2, mapV2toV));
result.putAll(filterAndMapValue(kv2, predicateForKV3, mapV3toV));
...
result.putAll(filterAndMapValue(kvN, predicateForKVN, mapVNtoV));
// do something with result

Question: Is there a more efficient way of doing this? This sounds like yet another case of reducing stuff (filtered maps) into a final collection (Map) which requires different reduction invocations (the value mapping part), and I am not sure if I am approaching this the correct way or not.

* - If they do, then I guess V1...VN can implement a parent method, say V convertToV(Object... possiblyAdditionalArgumentsToPerformTheConversion), so that my problem is reduced to just applying different forms of filtering for different maps. If there is also a simpler solution given this alternate assumption, feel free to mention it too.

like image 363
h.j.k. Avatar asked Apr 08 '15 07:04

h.j.k.


People also ask

How do you filter a map based on value in Java 8?

If I understand your filtering criteria correctly, you want to check if the filtered Stream you produced from the value List has any elements, and if so, pass the corresponding Map entry to the output Map . Map<String, List<BoMLine>> filtered = materials. entrySet() . stream() .

Can we use map and filter together in Java?

All you need is a mapping function to convert one object to another, and the map() function will do the transformation for you. It is also an intermediate stream operation which means you can call other Stream methods like a filter or collect on this to create a chain of transformation.

How do you filter a Stream map?

Since our filter condition requires an int variable we first need to convert Stream of String to Stream of Integer. That's why we called the map() function first. Once we have the Stream of Integer, we can apply maths to find out even numbers. We passed that condition to the filter method.


1 Answers

If you change your method to

public static <K, VN, V> Stream<Entry<K, V>> filterAndMapValue(Map<K, VN> map,
    Predicate<? super Entry<K, VN>> predicate,
    Function<? super Entry<K, VN>, ? extends V> mapper) {

    return map.entrySet().stream().filter(predicate)
              .map(e->new AbstractMap.SimpleEntry<>(e.getKey(), mapper.apply(e)));
}

you can perform the operation as a single Stream operation like:

Stream.of(filterAndMapValue(kv1, predicateForKV1, mapV1toV),
          filterAndMapValue(kv2, predicateForKV2, mapV2toV),
          filterAndMapValue(kv3, predicateForKV3, mapV3toV),
          …)
      .flatMap(Function.identity())
      .collect(Collectors.toMap(Entry::getKey, Entry::getValue));

Note that for a small number of input maps, you may use Stream.concat but as the number grows, the approach shown above is preferable.

I wouldn’t expect a noticeable performance gain, but this approach will verify your assumption that there are no duplicate keys in the remaining entries.

like image 101
Holger Avatar answered Oct 07 '22 10:10

Holger