【问题标题】:Java Streams - Using a setter inside map()Java Streams - 在 map() 中使用 setter
【发布时间】:2019-10-22 13:13:47
【问题描述】:

我和同事讨论过,我们不应该在 stream.map() 中使用 setter,就像这里建议的解决方案 - https://stackoverflow.com/a/35377863/1552771

对此答案有一条评论不鼓励以这种方式使用map,但没有给出理由说明为什么这是一个坏主意。有人可以提供一个可能的情况,为什么这会中断?

我看到一些讨论,人们谈论通过添加或删除项目来同时修改集合本身,但是使用map 只是为数据对象设置一些值有什么负面影响吗?

【问题讨论】:

  • 我猜这取决于您设置的什么map 应该将流的元素转换为不同的元素。因此,如果您只是“滥用”它来执行forEach 并继续流(太糟糕了 forEach 不返回流),那么我认为这只是“糟糕的风格”而不是“危险的”。就像设置 question.setAnswered(true) 时设置器执行其他操作一样 setAnswer(boolean b){ deppThought.meditateOverMeaningOfLive();} .. setter 用于设置,map() 用于映射..
  • 其中一个问题是,如果整个 Stream 管道决定不必执行它来计算最终结果,甚至可能不会执行 map 操作。我使用 Java 11 遇到过它——我不确定它是否适用于早期版本——尽管没有这样的优化,但对于 Java 8。
  • 我忘记了自己,但我有一种预感,除了@michalk 指定的问题之外,还有this 的原因。
  • @Avi 对原始问题的讨论提到修改对象不是干扰,因为它们不会修改数据 source (无论如何只包含引用)。跨度>
  • @michalk 在count() 的情况下,从Java 9 开始。在短路方法的情况下,如…Matchfind…,总是map 不是对所有元素执行。但是你也不能假设它只适用于必要的最小数量的元素,所以它只是不可预测的。 this answer 的大部分 peek 也适用于 map 中的副作用,仅在调整操作目的时(在需要时提供值)。

标签: java collections java-8 java-stream setter


【解决方案1】:

map 中使用副作用(例如调用setter)与将peek 用于非调试目的有很多相似之处,这已在In Java streams is peek really only for debugging? 中讨论过

This answer 有一个非常好的一般建议:

不要以非预期的方式使用 API,即使它实现了您的近期目标。这种方法将来可能会失效,而且未来的维护者也不清楚。

the other answer 命名相关的实际问题;我必须引用自己:

您必须了解的重要一点是,流是由终端操作驱动的。终端操作决定是所有元素都必须被处理还是根本不需要。

当您将具有副作用的操作放入 map 函数中时,您对将执行哪些元素甚至可能如何执行它有一个特定的期望,例如按哪个顺序。期望是否会实现,取决于其他后续 Stream 操作,甚至可能取决于细微的实现细节。

举几个例子:

IntStream.range(0, 10) // outcome changes with Java 9
    .mapToObj(i -> System.out.append("side effect on "+i+"\n"))
    .count();
IntStream.range(0, 2) // outcome changes with Java 10 (or 8u222)
    .flatMap(i -> IntStream.range(i * 5, (i+1) * 5 ))
    .map(i -> { System.out.println("side effect on "+i); return i; })
    .anyMatch(i -> i > 3);
IntStream.range(0, 10) // outcome may change with every run
    .parallel()
    .map(i -> { System.out.println("side effect on "+i); return i; })
    .anyMatch(i -> i > 6);

此外,正如链接答案中已经提到的,即使您有一个处理所有元素并已排序的终端操作,也无法保证中间操作的处理顺序(或并行流的并发性)。

当你有一个没有重复的流和一个处理所有元素的终端操作和一个只调用一个微不足道的 setter 的 map 函数时,代码可能会做你想要的事情,但是代码对微妙的依赖关系非常多这将成为维修的噩梦。这让我们回到了关于以非预期方式使用 API 的第一句话。

【讨论】:

    【解决方案2】:

    我认为这里真正的问题是它只是一种不好的做法,并且违反了该功能的预期用途。例如,也可以使用filter 完成同样的事情。这扭曲了它的使用,也使代码混乱,或者充其量是不必要的冗长。

       public static void main(String[] args) {
          List<MyNumb> foo =
                IntStream.range(1, 11).mapToObj(MyNumb::new).collect(
                      Collectors.toList());
          System.out.println(foo);
          foo = foo.stream().filter(i ->
          {
             i.value *= 10;
             return true;
          }).collect(Collectors.toList());
          System.out.println(foo);
    
       }
    
    
        class MyNumb {
           int value;
    
           public MyNumb(int v) {
              value = v;
           }
           public String toString() {
              return Integer.toString(value);
           }
        }
    

    所以回到原来的例子。根本不需要使用地图,导致以下相当丑陋的混乱。

    foos = foos.stream()
                .filter(foo -> { boolean b = foo.isBlue();
                                 if (b) {
                                    foo.setTitle("Some value");
                                 }
                                 return b;})
    
                .collect(Collectors.toList()); 
    
    

    【讨论】:

      【解决方案3】:

      Streams 不仅仅是一组新的 API,它们让您的事情变得更轻松。它还带来了函数式编程范式。

      而且,函数式编程范式最重要的方面是使用纯函数进行计算。纯函数是输出仅且仅取决于其输入的函数。 所以,Streams API 基本上应该使用无状态、无副作用的纯函数。

      引用 Joshua Bloch 的 Effective Java(第 3 版)中的内容

      如果您是流媒体新手,可能很难掌握其中的窍门。仅仅将您的计算表达为流管道可能很困难。当您成功时,您的程序将运行,但您可能几乎没有意识到任何好处。 Streams 不仅仅是一个 API,它是一个基于函数式编程的范式。为了获得流必须提供的表现力、速度以及在某些情况下的可并行性,您必须采用范式以及 API。流范式最重要的部分是将计算构建为一系列转换,其中每个阶段的结果尽可能接近前一阶段结果的纯函数。纯函数是其结果仅取决于其输入的函数:它不依赖于任何可变状态,也不更新任何状态。为了实现这一点,传递给流操作的任何函数对象,无论是中间的还是终端的,都应该没有副作用。 有时,您可能会看到类似 sn-p 的流代码,它在文本文件中构建单词的频率表:

      // Uses the streams API but not the paradigm--Don't do this!
      
      Map<String, Long> freq = new HashMap<>();
      
      try (Stream<String> words = new Scanner(file).tokens()) {
              words.forEach(word -> { freq.merge(word.toLowerCase(), 1L, Long::sum);
          });
      }
      

      这段代码有什么问题?毕竟,它使用了流、lambdas 和方法引用,并且得到了正确的答案。简而言之,它根本不是流代码;它是伪装成流代码的迭代代码。它没有从流 API 中获得任何好处,而且它(有点)比相应的迭代代码更长、更难阅读、更难维护。问题源于这样一个事实,即该代码在终端 forEach 操作中完成所有工作,使用一个 lambda 来改变外部状态(频率表)。一个 forEach 操作除了呈现流执行的计算结果之外做任何事情都是“代码中的臭味”,就像一个改变状态的 lambda 一样。那么这段代码应该怎么看呢?

      // Proper use of streams to initialize a frequency table
      
      Map<String, Long> freq;
      
      try (Stream<String> words = new Scanner(file).tokens()) {
          freq = words
          .collect(groupingBy(String::toLowerCase, counting()));
      }
      

      【讨论】:

        【解决方案4】:

        仅举几例:

        • map() with setter 是 interfering(它修改初始数据),而规范需要 non-interfering 函数。更多详情请阅读this post
        • map() with setter 是 stateful (您的逻辑可能取决于您正在更新的字段的初始值),而规范需要 stateless 函数
        • 即使您没有干扰您正在迭代的集合,setter 的 side effect 也是不必要的
        • map 中的设置器可能会误导未来的代码维护者
        • 等等……

        【讨论】:

          猜你喜欢
          • 2018-03-24
          • 1970-01-01
          • 2015-06-19
          • 2020-03-27
          • 2016-10-31
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-09-16
          相关资源
          最近更新 更多