【问题标题】:Java streams non-interference and side-effectsJava 流无干扰和副作用
【发布时间】:2018-10-23 03:29:23
【问题描述】:

查看 Java java.util.stream 包文档后,人们对在流使用中遵循的最佳实践产生了疑问。 考虑到这段代码:

HashMap<Integer,Integer> map = new HashMap<>();
map.put(1,1);
map.put(2,2);
map.put(3,3);
map.put(4,4);
map.keySet().parallelStream().forEach(key -> {
        if (key == 3) { 
            map.put(3,0);
        }
});
  1. 代码输出是否总是等于 ([1,1],[2,2],[3,0],[4,4])?
  2. 可以将 map.put(3,0) 视为非干扰操作吗?
  3. 可以将 map.put(3,0) 视为可接受的副作用吗?

换句话说,上述代码是否符合流文档中建议的最佳实践?

【问题讨论】:

  • 如果可以多次破坏代码,那么您就做到了。您通过修改流的源来违反合同,并且通过从任意未知线程修改不是线程安全的HashMap 来违反合同。此外,您期望输出为[1,1],[2,2],[3,0],[4,4],尽管HashMap 不保证任何订单,因此即使没有其他合同违规,您也不能认为结果是理所当然的。最后,您要问的是,是否与记录的约束相反“可以被认为符合最佳实践”……

标签: java java-stream


【解决方案1】:

你的例子肯定违反了non interference requirement

对于大多数数据源而言,防止干扰意味着确保在流管道执行期间根本不修改数据源。值得注意的例外是其源是并发集合的流,这些流是专门为处理并发修改而设计的。

您的数据源 HashMap 并非旨在处理并发修改,因此在流管道执行期间根本不应修改它。

因此,您的第二个和第三个问题的答案是否定的。

至于第一个问题,您的特定代码可能仍会产生预期的结果,因为您的条件确保只有一个线程会调用map.put(3,0)。但是,这仍然被认为是 Streams 的错误用法。

【讨论】:

  • 这仍然是偶然得到的结果。只有一个线程在修改地图,但它在其他线程正在读取/迭代它的同时进行。可能的结果集仍然更大,包括获取异常或无限循环。这在很大程度上取决于HashMap 的实现细节,例如这四个条目在具有默认容量的HashMap 中的分布不是非常友好的并行。此外,必须强调的是,期望 HashMap 的特定输出是错误的,即使在顺序情况下也是如此,因为不能保证条目的顺序。
【解决方案2】:

不,不,不。
避免副作用。
符合文档的示例代码:

Map<Integer,Integer> updatedMap = map.keySet().parallelStream()
        .filter(e -> e == 3)
        .collect(Collectors.toMap(Function.identity(), e -> 0));
map.putAll(updatedMap);

【讨论】:

    【解决方案3】:

    比较(一)

    map.keySet().parallelStream().forEach(key -> {
            if (key == 3) { 
                map.put(3, 0);
            }
    });
    

    (添加了一个新条目)

    与(b)

    map.entrySet().parallelStream().forEach(e -> {
            if (e.getKey() == 3) { 
                e.setValue(0);
            }
    });
    

    (没有创建、移动 Entry 对象。但请注意LinkedHashMap。)

    • (a) 不安全
    • (b) 安全

      1. 代码输出是否总是等于([1,1],[2,2],[3,0],[4,4])?

        (a) 否 (b) 是

      2. map.put(3, 0) 是否可以视为非干扰操作?

        (a) 否 (b) setValue(0)

      3. map.put(3, 0) 可以被视为可接受的副作用吗?

        (a) 否 (b) setValue(0)

    所以(a)是邪恶的,(b)是好的。

    为什么要提到 entrySet.setValue?

    实际上 Oracle 实现中的HashMap.put 可能与Entry.setValue 的作用相同。那将需要使用实现知识 - 丑陋。

    而 Entry.setValue 是基于原始映射的支持,人们可能会推断出只有值字段被覆盖。请注意,LinkedHashMap 需要重新排序条目,而重新排序又是不安全的。

    【讨论】:

    • 请注意 LinkedHashMap 默认情况下不会对 Entry.setValue 重新排序,只有在构造函数中指定了访问重新排序时。除此之外,没有为HashMap 指定线程安全保证,即使您只使用Entry.setValue
    • 感谢 LinkedHashMap。关于 HashMap pure:除了对 value 字段的并发访问之外,没有理由假设任何其他问题:对于 setValue,不关心映射,也不移动条目。作为可能的基类,它不能保证任何事情。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-10-15
    • 1970-01-01
    • 1970-01-01
    • 2013-05-09
    • 2011-03-11
    • 2020-05-22
    • 1970-01-01
    相关资源
    最近更新 更多