【问题标题】:Using Streams to replace loops使用 Streams 替换循环
【发布时间】:2022-01-09 07:28:09
【问题描述】:

我有以下代码:

boolean found = false;
for (Commit commit : commits.values()) {
    if (commit.getLogMessage().compareTo(commitMessageToSearch) == 0) {
        System.out.println(commit.getSha());
        found = true;
    }
}
if (!found) {
    System.out.println("aksdhlkasj");
}

有没有办法用Java中的流或其他任何东西简洁地编写这个

【问题讨论】:

    标签: java java-stream


    【解决方案1】:

    以下选项可用:

    1. 使用StreamEx库(maven repo)及其准中间操作StreamEx::ifEmpty
    import one.util.streamex.StreamEx;
    // ...
    String msgSearch = "commit message";
    StreamEx.of(commits.values())
            .filter(c -> c.getLogMessage().compareTo(msgSearch) == 0)
            .ifEmpty("no commit message found: " + msgSearch)
            .map(Commit::getSha)
            .forEach(System.out::println);
    
    • 另外:非常简洁干净,单次运行
    • 减号:使用第 3 方库

    1. 使用短路匹配检查流的内容没有多余的收集到一个列表中只是为了检查它是否为空
    if (commits.values().stream()
        .map(Commit::getLogMessage) // equals should be equivalent to compareTo == 0
        .anyMatch(msgSearch::equals)
    ) {
        commits.values().stream()
            .filter(c -> c.getLogMessage().compareTo(msgSearch) == 0)
            .map(Commit::getSha)
            .forEach(System.out::println);
    } else {
        System.out.println("no commit message found: " + msgSearch);
    }
    
    • 另外:使用标准 API(无第三方),没有副作用
    • 缺点:过于冗长,在最坏的情况下(最后是匹配元素的长列表)流的双重迭代

    1. 有效地使用流中的最终变量及其设置作为副作用。

    免责声明:有状态流的使用和副作用是not recommended in general

    最好的方法是避免有状态的行为参数完全流式操作...
    通常不鼓励对流操作的行为参数产生副作用,因为它们通常会导致无意中违反无状态要求以及其他线程安全隐患。
    少数流操作,例如forEach()peek(),只能通过副作用操作;这些应该小心使用

    因此,如果 AtomicBoolean 被仔细选择为 found 标志的布尔值的线程安全和快速容器,从流内部设置为 true (永不重置),可以提供以下解决方案并降低安全和性能风险:

    // effectively final and thread-safe container boolean
    AtomicBoolean found = new AtomicBoolean(); 
    
    commits.values().stream()
        .filter(c -> c.getLogMessage().compareTo(commitMessageToSearch) == 0)
        .map(Commit::getSha)
        .forEach(sha -> {
            found.set(true); 
            System.out.println(sha);
        });
    
    if (!found.get()) {
        System.out.println("none found");
    }
    
    • 另外:使用标准 API(无第三方库),单次运行,无冗余收集
    • 减号:使用副作用,纯粹主义者不鼓励,只是模仿 for-each 循环

    【讨论】:

    • 我不是反对者,但我很可能知道原因。 Stream 通过修改AtomicBoolean 的状态违反了“无副作用”的原则。有关此主题的更多信息,请参阅我的问题,因为在我了解 Streams 的真正目的和行为之前,我最初以这种方式使用它们:stackoverflow.com/questions/50977793/…
    • @Nikolas,我为此写了一份免责声明,并提供了一个没有副作用的版本(比循环解决方案更冗长)。 OP 的问题是这种转换为流的解决方案是否可行,所以我试图用上述警告来证明可行性。您提到的示例是使用AtomicInteger,这里AtomicBoolean 的值设置为true once 并且没有切换,所以我想不出一个真正的负面 后果这个解决方案应该适用于并行和顺序流。
    • @NikolasCharalambidis,我发现了一个使用 StreamEx::ifEmpty 方法的有趣解决方案,尽管它是为解决标准 API 的已知限制而创建的第 3 方库
    • 有趣的是,还有一个使用标准 JDK Stream 的解决方案。
    【解决方案2】:

    如果您想打印出所有的出现并打印一些字符串,以防万一,没有找到项目,恐怕除了收集所有相关的sha之外别无他法将值放入列表并使用 Optional 检查其是否为空:

    commits.values()
           .stream()
           .filter(commit -> commit.getLogMessage().compareTo(commitMessageToSearch) == 0)
           .map(Commit::getSha)
           .peek(System.out::println)
           .collect(Collectors.collectingAndThen(Collectors.toList(), Optional::of))
           .filter(List::isEmpty)
           .ifPresent(emptyList -> System.out.println("aksdhlkasj"));
    

    虽然中间输出Optional<List<?>>有违常识,但它有助于使用Optinal进行进一步处理,并轻松处理列表为空的情况。

    不过,我认为这种形式更具可读性:

    List<String> shaList = commits.values()
            .stream()
            .filter(commit -> commit.getLogMessage().compareTo(commitMessageToSearch) == 0)
            .map(Commit::getSha)
            .peek(System.out::println)
            .collect(Collectors.toList());
            
    if (shaList.isEmpty()) {
        System.out.println("aksdhlkasj");
    }
    

    【讨论】:

    • 这实际上类似于@Unmitigated 的现有答案,它不会像循环解决方案那样打印 all 匹配的 SHA。也许有一天,一个处理/记录空流的方法最终会被添加到 Stream API 以摆脱这种笨拙(例如,像forEachOrEmpty 接受两个消费者)。
    • @AlexRudenko 似乎@Unmitigated 对这个问题有相同的理解,但无论如何我的答案是不同的。我最初认为 OP 错过了break;。现在我理解了这个问题并编辑了我的答案。有兴趣的请看一下。
    【解决方案3】:

    您可以将Stream#filterStream#findFirst 一起使用。

    System.out.println(commits.values().stream()
        .filter(commit -> commit.getLogMessage().compareTo(commitMessageToSearch) == 0)
        .findFirst().map(Commit::getSha).orElse("aksdhlkasj"));
    

    【讨论】:

    • 将管道放入方法参数中会损害可读性。改用ifPresent()ifPresentOrElse() 调用println()
    • @Unmitigated 是 findFirst 这里需要吗?因为我想打印所有匹配提交,而不仅仅是第一个 on
    • @daniel 如果您需要打印所有匹配项,请不要致电findFirst
    • 那么如果没有找到匹配,aksdhlkasj应该如何打印?
    猜你喜欢
    • 1970-01-01
    • 2019-04-04
    • 2018-08-20
    • 1970-01-01
    • 1970-01-01
    • 2021-04-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多