我什么时候应该使用溪流?

mcu*_*nez 86 java java-8 java-stream

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

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

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

列表

private static final List<String> EXCLUDE_PATHS = Arrays.asList(new String[]{
    "my/path/one",
    "my/path/two"
});
Run Code Online (Sandbox Code Playgroud)

示例 - 流

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

示例 - For-Each循环

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

请注意,path参数始终为小写.

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

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

Ste*_*ies 72

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

此流使用应该与for循环一样快:

EXCLUDE_PATHS.stream()  
                               .map(String::toLowerCase)
                               .anyMatch(path::contains);
Run Code Online (Sandbox Code Playgroud)

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

两者collect()&anyMatch()是终端的操作.anyMatch()但是,在第一个找到的项目退出时,collect()需要处理所有项目.

  • 网上有一些关于流API性能的非常有趣的博客文章和演示文稿,我发现这对于理解这些东西是如何工作非常有帮助的.如果你对此感兴趣,我绝对可以推荐一下研究. (4认同)
  • 太棒了,不知道`findFirst()`和`filter()`的结合.显然,我确实不知道如何使用流和我想的. (2认同)

Hol*_*ger 31

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

使用您的.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);
}
Run Code Online (Sandbox Code Playgroud)

代替.因此,您不必在每次调用时重复转换为低位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));
}
Run Code Online (Sandbox Code Playgroud)

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

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

顺便说一下,上面的代码示例保留了原始代码的逻辑,这对我来说是个问题.如果指定的路径包含列表中的任何元素,则您的isExcluded方法返回true,因此它返回truefor /some/prefix/to/my/path/one,my/path/one/and/some/suffixeven或even /some/prefix/to/my/path/one/and/some/suffix.

即使dummy/path/onerous被认为符合标准,因为它contains是字符串my/path/one......

  • 我评论说这个操作是你真正想要的,所以没有必要改变它.我将为未来的读者保留最后一部分,因此他们知道这不是典型的操作,而且已经讨论过,不需要进一步的评论...... (3认同)

rvi*_*t34 19

是啊.你是对的.您的流方法将有一些开销.但是你可以使用这样的结构:

private boolean isExcluded(String path) {
    return  EXCLUDE_PATHS.stream().map(String::toLowerCase).anyMatch(path::contains);
}
Run Code Online (Sandbox Code Playgroud)

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

  • 是的!这比我的第一个建议还要好. (5认同)
  • `anyMatch`是`filter(...).findFirst().isPresent()`的快捷方式? (3认同)

Pau*_*ida 8

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

如果我们想要性能,我们应该使用parallelStream,它被设计为.通常,序列号较慢.

这是一个很好的文章阅读有关,性能. ForLoopStreamParallelStream

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

  • 请注意,对于小流和其他情况,由于启动成本,并行流可能会更慢.如果你有一个有序的终端操作,而不是一个无序的可并行化的操作,最后重新同步. (4认同)

mom*_*omo 5

激进的答案:

绝不。曾经。曾经。

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

我发现很难重构和组织这样的代码,并且在流密集型系统中到处都看到冗余和过度迭代。用同样的方法你可能会看到它5次。相同的列表,发现不同的东西。

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

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

另外,流代码不容易提取或重构,除非您想开始添加返回Optionals、等奇怪的方法PredicatesConsumers并且最终会返回方法并采用各种奇怪的通用约束,其顺序和含义只有上帝知道。

当您需要访问方法来找出各种事物的类型时,会进行过多的推断。

试图让 Java 表现得像HaskellLisp这样的函数式语言是一件愚蠢的事。基于重流的 Java 系统总是比无流系统更复杂,并且性能更低,重构和维护也更复杂。

因此也有更多的错误和充满补丁工作。由于此类系统中经常存在冗余,因此粘合工作无处不在。有些人就是不介意裁员。我不是他们中的一员。你也不应该是。

当 OpenJDK 介入时,他们开始向该语言添加一些东西,但没有真正考虑得足够透彻。现在不仅仅是 Java Streams 是一个问题。现在系统本质上更加复杂,因为它们需要更多关于这些 API 的基础知识。您可能有,但您的同事没有。他们肯定知道什么是 for 循环以及什么是 if 块。

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

大多数喜欢并喜欢流方法而不是 for 循环的人很可能是在 Java 8 之后开始学习 Java 的人。以前的人讨厌它。问题是它的使用、重构要复杂得多,而且以正确的方式使用也更加困难。避免搞砸需要技巧,而修复搞砸的问题则需要更多的技巧和精力。

当我说它的性能更差时,它并不是与 for 循环相比,这也是一个非常真实的事情,但更多的是由于此类代码必须过度迭代各种事物的趋势。迭代列表来查找项目被认为非常容易,因此往往会一遍又一遍地重复。

我还没有看到任何一个系统从中受益。我见过的所有系统都实施得很糟糕,主要是因为它,而且我曾在世界上一些最大的公司工作过。

代码绝对不比 for 循环更具可读性,而且 for 循环绝对更灵活和可重构。我们今天看到如此多复杂的糟糕系统和错误到处都是,我向你保证是因为严重依赖流来过滤,更不用说伴随着 Lombok 和 Jackson 的过度使用。这三个是系统实施不当的标志。关键词过度使用。修补工作方法。

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

通常要求数据库为您过滤事物的习惯现在并不罕见,而是使用基本查询返回一个大的事物列表,并使用各种迭代逻辑和方法来过滤掉不需要的内容,当然它们使用流来做这个。围绕着这个大列表出现了各种各样的方法,其中有各种各样的东西来过滤掉东西。

通常是冗余的过滤,因此逻辑也是如此。一遍又一遍地。

当然,我不是指你。但你的同事。正确的?

就我个人而言,我很少重复任何事情。我使用正确的数据集并依靠数据库来为我过滤它。一次。然而,在流密集型系统中,您会随处看到迭代。

在最深的方法中,在调用者中,调用者的调用者,调用者的调用者的调用者。到处都是溪流。它很丑。祝您重构位于小型 lambda 表达式中的代码好运。祝您重用它们好运。没有人会希望重用你好的谓词。

如果他们想使用它们,你猜怎么着?他们需要使用更多的 Stream。你只是让自己上瘾,并将自己进一步逼入绝境。现在,您是否建议我开始将所有代码拆分为微小的谓词、消费者、函数和 BiFcuntions?只是为了让我可以在 Streams 中重用该逻辑吗?

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

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

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

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

这并不是说它没有用例,或者它有时可能有用,但允许它相关的风险远远超过了好处。

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

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

Futures API为例。

当然,您可以开始编码所有内容以返回 Promise 或 Future,但您真的想这样做吗?这能解决什么问题吗?您的整个系统真的可以随处跟进吗?

这对你来说会更好,还是你只是在尝试并希望在某个时候能受益?

有些人在JavaRx 中过度使用,在 JavaScript 中也过度使用 Promise。当你真正想要基于未来的东西时,确实很少有情况,并且会感觉到很多极端情况,你会发现这些 API 有一定的限制,而你就被制造出来了。

您可以构建非常非常复杂且更易于维护的系统,而无需那些废话。

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

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

此外,您很少自己开发此类系统。

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

因此,虽然您可能知道如何正确使用它们,但可以放心,其他 9 个实际上并不知道。他们只是喜欢尝试和边做边学。

我将为您留下这些精彩的真实代码示例,以及数千个类似的示例:

在此输入图像描述

或这个:

在此输入图像描述

或这个:

在此输入图像描述

或这个:

在此输入图像描述

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

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

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

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

  • “有一件事是肯定的,可怕的程序员喜欢流。”。请提供证据。我本以为“可怕的程序员”甚至不会理解它们。或者你认为你的断言也证明了相反的情况?事实并非如此。很难说你在这里提出了一个连贯的案例。我们知道你不喜欢流,但我的经验是,与更一般的编码不同,在它们的位置它们往往会第一次工作:并且“可怕的编码员”不理解它们本身就是一种奖励。你的例子并不是不言而喻的“可怕”,而且你很有可能在这里批评工作代码。 (3认同)
  • 我并不是说所有 Stream 编码员都是糟糕的编码员。我是说可怕的程序员喜欢他们。如果它们正如您所说的更难使用,那么是什么让您认为他们不会尝试使用它们?此外,通过您的相同陈述,您肯定它们使用起来更复杂,所以我认为您只是提供了自己反对它们的论点。为什么要让你的代码库变得复杂而不是保持其灵活、简单和轻便?还有证据?我附加的代码片段看起来像是一个优秀的程序员写的吗?还是可怕的?也许这段代码对你来说读起来像诗? (3认同)