有没有一种优雅的方式将一个map流缩减为一个map?

14

给定一些地图,是否有一种方法将它们所有的条目放入一个地图中?

假设不考虑空值、覆盖条目等问题,我想编写的代码如下:

public static <K, V> Map<K, V> reduce(Map<K, V>... maps) {
    return Arrays.stream(maps)
        .reduce(new HashMap<K, V>(), (a, b) -> a.putAll(b));
}

但是这样会产生编译错误,因为a.putAll(b)的返回类型是void。如果它返回this,就可以解决问题。

为了解决这个问题,我编写了以下代码:

public static <K, V> Map<K, V> reduce(Map<K, V>... maps) {
    return Arrays.stream(maps)
        .reduce(new HashMap<K, V>(), (a, b) -> {a.putAll(b); return a;});
}

这段代码可以编译并运行,但其中的lambda表达式不够简洁;在编写return a;时感觉有些重复。

一种方法是重构出一个实用方法:

public static <K, V> Map<K, V> reduce(Map<K, V> a, Map<K, V> b) {
    a.putAll(b);
    return a;
}

清理Lambda函数:

public static <K, V> Map<K, V> reduce(Map<K, V>... maps) {
    return Arrays.stream(maps)
       .reduce(new HashMap<K, V>(), (a, b) -> reduce(a, b));
}

但现在我有一个虽然可重用,但有点无用的实用方法。

是否有更优雅的方法在lambda表达式中调用累加器上的方法并返回它?


5
很遗憾,使用 reduce 的这种方式基本上是有问题的:在序列执行时可能有效,但在并行执行时会出错。这是因为 reduce() 方法将其第一个(身份)参数视为可重复使用的值。因此它适用于原始类型和不可变类型,但是对于可变值(如 new HashMap<>())在并行流中将被不同的线程同时进行修改,这可能导致出现问题。解决方法是使用 collect(),就像 @Pshemo 的答案中所示。 - Maurice Naftalin
1个回答

19

reduce 的工作方式与

U result = identity;
for (T element : this stream)
    result = accumulator.apply(result, element)
return result;

这意味着代表累加器应用的 lambda 需要返回结果(最终或中间结果)。如果您想避免这种行为,请使用 collect,它的工作方式更像。
R result = supplier.get();
for (T element : this stream)
    accumulator.accept(result, element);
return result;

因此,代表accumulator.accept的lambda不需要返回任何值,而是基于element修改result

例如:

public static <K, V> Map<K, V> reduce(Map<K, V>... maps) {
    return Arrays.stream(maps)
            .collect(HashMap::new, Map::putAll, Map::putAll);
            //                          ^            ^
            //                          |         collect results from parallel streams
            //                       collect results in single thread
}

1
@Bohemian,我对实现不是特别熟悉,但“reduce”应该是可并行化的。它明确要求是可结合的,因此实现可以执行(a * b) * (c * d)而不是(((a * b) * c) * d)(其中一个线程获取ab,另一个线程获取cd)。 - Tom Hawtin - tackline
1
对于关联运算符,表达式内操作的顺序很重要,而不是时间上的先后。在我上面的例子中,如果 * 表示字符串连接,无论你怎么做都可以正常工作。reduce 不适用于普通的非交换哈希函数(尽管可以用于 Set.hashCode 的实现)。foldLeft 是合适的,但出于某种原因还没有(至少目前为止)。 (来自 Stuart Marks 自己的话:https://dev59.com/rWAf5IYBdhLWcg3wwk4V#24316429) - Tom Hawtin - tackline
1
@Bohemian:当然,你必须指定一个无副作用的[可结合](http://en.wikipedia.org/wiki/Associative_property)函数来进行`reduce`,以便获得与并行缩减一致的结果。如果符合条件,就没有问题了。令你惊讶的是:*它可以*使用字符串连接,因为连接是可结合的,即如果`a`,`b`和`c`都是`String`,则`(a+b)+c`与`a+(b+c)`具有相同的结果。这意味着`a+b+c+d`也与`(a+b)+(c+d)`具有相同的结果,因此可以同时评估`(a+b)`和`(c+d)`。 - Holger
2
@Bohemian:缩减操作是完全可并行化的,只要你的缩减函数是可结合的。reducecollect之间的区别在于,reduce通过消耗并生成新的(例如求和、最大值、最小值)来工作,而collect则通过将元素合并到可变的结果容器(列表、集合、映射、数组等)中来工作。 - Brian Goetz
1
如果两个或多个映射具有重复的键,会发生什么? - Nitin9791
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接