有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

在Java 8中使用嵌套数据结构在功能上反转映射

我有个问题,现在快把我逼疯了。我试图避免为这个贴图反转创建一个中间对象。(目标透视:我有一个嵌套数据结构的地图,我想反转和分解。所以

Map<Foo,Set<String>> fooStringMap

变成

Map<String,Foo> expandedStringFooMap

//Inverting a map is simple
private <X,Y> Map<Y,X> invertMap(Map<X,Y> source){
    return source.entrySet().stream()
                            .collect(Collectors.toMap(Entry::getValue,Entry::getKey)

private <A,B> Map<A,B> explodeMapWithCollection(Map<? extends Collection<A>, B> collectionMap){
 collectionMap.entrySet().stream()
            .flatMap(x -> x.getKey().stream().collect(Collectors.toMap(Function.identity(),x.getValue())))
            .collect(Collectors.toMap(Entry::getKey,Entry::getValue));
}

目前,这不起作用。我甚至不认为上面会编译,所以只考虑它是伪代码。

我用这样的一对解决了这个问题:

someMap.keySet().stream().flatMap(key->someMap.get(key).stream().map(val -> new 
Pair<>(val,key))).collect(Collectors.toMap(Pair::getLeft,Pair::getRight)));

这就像一种魅力,但我(出于我自己的启发)希望避免创建中间对。我知道一定有办法做到这一点,但我似乎迷路了


共 (4) 个答案

  1. # 1 楼答案

    下面是一种在条目集上使用自定义^{}的方法。有人可能会说,由于累加器中隐藏了forEach,这不是“完全功能”,但在某个时候,必须创建映射条目,我不确定是否有一种“优雅”的方式来使用来自Set的流(条目值),并且仍然有可能访问条目键(它将成为新条目的值)

    附带说明(尽管我冒着被否决的风险,拿起棍棒进行程序性编程):你不必仅仅因为你能做到就用功能性的方式来做。当你说你“迷失在语法中”,那么

    1. 几周后再次阅读此代码时,您会怎么想
    2. 第一次阅读此代码时,您的同事会怎么想?(我担心的是那个带着电锯和守门员面具的人…)

    我建议保持简单。(尽管最通用的程序形式乍一看仍可能令人困惑)

    import java.util.Arrays;
    import java.util.Collection;
    import java.util.LinkedHashMap;
    import java.util.LinkedHashSet;
    import java.util.Map;
    import java.util.Map.Entry;
    import java.util.Set;
    
    public class MapInvert
    {
        public static void main(String[] args)
        {
            Map<Integer, Set<String>> map = 
                new LinkedHashMap<Integer, Set<String>>();
    
            map.put(1, new LinkedHashSet<String>(Arrays.asList("A","B","C")));
            map.put(2, new LinkedHashSet<String>(Arrays.asList("D","E","F")));
            map.put(3, new LinkedHashSet<String>(Arrays.asList("G","H","I")));
    
            Map<String, Integer> resultA = inverseEx(map);
            System.out.println("Procedural: "+resultA);
    
            Map<String, Integer> resultB = map.entrySet().stream().collect(
                LinkedHashMap::new, 
                (m, e) -> e.getValue().forEach(v -> m.put(v, e.getKey())), 
                (m0, m1) -> m0.putAll(m1));
            System.out.println("Functional: "+resultB);
        }
    
        /**
         * Invert the given map, by mapping each element of the values to
         * the respective key
         *  
         * @param map The input map
         * @return The inverted map
         */
        private static <K, V> Map<V, K> inverseEx(
            Map<K, ? extends Collection<? extends V>> map)
        {
            Map<V, K> result = new LinkedHashMap<V, K>();
            for (Entry<K, ? extends Collection<? extends V>> e : map.entrySet())
            {
                for (V v : e.getValue())
                {
                    result.put(v, e.getKey());
                }
            }
            return result;
        }
    }
    
  2. # 2 楼答案

    可以说,我想在这里改变这个问题的术语。谷歌优秀的Guava library有一个^{} interface和一个^{}子类型,有一些实现。{}的文档告诉我们:

    You can visualize the contents of a multimap either as a map from keys to nonempty collections of values:

    • a → 1, 2
    • b → 3

    ... or as a single "flattened" collection of key-value pairs:

    • a → 1
    • a → 2
    • b → 3

    SetMultimap类型有一个^{} method返回Set<Map.Entry<K, V>>结果。您可以直接在该流上stream()map()反转条目,然后使用该流构建反向映射。所以像这样的事情(我肯定我做的不是最好的方式):

    public static <K, V> ImmutableSetMultimap<V, K> invert(SetMultimap<? extends K, ? extends V> input) {
        return input
                .entries()
                .stream()
                .map(e -> new Map.Entry<V, K>() {
                    // This inner class should probably be abstracted out into its own top-level thing
                    @Override
                    public V getKey() {
                        return e.getValue();
                    }
    
                    @Override
                    public K getValue() {
                        return e.getKey();
                    }
    
                    @Override
                    public K setValue(K value) {
                        throw new UnsupportedOperationException();
                    }
                })
                .collect(new ImmutableSetMultimapCollector<>());
    }
    

    现在,Guava似乎还没有完全跟上Java8的速度,所以您需要编写自己的ImmutableSetMultimapCollector(或者您想要生成的任何输出类),但这会被反复使用,所以值得This article gives some guidance.

    还请注意,通过使用SetMultimap作为结果类型,我们可以在不丢失信息的情况下反转相同值映射到两个不同键的输入。这很可能是一个加号

    因此,我要在这里强调两个教训:

    • 番石榴是一个非常棒的图书馆。学习并使用它
    • 当您使用为问题定制的工具时,问题往往会变得简单得多。在这种情况下,Multimap就是这样一种工具
  3. # 3 楼答案

    为了简单起见,我们假设您希望将Map<Long, Set<String>>转换为Map<String, Long>,其中String值跨键唯一

    我认为这个操作是一个带有类型为Map<String, Long>的累加器的左折,在Java8中它变成了一个带有累加器组合器的约化:参见Javadoc,还有this related answer

    一种写作方式是这样的:

    public static void main(String[] args) {
        Map<Long, Set<String>> map = new HashMap<>();
        map.put(1L, new HashSet<String>());
        map.get(1L).add("a");
        map.get(1L).add("b");
        map.put(2L, new HashSet<>());
        map.get(2L).add("c");
        map.get(2L).add("d");
        map.get(2L).add("e");
        Map<String, Long> result = map.entrySet().stream().reduce(
                new HashMap<String, Long>(), 
                (accumulator, entry) -> {
                    // building an accumulator of type Map<String, Long> from a Map.Entry<Long, Set<String>>
                    entry.getValue().forEach(s -> accumulator.put(s, entry.getKey()));
                    return accumulator;
                }, 
                (accumulator1, accumulator2) -> {
                    // merging two accumulators of type Map<String, Long>
                    accumulator1.keySet().forEach(k -> accumulator2.put(k, accumulator1.get(k)));
                    return accumulator2;
                }
            );
        result.keySet().forEach(k -> System.out.println(k + " -> " + result.get(k)));
    }
    

    其输出如下:

    a -> 1
    b -> 1
    c -> 2
    d -> 2
    e -> 2
    

    注意:这与另一个answer中的想法相同,我以前没有注意到:)

  4. # 4 楼答案

    这是一个使用“reduce”的功能版本。在功能上这样做的主要缺点是,由于缺乏持久性数据结构,将导致性能不佳

    import java.util.*;
    import java.util.function.*;
    import java.util.stream.*;
    
    class Test {
    
        public static <K,V> Map<K,V> combineMaps(Map<K,V> map1, Map<K,V> map2) {
            Map<K,V> map = new HashMap<K,V>();
            map.putAll(map1);
            map.putAll(map2);
            return map;
        }
    
        public static BiFunction<Map<String,Integer>,Map.Entry<Integer,Set<String>>,Map<String,Integer>> accumulator =
            (map, entry) -> combineMaps(map, entry.getValue().stream().collect(Collectors.toMap(k -> k, k -> entry.getKey())));
    
        public static BinaryOperator<Map<String,Integer>> binOperator =
            (map1, map2) -> combineMaps(map1, map2);
    
        public static void main(String[] args) {
            Set<String> setOne = new HashSet<String>();
            setOne.add("one");
            setOne.add("two");
            setOne.add("three");
    
            Set<String> setTwo = new HashSet<String>();
            setTwo.add("four");
            setTwo.add("five");
            setTwo.add("six");
    
            Map<Integer,Set<String>> myMap = new HashMap<Integer, Set<String>>();
            myMap.put(1, setOne);
            myMap.put(2, setTwo);
    
            Map<String,Integer> newMap = myMap.entrySet().stream()
                .reduce(new HashMap<String,Integer>(), accumulator, binOperator);
    
            System.out.println(newMap.get("five"));
        }
    }