【问题标题】:When should I use streams?我什么时候应该使用流?
【发布时间】:2017-07-18 02:33:11
【问题描述】:

我在使用List 及其stream() 方法时遇到了一个问题。虽然我知道如何使用它们,但我不太确定何时使用它们。

例如,我有一个列表,其中包含到不同位置的各种路径。现在,我想检查一个给定的路径是否包含列表中指定的任何路径。我想根据条件是否满足返回boolean

当然,这本身并不是一项艰巨的任务。但我想知道我应该使用流还是 for(-each) 循环。

名单

private static final List<String> EXCLUDE_PATHS = Arrays.asList(
    "my/path/one",
    "my/path/two"
);

使用流的示例:

private boolean isExcluded(String path) {
    return EXCLUDE_PATHS.stream()
                        .map(String::toLowerCase)
                        .filter(path::contains)
                        .collect(Collectors.toList())
                        .size() > 0;
}

使用 for-each 循环的示例:

private boolean isExcluded(String path){
    for (String excludePath : EXCLUDE_PATHS) {
        if (path.contains(excludePath.toLowerCase())) {
            return true;
        }
    }
    return false;
}

注意path 参数总是小写

我的第一个猜测是 for-each 方法更快,因为如果满足条件,循环将立即返回。而流仍会遍历所有列表条目以完成过滤。

我的假设正确吗?如果是这样,那么为什么(或者更确切地说何时)我会使用stream() 吗?

【问题讨论】:

  • 流比传统的 for 循环更具表现力和可读性。在后面你需要小心 if-then 和条件等的内在函数。流表达式非常清楚:将文件名转换为小写,然后通过某些东西过滤,然后计数,收集等结果:非常迭代计算流的表达。
  • 这里不需要new String[]{…}。只需使用Arrays.asList("my/path/one", "my/path/two")
  • 如果您的来源是String[],则无需调用Arrays.asList。您可以使用Arrays.stream(array) 流式传输阵列。顺便说一句,我很难完全理解isExcluded 测试的目的。 EXCLUDE_PATHS 的元素是否真的包含在路径中的某处是否真的很有趣? IE。 isExcluded("my/path/one/foo/bar/baz") 将返回 true,以及 isExcluded("foo/bar/baz/my/path/one/")...
  • 太好了,我不知道Arrays.stream 方法,感谢您指出这一点。确实,我发布的示例对除我之外的其他人似乎毫无用处。我知道isExcluded 方法的行为,但这实际上只是我自己需要的东西,因此,回答你的问题:是的,这很有趣,因为我不想提及,因为它不适合原始问题的范围。
  • 在我们的团队中,我们发现流的使用导致比 for 循环等更多的问题和错误。这是因为并非所有团队都是流专家,而且流代码更神秘 - 你不能如果您不是流专家,请猜猜发生了什么,但任何人都可以阅读 for 循环和 if 语句。因此,在我们的团队中,我们更喜欢更长的显式代码,而不是具有很多功能但没有人真正知道它是否/如何工作以及是否存在性能问题的单行代码。其他团队可能更喜欢简洁的代码。

标签: java java-8 java-stream


【解决方案1】:

激进的回答:

从不。曾经。 曾经。

我几乎从不迭代任何东西的列表,尤其是为了找到东西,但 Stream 用户和系统似乎充满了这种编码方式。

很难重构和组织这样的代码,因此冗余和过度迭代无处不在。在相同的方法中,您可能会看到 5 次。相同的列表,发现不同的东西。

它也不是真的更短。很少是。绝对不是更具可读性,但这是一种主观意见。有人会说是。我不。人们可能会因为自动完成而喜欢它,但在我的编辑器 Intellij 中,我可以只使用 iteritar 并使用类型和所有内容为我自动创建 for 循环。

误用和过度使用,最好完全避免。 Java 不是一种真正的函数式语言,Java 泛型很烂,表达力不够,当然更难阅读、解析和重构。

流代码不容易提取或重构,除非您想开始添加返回 OptionalsPredicatesConsumers 的奇怪方法,否则您最终会得到方法返回并采用各种奇怪的通用约束有命令和意义只有上帝知道。很多推断,您需要访问方法来找出各种事物的类型。

试图让 Java 表现得像 HaskellLisp 这样的函数式语言是愚蠢的。一个基于 Streams 的重型 Java 系统总是比没有系统更复杂,而且性能更差,重构和维护也更复杂。因此也有更多的错误并充满了补丁工作。到处都是胶水。

当 OpenJDK 参与进来时,他们开始在语言中添加一些东西,而没有真正彻底地考虑它。它不仅仅是 Java Streams。因此,这样的系统本质上更复杂,因为它们需要更多的基础知识。你可能有,但你的同事没有。他们肯定知道什么是 for 循环和什么是 if 块。

此外,由于您也不能将任何内容分配给非 final 变量,因此您很少在循环时同时做两件事,因此您最终会迭代两次或三次。

大多数喜欢和更喜欢 Streams 方法而不是 for 循环的人是在 Java 8 之后开始学习 Java 的人。那些以前讨厌它的人。问题是它的使用要复杂得多,重构要复杂得多,而且更难以正确的方式使用。

当我说它的性能更差时,它并不是与 for 循环相比,这也是一个非常真实的事情,但更多的是由于此类代码必须过度迭代大量事物的趋势。

我们认为迭代列表很容易找到它往往会一遍又一遍地完成的项目。

我还没有看到一个系统从中受益。我见过的所有系统都执行得很糟糕,主要是因为它。

代码绝对不比 for 循环更具可读性,而且在 for 循环中绝对更灵活和可重构。我们今天到处看到如此复杂的垃圾系统和错误的原因是我向你保证,由于严重依赖 Streams 进行过滤,更不用说过度使用 Lombok 和 Jackson。这三个是执行不力的系统的标志。关键字过度使用。一种补丁工作方法。

再次,我认为迭代列表以查找任何内容非常糟糕。然而,对于基于 Stream 的系统,这就是人们一直在做的事情。解析和检测一个迭代可能是 O(N2) 的情况也并不罕见,也很困难,而使用 for 循环你会立即看到它。

通常习惯要求数据库为您过滤事物 基本查询会返回一大堆事物并扩展各种迭代事物和方法以涵盖用例以过滤掉不受欢迎的事物并不少见,当然他们使用 Streams 来做到这一点。各种各样的方法出现在这个大列表周围,需要过滤各种东西。

一遍又一遍。当然,我不是指你。你的同事。对吧?

我几乎从不迭代任何东西。我使用正确的数据集并依靠数据库为我过滤它。 一次。然而,在 Streams 繁重的系统中,您会随处看到这一点。在最深的方法中,在调用者中,调用者的调用者,调用者的调用者的调用者。处处流淌。这是丑陋的。祝你好运重构那些存在于小 lambda 中的代码。

祝你好运重用它们。没有人会想重用你的好谓词。如果他们想使用它们,你猜怎么着。他们需要使用更多的 Streams。你只是让自己上瘾并进一步走投无路。现在,您是否建议我开始将所有代码拆分为微小的谓词、消费者、函数和 BiFuntions?只是为了让我可以为 Streams 重用该逻辑?

当然,我也讨厌 Javascript 中的它,过度迭代无处不在。

您可能会说迭代列表的成本并不高,但系统复杂性增加,冗余增加,因此维护成本和错误数量增加。它变成了一种基于补丁和胶水的方法来处理各种事情。只需添加另一个过滤器并删除它,而不是以正确的方式编写代码。

此外,您需要三台服务器来托管所有用户,而我只需一台即可。因此,这种系统所需的可扩展性将比非流重系统更早地需要。对于小型项目,这是一个非常重要的指标。如果你可以有 5000 个并发用户,我的系统可以处理两倍或三倍。

我的代码中不需要它,当我负责新项目时,第一条规则是完全禁止使用 Streams。

这并不是说它没有用例,或者它有时可能有用,但与它相关的风险远大于好处。

当您开始使用 Streams 时,您实际上是在采用一种全新的新的编程范式系统的整个编程风格将会改变,这就是我关心的

你不想要那种风格。它并不优于旧样式。尤其是在 Java 上。

使用 Futures API。当然,您可以开始编写所有代码以返回 Promise 或 Future,但您真的想要吗?这能解决任何问题吗?您的整个系统真的可以随处跟进吗?它对您会更好,还是您只是在尝试并希望在某个时候受益?

有些人在 JavaScript 中过度使用 JavaRx 和过度承诺。当您真正想要基于未来的事物时,确实很少有案例,并且会感觉到很多极端案例,您会发现这些 API 具有某些限制并且您刚刚成功。

您可以构建非常复杂且可维护性要高得多的系统,而无需任何废话。这就是它的意义所在。这与您的爱好项目扩展并成为可怕的代码库无关。

这是关于构建大型复杂企业系统并确保它们保持连贯、一致的可重构和易于维护的最佳方法。

此外,您很少独自开发此类系统。您很可能与至少 10 人以上的人一起工作,他们都在试验和过度使用 Streams。因此,虽然您可能知道如何正确使用它们,但您可以放心,其他 9 人不会。

我将为您提供这些精彩的真实代码示例,其中有数千个更像它们:

或者这个:

或者这个:

或者这个:

尝试重构上述内容。我挑战你。试一试。一切都是流,无处不在。这就是 Stream 开发人员所做的,他们做得太过分了,而且没有简单的方法来掌握代码实际在做什么。这个方法返回了什么,这个转换在做什么,我最终得到了什么。一切都是推断出来的。确实更难阅读。

如果你明白这一点,那么你一定是爱因斯坦,但你应该知道不是每个人都和你一样,在不久的将来这可能是你的系统。

请注意,这并不孤立于这个项目,但我已经看到其中许多与这些结构非常相似。

有一点是肯定的,可怕的程序员喜欢流。

【讨论】:

  • “有一点是肯定的,可怕的程序员喜欢流。”。请提供证据。我会认为“可怕的编码员”甚至不会理解他们。或者你认为你的断言也证明了相反的情况?它没有。很难说你在这里提出了一个连贯的案例。我们知道您不喜欢流,但我的经验是,与更一般的编码不同,它们倾向于第一次工作,而“可怕的编码人员”不理解它们是一种奖励。您的示例并不明显“可怕”,您在这里批评工作代码是一个不错的选择。
  • 我并不是说所有 Stream 编码器都是可怕的编码器。我是说可怕的程序员爱他们。如果它们如您所说更难使用,那么您认为他们不会尝试使用它们的原因是什么?此外,通过您的相同陈述,您肯定它们使用起来更复杂,所以我认为您只是提供了自己的论据来反对它们。为什么要使您的代码库复杂化,而不是保持其灵活、简单和轻便?还有,证据?我附上的代码片段,看起来像一个好的编码员写的吗?还是可怕的?也许这段代码对你来说就像诗一样?
  • 如果对你来说这段代码不是“不言而喻的可怕”,那么我认为我们无法就任何事情达成一致。但是你的评论被注意到了。
  • 为什么用图片代替纯文本代码?我们有一条规则,代码永远不应该作为图像发布,因为:它不能被复制/粘贴,无法通过搜索找到,依赖屏幕阅读器的人无法访问它,并且图像在某些地方被禁用(例如,我不会'无法在工作中看到您的代码,因为我的雇主阻止了来自多个地方的图像,包括 Stack Overflow)。应该回滚这些更改。
【解决方案2】:

你的假设是正确的。您的流实现比 for 循环慢。

这种流的使用应该和 for 循环一样快:

EXCLUDE_PATHS.stream()  
    .map(String::toLowerCase)
    .anyMatch(path::contains);

这将遍历项目,将String::toLowerCase 和过滤器逐个应用于项目并终止于第一个匹配的项目

collect() & anyMatch() 都是终端操作。 anyMatch() 在找到第一个项目时退出,而 collect() 要求处理所有项目。

【讨论】:

  • 太棒了,不知道 findFirst()filter() 的组合。显然,我像我想象的那样知道如何使用流。
  • 网络上有一些关于流 API 性能的非常有趣的博客文章和演示文稿,我发现它们对于理解这些东西在后台是如何工作的非常有帮助。如果您对此感兴趣,我绝对可以建议您研究一下。
  • 在您编辑后,我觉得您的答案是应该被接受的,因为您也在另一个答案的 cmets 中回答了我的问题。不过,我想感谢 @rvit34 发布代码:-)
  • 正如您所指出的,大多数人不会知道所有复杂的方式,例如 findFirst、anyMatch 或类似方式,并且人们更倾向于以错误的方式进行流式传输。我的意思是这是最好的例子。了解有关 Streams 的一切需要更多知识,而其他 9 位同事则不需要。
【解决方案3】:

正如其他人提到的许多优点,但我只想在流评估中提及惰性评估。当我们使用map() 创建一个小写路径流时,我们并没有立即创建整个流,而是延迟构造,这就是为什么性能应该与传统的 for环形。它没有进行全扫描,map()anyMatch() 同时执行。一旦anyMatch()返回true,就会短路。

【讨论】:

    【解决方案4】:

    是的。你说的对。您的流方法会有一些开销。但是你可以使用这样的结构:

    private boolean isExcluded(String path) {
        return  EXCLUDE_PATHS.stream().map(String::toLowerCase).anyMatch(path::contains);
    }
    

    使用流的主要原因是它们使您的代码更简单易读。

    【讨论】:

    • anyMatchfilter(...).findFirst().isPresent() 的快捷方式吗?
    • 是的!这比我的第一个建议还要好。
    【解决方案5】:

    Java 中流的目标是简化编写并行代码的复杂性。它的灵感来自函数式编程。串口流只是为了让代码更干净。

    如果我们想要性能,我们应该使用设计的parallelStream。一般来说,串行的速度较慢。

    有一篇关于 ForLoop, Stream and ParallelStream Performance 的好文章值得阅读。

    在您的代码中,我们可以使用终止方法在第一次匹配时停止搜索。 (任意匹配...)

    【讨论】:

    • 请注意,对于小流和其他一些情况,由于启动成本,并行流可能会更慢。如果你有一个有序的终端操作,而不是一个无序的可并行化的,那么在最后重新同步。
    【解决方案6】:

    是否使用 Streams 的决定不应由性能考虑驱动,而应由可读性驱动。当真正涉及到性能时,还有其他考虑因素。

    使用您的 .filter(path::contains).collect(Collectors.toList()).size() &gt; 0 方法,您正在处理所有元素并将它们收集到一个临时的 List 中,然后再比较大小,但这对于由两个元素组成的 Stream 几乎无关紧要。

    如果您有大量元素,使用.map(String::toLowerCase).anyMatch(path::contains) 可以节省 CPU 周期和内存。尽管如此,这会将每个 String 转换为其小写表示,直到找到匹配项。显然,使用是有道理的

    private static final List<String> EXCLUDE_PATHS =
        Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
              .collect(Collectors.toList());
    
    private boolean isExcluded(String path) {
        return EXCLUDE_PATHS.stream().anyMatch(path::contains);
    }
    

    相反。因此,您不必在每次调用 isExcluded 时都重复转换为小写。如果EXCLUDE_PATHS中的元素个数或者字符串的长度真的很大,可以考虑使用

    private static final List<Predicate<String>> EXCLUDE_PATHS =
        Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
              .map(s -> Pattern.compile(s, Pattern.LITERAL).asPredicate())
              .collect(Collectors.toList());
    
    private boolean isExcluded(String path){
        return EXCLUDE_PATHS.stream().anyMatch(p -> p.test(path));
    }
    

    使用LITERAL 标志将字符串编译为正则表达式模式,使其行为类似于普通字符串操作,但允许引擎花费一些时间来准备,例如使用 Boyer Moore 算法,在实际比较时效率更高。

    当然,这只有在有足够的后续测试来补偿花费在准备上的时间时才会有所回报。确定是否会出现这种情况是实际性能考虑之一,除了第一个问题是否该操作是否对性能至关重要。不是使用 Streams 还是 for 循环的问题。

    顺便说一句,上面的代码示例保留了您原始代码的逻辑,这在我看来是有问题的。你的isExcluded 方法返回true,如果指定的路径包含列表中的任何元素,那么它返回true/some/prefix/to/my/path/one,以及my/path/one/and/some/suffix 甚至/some/prefix/to/my/path/one/and/some/suffix

    即使dummy/path/onerous 也被视为满足条件,因为它contains 字符串my/path/one...

    【讨论】:

    • 对可能的性能优化有很好的见解,谢谢。 关于您回答的最后一部分:如果我对您的评论的回复不满意,请将我的示例代码视为其他人理解我所问内容的帮助 - 而不是实际代码。此外,如果您有更好的示例,您可以随时编辑问题。
    • 我接受你的评论,这个操作是你真正想要的,所以没有必要改变它。我将仅保留最后一部分以供将来的读者使用,因此他们知道这不是典型的操作,而且已经讨论过,不需要进一步的 cmets...
    • 实际上,当工作内存量超过服务器限制时,流非常适合用于内存优化
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-09-07
    • 2012-09-22
    • 1970-01-01
    • 2012-12-23
    • 2021-07-13
    • 1970-01-01
    • 2010-12-30
    相关资源
    最近更新 更多