简体   繁体   中英

how to use java streams api to transform a hierarchal map to a flat map

So, I have this function which takes a map, where a name is associated with one of a number of different indexes into an array. The index numbers will only ever have one name associated with them, so no duplicates and no nulls so it's safe to flatten the hierarchy using the following function.

public Map<Integer, Object> normalize( Map<Object, List<Integer>> hierarchalMap ) {

    Map<Integer, Object> normalizedMap = new HashMap<>();
    for (Map.Entry<Object, List<Integer>> entry : hierarchalMap.entrySet()) {
        for (Integer integer : entry.getValue()) {
            noramizedMap.put(integer, entry.getKey());
        }
    }
    return normalizedMap;
}

I'm trying to change this function into using the streams API and I've gotten this far:

Map<Integer, Object> noramizedMap = new HashMap<>();
for (Map.Entry<Object, List<Integer>> entry : vars.entrySet()) {

    entry.getValue().forEach(e -> noramizedMap.put(e, entry.getValue()));

}

If this were some other functional language i'd do a partial bind or whatever but with java I try and unwrap the outer loop into a stream...collectTo i just get lost.

Assuming my comment is correct, you can do with Streams like this:

hierarchalMap.entrySet()
            .stream()
            .flatMap(entry -> entry.getValue()
                    .stream()
                    .map(i -> new AbstractMap.SimpleEntry<>(entry.getKey(), i)))
            .collect(Collectors.toMap(Entry::getValue, Entry::getKey));

This assumes there are no duplicates and no nulls.

I think this should do what you need:

public Map<Integer, Object> normalizeJava8(Map<Object, List<Integer>> hierarchalMap ) {
    return hierarchalMap
            .entrySet()
            .stream()
            .collect(
                    HashMap::new,
                    (map, entry) -> 
                            entry.getValue().forEach(i -> map.put(i, entry.getKey())),
                    HashMap::putAll);
}

It's often the case when working with Java 8 streams that you have to put more logic into the "collect" part of the operation than in an equivalent construction in another language, due partially to the lack of a convenient tuple type. Intuitively it might make seem more sane to create a list of pairs then collect them into a map, but that ends up being more code and more computationally intensive than putting that logic in the .collect

With streams and Java 9:

Map<Integer, Object> normalizedMap = hierarchalMap.entrySet().stream()
    .flatMap(e -> e.getValue().stream().map(i -> Map.entry(i, e.getKey())))
    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

This is almost identical to this answer , except I'm using the Map.entry() method to create the pairs and am putting the integers as the keys.


Here's a another, less verbose way to do the same without streams:

Map<Integer, Object> normalizedMap = new HashMap<>();
hierarchalMap.forEach((k, v) -> v.forEach(i -> normalizedMap.put(i, k)));

Here are two convenience collectors you can use in java 8 that are not just limited to maps.

public static <T, K, V> Collector<T, ?, Map<K, V>> flatInverseMapping(Function<? super T, ? extends Stream<? extends K>> keyStreamFunction,
        Function<? super T, ? extends V> valueFunction) {
    return Collector.of(HashMap::new,
            (m, v) ->
                    keyStreamFunction.apply(v).forEach(innerV -> m.put(innerV, valueFunction.apply(v))),
            (m1, m2) -> {
                m1.putAll(m2);
                return m2;
            });
}

public static <T, K, V> Collector<T, ?, Map<K, V>> flatInverseMapping(Function<? super T, ? extends Collection<? extends K>> keyStreamFunction,
        Function<? super T, ? extends V> valueFunction) {
    return Collector.of(HashMap::new,
            (m, v) ->
                    keyStreamFunction.apply(v).forEach(innerV -> m.put(innerV, valueFunction.apply(v))),
            (m1, m2) -> {
                m1.putAll(m2);
                return m2;
            });
}

Since both streams and collections have a forEach, it makes both implementations identical other than input object.

For a brief explanation of how this works, the output of the collector is a Map of the K and V (key and value) params that are defined by the outputs of the two functions. For each of the key values derived from the input object in the stream, the same value function will be applied so the map will be inverted with a consistent value across shared keys. Note that if there are multiple items in the stream that resolve to the same keys, this will not throw the merge exception like the normal toMap implementation. The BiConsumer will need to be changed to this to maintain that behavior:

(var1, var2) -> {
    Iterator<Map.Entry<K,V>> var3 = var2.entrySet().iterator();

    while(var3.hasNext()) {
        Map.Entry<K,V> var4 = var3.next();
        var1.merge(var4.getKey(), var4.getValue(), (v0, v1) -> {
            throw new IllegalStateException(String.format("Duplicate key %s", v0));
        });
    }
    return var1;
}

For reference, this was essentially copied from the Collectors.toMap code.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM