功能在Java 8中反转具有嵌套数据结构的映射

时间:2016-04-22 14:01:19

标签: java data-structures functional-programming

我遇到了一个让我疯狂的问题。我试图避免为这个地图反演创建一个中间对象。 (对目标的肯定:我有一个嵌套数据结构的地图,我想反转和爆炸。所以,

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)));

这就像一个魅力,但我(为了我自己的启发)喜欢避免创建中间对。我知道有必要这样做,但我似乎迷失在synatax中。

4 个答案:

答案 0 :(得分:2)

以下是一种在条目集上使用自定义Stream#collect的方法。有人可能会说,这不是“完全正常运转”。由于隐藏在累加器中的forEach,但在某些时候,必须创建地图条目,并且我不确定是否存在&#34;优雅&#34;使用来自Set的流(条目值)的方法,仍然可以访问条目密钥(它将成为新条目的值)。

旁注(虽然我通过接受程序编程的方法冒险投票):你不能

当你说你在语法&#34;中丢失了,那么

  1. 几个星期后再次阅读此代码后您会怎么想?
  2. 第一次阅读此代码时,您的同事会怎么想? (我关注那个带电锯和守门员面具的人......)
  3. 我建议保持简单。 (尽管最通用的程序形式在第一眼看上去仍然令人困惑)

    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;
        }
    }
    

答案 1 :(得分:2)

这是使用'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"));
    }
}

答案 2 :(得分:0)

我会"cut the knot"在这里,可以这么说,并改变问题的条款。谷歌的优秀Guava library有一个Multimap interface和一个SetMultimap子类型,其中包含一些实现。 Multimap的文档告诉我们:

  

您可以将多图的内容可视化为从键到非空的值集合的映射:

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

...或作为单个“扁平化”的键值对集合:

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

SetMultimap类型的entries 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似乎没有达到Java 8的速度,因此您需要编写自己的ImmutableSetMultimapCollector(或者您想要生成的任何输出类),但这样可以重复使用这是值得的。 This article gives some guidance.

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

所以我在这里强调两个教训:

  • Guava是一个非常棒的图书馆。学习并使用它!
  • 当您使用为其定制设计的工具时,问题通常会变得更加简单。在这种情况下,Multimap就是这样一种工具。

答案 3 :(得分:0)

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

我认为这个操作是一个带有Map<String, Long>类型累加器的 fold left ,它在Java 8中变为带有累加器合并器:请参阅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中的想法相同,我之前没有注意到:)