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"
});
示例 - 流
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()?
Ste*_*ies 72
你的假设是正确的.您的流实现比for循环慢.
此流使用应该与for循环一样快:
EXCLUDE_PATHS.stream()  
                               .map(String::toLowerCase)
                               .anyMatch(path::contains);
这将逐项遍历项目,应用String::toLowerCase和过滤到项目,并在匹配的第一个项目处终止.
两者collect()&anyMatch()是终端的操作.anyMatch()但是,在第一个找到的项目退出时,collect()需要处理所有项目.
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);
}
代替.因此,您不必在每次调用时重复转换为低位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,因此它返回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......
rvi*_*t34 19
是啊.你是对的.您的流方法将有一些开销.但是你可以使用这样的结构:
private boolean isExcluded(String path) {
    return  EXCLUDE_PATHS.stream().map(String::toLowerCase).anyMatch(path::contains);
}
使用流的主要原因是它们使您的代码更简单易读.
Java中流的目标是简化编写并行代码的复杂性.它的灵感来自函数式编程.串行流只是为了使代码更清晰.
如果我们想要性能,我们应该使用parallelStream,它被设计为.通常,序列号较慢.
这是一个很好的文章阅读有关,和性能. ForLoopStreamParallelStream
在您的代码中,我们可以使用终止方法来停止第一次匹配的搜索.(anyMatch ...)
激进的答案:
绝不。曾经。曾经。
我几乎从未迭代过任何东西的列表,尤其是为了查找某些东西,但流用户和系统似乎充满了这种编码方式。
我发现很难重构和组织这样的代码,并且在流密集型系统中到处都看到冗余和过度迭代。用同样的方法你可能会看到它5次。相同的列表,发现不同的东西。
它也并不是真的更短。很少有。绝对不更具可读性,但这是一个主观意见。有人会说是的。我不。人们可能会喜欢它,因为它具有自动完成功能,但在我的编辑器 Intellij 中,我可以使用iteroritar并自动为我创建包含类型和所有内容的 for 循环。
经常被误用和过度使用,我认为最好完全避免它。Java 不是真正的函数式语言,Java 泛型很糟糕并且表达能力不够,而且当然更难以阅读、解析和重构。只需尝试访问任何本机 Java 流库即可。你觉得这样容易解析吗?
另外,流代码不容易提取或重构,除非您想开始添加返回Optionals、等奇怪的方法Predicates,Consumers并且最终会返回方法并采用各种奇怪的通用约束,其顺序和含义只有上帝知道。
当您需要访问方法来找出各种事物的类型时,会进行过多的推断。
试图让 Java 表现得像Haskell或Lisp这样的函数式语言是一件愚蠢的事。基于重流的 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 开发人员所做的事情,他们做得太过分了,并且没有简单的方法来掌握代码实际上在做什么。这个方法返回什么,这个转换在做什么,我最终会得到什么。一切都是推断出来的。阅读起来肯定要困难得多。
如果你明白这一点,那么你一定是爱因斯坦,但你应该知道并不是每个人都像你一样,这可能会在不久的将来成为你的系统。
请注意,这并不是孤立于这个项目的,但我已经看到其中许多与这些结构非常相似。
有一点是肯定的,可怕的程序员喜欢流。
| 归档时间: | 
 | 
| 查看次数: | 22871 次 | 
| 最近记录: |