Java8:HashMap<X, Y> 到 HashMap<X, Z> 使用 Stream / Map-Reduce / Collector

Posted

技术标签:

【中文标题】Java8:HashMap<X, Y> 到 HashMap<X, Z> 使用 Stream / Map-Reduce / Collector【英文标题】:Java8: HashMap<X, Y> to HashMap<X, Z> using Stream / Map-Reduce / Collector 【发布时间】:2014-11-12 05:33:27 【问题描述】:

我知道如何从Y“转换”一个简单的Java List -> Z,即:

List<String> x;
List<Integer> y = x.stream()
        .map(s -> Integer.parseInt(s))
        .collect(Collectors.toList());

现在我想对地图做基本相同的事情,即:

INPUT:

  "key1" -> "41",    // "41" and "42"
  "key2" -> "42"      // are Strings


OUTPUT:

  "key1" -> 41,      // 41 and 42
  "key2" -> 42       // are Integers

解决方案不应仅限于String -> Integer。就像上面的 List 示例一样,我想调用任何方法(或构造函数)。

【问题讨论】:

【参考方案1】:
Map<String, String> x;
Map<String, Integer> y =
    x.entrySet().stream()
        .collect(Collectors.toMap(
            e -> e.getKey(),
            e -> Integer.parseInt(e.getValue())
        ));

它不如列表代码那么好。您不能在map() 调用中构造新的Map.Entrys,因此该工作将混入collect() 调用中。

【讨论】:

您可以将e -&gt; e.getKey() 替换为Map.Entry::getKey。但这是品味/编程风格的问题。 其实这是性能问题,你的建议略优于 lambda 'style'【参考方案2】:

以下是Sotirios Delimanolis' answer 的一些变体,从 (+1) 开始非常好。考虑以下几点:

static <X, Y, Z> Map<X, Z> transform(Map<? extends X, ? extends Y> input,
                                     Function<Y, Z> function) 
    return input.keySet().stream()
        .collect(Collectors.toMap(Function.identity(),
                                  key -> function.apply(input.get(key))));

这里有几点。首先是泛型中通配符的使用;这使得函数更加灵活。例如,如果您希望输出映射的键是输入映射键的超类,则需要使用通配符:

Map<String, String> input = new HashMap<String, String>();
input.put("string1", "42");
input.put("string2", "41");
Map<CharSequence, Integer> output = transform(input, Integer::parseInt);

(还有一个关于地图值的示例,但它确实是人为的,我承认为 Y 使用有界通配符只会在边缘情况下有所帮助。)

第二点是,我没有在输入映射的entrySet 上运行流,而是在keySet 上运行它。我认为,这使得代码更简洁,代价是必须从映射中获取值,而不是从映射条目中获取值。顺便说一句,我最初将key -&gt; key 作为toMap() 的第一个参数,但由于某种原因导致类型推断错误而失败。将其更改为 (X key) -&gt; key 有效,Function.identity() 也是如此。

还有一个变种如下:

static <X, Y, Z> Map<X, Z> transform1(Map<? extends X, ? extends Y> input,
                                      Function<Y, Z> function) 
    Map<X, Z> result = new HashMap<>();
    input.forEach((k, v) -> result.put(k, function.apply(v)));
    return result;

这使用Map.forEach() 而不是流。我认为这更简单,因为它省去了收集器,这些收集器在地图上使用起来有些笨拙。原因是Map.forEach() 将键和值作为单独的参数提供,而流只有一个值——您必须选择是使用键还是映射条目作为该值。不利的一面是,这缺乏其他方法的丰富、流畅的优点。 :-)

【讨论】:

Function.identity() 可能看起来很酷,但由于第一个解决方案需要对每个条目进行映射/哈希查找,而所有其他解决方案都不需要,所以我不推荐它。【参考方案3】:

这样的通用解决方案

public static <X, Y, Z> Map<X, Z> transform(Map<X, Y> input,
        Function<Y, Z> function) 
    return input
            .entrySet()
            .stream()
            .collect(
                    Collectors.toMap((entry) -> entry.getKey(),
                            (entry) -> function.apply(entry.getValue())));

例子

Map<String, String> input = new HashMap<String, String>();
input.put("string1", "42");
input.put("string2", "41");
Map<String, Integer> output = transform(input,
            (val) -> Integer.parseInt(val));

【讨论】:

【参考方案4】:

Guava 的函数 Maps.transformValues 正是您要找的,它与 lambda 表达式配合得很好:

Maps.transformValues(originalMap, val -> ...)

【讨论】:

我喜欢这种方法,但请注意不要将 java.util.Function 传递给它。由于它需要 com.google.common.base.Function,Eclipse 给出了一个无用的错误 - 它说 Function 不适用于 Function,这可能会令人困惑:“方法 transformValues(Map, Function super V1 Maps 类型中的 ,V2>) 不适用于参数 (Map, Function)" 如果你必须传递一个java.util.Function,你有两个选择。 1. 通过使用 lambda 让 Java 类型推断来避免该问题。 2. 使用 javaFunction::apply 之类的方法引用来生成类型推断可以计算出的新 lambda。 另请注意,与此页面上的其他解决方案不同,此解决方案将视图返回到底层地图,而不是副本。【参考方案5】:

它是否必须 100% 实用且流畅?如果没有,这个怎么样,尽可能短:

Map<String, Integer> output = new HashMap<>();
input.forEach((k, v) -> output.put(k, Integer.valueOf(v));

(if you can live with the shame and guilt of combining streams with side-effects)

【讨论】:

【参考方案6】:

我的增强标准流 API 的 StreamEx 库提供了一个更适合转换地图的 EntryStream 类:

Map<String, Integer> output = EntryStream.of(input).mapValues(Integer::valueOf).toMap();

【讨论】:

【参考方案7】:

为了学习目的而始终存在的替代方法是通过 Collector.of() 构建您的自定义收集器,尽管这里的 toMap() JDK 收集器很简洁 (+1 here)。

Map<String,Integer> newMap = givenMap.
                entrySet().
                stream().collect(Collector.of
               ( ()-> new HashMap<String,Integer>(),
                       (mutableMap,entryItem)-> mutableMap.put(entryItem.getKey(),Integer.parseInt(entryItem.getValue())),
                       (map1,map2)-> map1.putAll(map2); return map1;
               ));

【讨论】:

我从这个自定义收集器作为基础开始并想补充一点,至少在使用 parallelStream() 而不是 stream() 时,binaryOperator 应该重写为更类似于 map2.entrySet().forEach(entry -&gt; if (map1.containsKey(entry.getKey())) map1.get(entry.getKey()).merge(entry.getValue()); else map1.put(entry.getKey(),entry.getValue()); ); return map1 或减少时会丢失值。【参考方案8】:

如果您不介意使用 3rd 方库,我的 cyclops-react 库具有所有 JDK Collection 类型的扩展,包括 Map。我们可以直接使用 'map' 运算符转换地图(默认情况下,地图作用于地图中的值)。

   MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                                .map(Integer::parseInt);

bimap 可用于同时转换键和值

  MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                               .bimap(this::newKey,Integer::parseInt);

【讨论】:

【参考方案9】:

声明性和更简单的解决方案是:

map.replaceAll((key, val) -> getUpdatedListFor(key, val));

yourMutableMap.replaceAll((key, val) return_value_of_bi_your_function); NB。请注意您正在修改地图状态。所以这可能不是你想要的。

干杯: http://www.deadcoderising.com/2017-02-14-java-8-declarative-ways-of-modifying-a-map-using-compute-merge-and-replace/

【讨论】:

以上是关于Java8:HashMap<X, Y> 到 HashMap<X, Z> 使用 Stream / Map-Reduce / Collector的主要内容,如果未能解决你的问题,请参考以下文章

使用Java8 Stream API合并两个hashmap列表

基于Java8初探HashMap

Java8里的HashMap对象lambda遍历方法

Java8 HashMap ,hashTable,TreeMap 看源代码看到的一些特性

Java8函数式编程

Java8中HashMap扩容算法小计