When should I use streams?

我只是在使用 List及其 stream()方法时遇到了一个问题。虽然我知道 怎么做使用它们,但我不太确定 什么时候使用它们。

For example, I have a list, containing various paths to different locations. Now, I'd like to check whether a single, given path contains any of the paths specified in the list. I'd like to return a boolean based on whether or not the condition was met.

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

名单

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

Example using Stream:

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 方法更快,因为如果满足条件,循环将立即返回。而流仍将循环遍历所有列表条目以完成筛选。

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

43486 次浏览

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

这个流的使用速度应该和 for 循环一样快:

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

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

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

Yeah. You are right. Your stream approach will have some overhead. But you may use such a construction:

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

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

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

如果我们想要性能,我们应该使用并行流,这是设计。串行的,一般来说,是较慢的。

有一篇关于 ABC0、 ABC1和 ParallelStream的性能的好文章可以读。

在您的代码中,我们可以使用终止方法来停止第一个匹配的搜索

The decision whether to use Streams or not should not be driven by performance consideration, but rather by readability. When it really comes to performance, there are other considerations.

使用 .filter(path::contains).collect(Collectors.toList()).size() > 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);
}

instead. So you don’t have to repeat the conversion to lowcase in every invocation of isExcluded. If the number of elements in EXCLUDE_PATHS or the lengths of the strings becomes really large, you may consider using

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标志的 regex 模式,使其行为与普通字符串运算类似,但允许引擎花一些时间进行准备,例如使用 Boyer Moore 算法,以便在进行实际比较时更有效率。

当然,只有在有足够的后续测试来补偿准备工作所花费的时间时,这种方法才会有效。确定是否会出现这种情况,是实际的性能考虑因素之一,除了第一个问题外,还要考虑该操作是否永远都是性能关键因素。不是使用 Streams 还是 for循环的问题。

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

甚至 dummy/path/onerous也被认为符合条件,因为它是字符串 contains,字符串 my/path/one..。

正如其他人已经提到了很多好的观点,但是我只想在流评估中提到 懒惰的评估。当我们使用 map()创建一个小写路径流时,我们不会立即创建整个流,而是使用 懒散地建造,这就是为什么性能应该等同于传统的 for 循环。它没有进行完整的扫描,map()anyMatch()是同时执行的。一旦 anyMatch()返回 true,它将被短路。

激进的回答:

永远,永远,永远。

我几乎从来没有迭代过任何列表,特别是为了找到某些东西,然而流用户和系统似乎充满了这种编码方式。

我发现重构和组织这样的代码很困难,而且我看到在流量大的系统中到处都是冗余和重复。用同样的方法你可能会看到它5次。同样的清单,找到不同的东西。

It is also not really shorter either. Rarely is. Definitely not more readable but that is a subjective opinion. Some people will say it is. I don't. People might like it due to autocompletion but in my editor Intellij, I can just iter or itar and have the for loop auto created for me with types and everything.

经常被误用和过度使用,我认为最好完全避免。Java 不是一种真正的函数式语言,Java 泛型糟糕透顶,表达能力不够强,当然也更难以阅读、解析和重构。只要尝试访问任何本机 Java 流库。你觉得这很容易理解吗?

此外,流代码不容易提取或重构,除非你想开始添加奇怪的方法返回 OptionalsPredicatesConsumers和什么不,你最终有方法返回和采取各种奇怪的通用约束的顺序和含义只有上帝知道。

在需要访问方法来确定各种事物的类型时,推断出的内容太多了。

试图让 Java 表现得像 Haskell口齿不清这样的函数式语言是愚蠢的。基于大量流的 Java 系统总是比没有流的系统更复杂,性能更差,重构和维护更复杂。

因此也更多的错误和补丁工作填补。由于这样的系统经常需要填充冗余,所以到处都要进行粘合工作。有些人就是不介意裁员。我不是他们中的一员。你也不该害怕。

当 OpenJDK 参与进来的时候,他们开始在语言中添加一些东西,但是并没有认真考虑。现在的问题不仅仅是 Java 流。现在的系统天生就更复杂,因为它们需要更多这些 API 的基础知识。你可能有,但你的同事没有。他们肯定知道 for 循环和 if 块是什么。

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

大多数喜欢并且更喜欢流方法而不是 for 循环的人很可能是在 Java8之后开始学习 Java 的人。以前的人讨厌它。问题是,它的使用要复杂得多,重构和更难以使用正确的方式。它需要不搞砸的技能,然后更多的技能和能量来修复他妈的错误。

当我说它的性能更差的时候,它并不是和 for 循环相比,for 循环也是一个非常实际的东西,但更多的是由于这样的代码必须过度迭代范围很广的东西。人们认为迭代一个列表来查找一个项目是如此容易,以至于它往往被一遍又一遍地重复。

I've not seen a single system that has benefitted from it. All of the systems I have seen are horribly implemented, mostly because of it, and I've worked in some of the biggest companies in the world.

代码肯定不比 for 循环更具可读性,而 for 循环肯定更加灵活和可重构。今天我们看到这么多复杂的垃圾系统和漏洞无处不在的原因是,我向你保证,这是因为我们严重依赖于过滤数据流,更不用说过度使用龙目岛和杰克逊了。这三点是一个执行不力的系统的标志。关键字 过度使用。修补工作的方法。

同样,我认为迭代一个列表来查找任何东西是非常糟糕的。然而,对于基于 Stream 的系统,这正是人们一直在做的事情。解析和检测迭代可能是 O (N2)也并不少见,而使用 for 循环时,您会立即看到它。

通常要求数据库为你过滤的东西现在并不罕见,相反,一个基本查询返回一个大列表的东西,各种迭代逻辑和方法过滤出不受欢迎的,当然他们使用流来做这件事。各种各样的方法围绕着这个大列表产生,其中包含了各种各样的东西来过滤掉这些东西。

经常是冗余过滤,因此也有逻辑

当然,我指的不是你,而是你的同事,对吧?

就我个人而言,我很少迭代任何东西。我使用正确的数据集,并依靠数据库为我过滤它。Once.然而,在流重型系统中,到处都可以看到迭代。

在最深的方法中,在调用方中,在调用方的调用方中,在调用方的调用方中,在调用方的调用方中。到处都是水流。确实很丑。祝你重构小 lambdas 中的代码顺利。祝你好运能重复使用它们。没有人会期待重用你漂亮的谓词。

如果他们想用的话,你猜怎么着?他们需要使用更多的流。你只是让自己上瘾,把自己逼得更紧了。现在,你是否建议我开始将我所有的代码分割成小谓词、消费者、函数和双函数?这样我就可以在 Streams 中重用这个逻辑?

当然,我也很讨厌 Javascript,因为在 Javascript 中,菜鸟前端开发人员到处都是迭代。

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

此外,在需要三台服务器来承载所有用户的情况下,我只需要一台服务器即可。因此,这样一个系统所需要的可伸缩性要比非流重型系统所需要的可伸缩性要早得多。对于小型项目来说,这是一个非常重要的指标。如果有5000个并发用户,我的系统可以处理两到三倍的数量。

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

这并不是说它没有用例,也不是说它有时可能有用,而是说 相关风险允许它远离 超过的好处。

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

你不会想要那种风格。它并不比旧的风格更好。特别是在 Java 上。

Take the 期货空气污染指数 as an example.

当然,您可以开始编写所有代码来返回一个项目或未来,但是您真的想这样做吗?这能解决问题吗?你的整个系统真的能在任何地方跟进吗?

这对你来说是否更好,或者你只是在尝试,希望在某个时候你能从中受益?

也有人在 JavaScript 中过多地使用 JavaRx和承诺。真的很少有情况下,当你真的想有东西未来的基础上,非常多的角落情况下,你会感觉到,你会发现,这些 API 有一定的限制,你刚刚得到。

您可以构建非常复杂且可维护性更强的系统,而不需要所有这些废话。

这就是它的意义所在。它不是关于你的兴趣项目的扩展和成为一个可怕的代码库。

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

此外,您很少独自在这样的系统上工作。

您很可能与至少10个人一起工作,他们都在尝试和过度使用 Streams。

So while you might know how to use them properly you can rest assure the other 9 really don't. They just love experimenting and learning by doing.

我将给你们留下这些真实代码的精彩例子,成千上万与它们更加相似:

enter image description here

或者这样:

enter image description here

或者这样:

enter image description here

或者这样:

enter image description here

尝试重构以上任何一个。我向你挑战。试试看。一切都是溪流,无处不在。这就是 Stream 开发人员所做的,他们做过头了,而且没有简单的方法来理解代码实际上在做什么。这个方法返回了什么,这个转换做了什么,我最终得到了什么。一切都是推断出来的。肯定更难读。

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

请注意,这不是孤立的这一个项目,但我已经看到了很多非常类似的这些结构。

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