在Java 8 Stream中设置布尔标志

6
我想知道在Java流中设置布尔标志值的最佳实践是什么。以下是我想要做的示例:
    List<Integer> list = Arrays.asList(1,2,3,4,5);
    boolean flag = false;
    List<Integer> newList = list.stream()
                                //many other filters, flatmaps, etc...
                                .filter(i -> {
                                    if(condition(i)){
                                        flag = true;
                                    }
                                    return condition(i);
                                })
                                //many other filters, flatmaps, etc...
                                .collect(Collectors.toList());
    //do some work with the new list and the flag

然而,这违反了语言限制“lambda表达式中使用的变量应该是final或有效的final”。我能想到几种解决方案,但不确定哪种最好。我首先想到的解决方案是将与condition匹配的元素添加到列表中,并检查List::isEmpty。也可以将flag包装在AtomicReference中。
请注意,我的问题类似于这个问题,但我尝试在最后提取布尔值而不是设置变量。

1
可以将 flag 包装在 AtomicReference 中。或者是 AtomicBoolean - shmosel
2
你的第一个解决方案看起来非常不错:检查 isEmpty 似乎是正确的方法。 - Louis Wasserman
@LouisWasserman 我认为他不是在谈论结果列表,因为它需要进行额外的转换。 - shmosel
@LouisWasserman 我无法检查 newList.isEmpty(),因为之前和之后应用了许多其他流操作。我必须为该条件创建一个新列表。这是我提到的可能解决方案之一。 - mitch
4
阅读此帖的任何人都不应认为这是一个“好主意”,即从 filter() 中产生副作用。原作者应发布更多代码,因为问题可能可以更优雅地解决,并符合函数式编程的最佳实践(例如使用 findAny())。 - Brad
显示剩余4条评论
3个回答

11

不要让完全无关的任务影响你生成newList的任务。只需使用

boolean flag = list.stream().anyMatch(i -> condition(i));

接着另一个流代码。

通常有两个典型的反对意见:

  1. 但这会迭代两次。

    是的,确实如此,但是谁说迭代两次ArrayList是个问题呢?除非您知道您真正拥有一个昂贵的遍历流源(例如外部文件),否则不要试图避免多个流操作。如果您有这样的昂贵源,则可能仍然更容易先收集元素到一个集合中,然后可以遍历两次该集合。

  2. 但它比原始代码评估了condition(…)多次。

    事实上,它评估的次数比您的原始代码要少。

.filter(i -> {
    if(condition(i)){
        flag = true;
    }
    return condition(i);
})

由于 anyMatch 在找到第一个匹配项后就停止了搜索,而您原始的谓词每个元素都会无条件地评估两次 condition(i),因此可能会影响性能。


如果在条件之前有几个中间步骤,则可以将其收集到一个中间的 List 中,例如:

List<Integer> intermediate = list.stream()
    //many other filters, flatmaps, etc...
    .filter(i -> condition(i))
    .collect(Collectors.toList());
boolean flag = !intermediate.isEmpty();
List<Integer> newList = intermediate.stream()
    //many other filters, flatmaps, etc...
    .collect(Collectors.toList());

但往往情况并非如初看时那样昂贵。类似的中间步骤在不同的流操作中性能特征可能会有所变化,具体取决于实际的终端操作。因此,在进行这些步骤时,仍然可以足够地即兴完成。

boolean flag = list.stream()
    //many other filters, flatmaps, etc...
    .anyMatch(i -> condition(i));
List<Integer> newList = list.stream()
    //many other filters, flatmaps, etc...
    .filter(i -> condition(i))
    //many other filters, flatmaps, etc...
    .collect(Collectors.toList());

如果您担心代码重复本身,仍然可以将公共代码放入返回流的实用方法中。

只有在非常罕见的情况下,才有必要进入低级API并像这个答案中所示查看流。如果您这样做,不应该选择Iterator的路线,因为它会丢失关于内容的元信息,而应该使用Spliterator

Spliterator<Integer> sp = list.stream()
    //many other filters, flatmaps, etc...
    .filter(i -> condition(i))
    .spliterator();
Stream.Builder<Integer> first = Stream.builder();
boolean flag = sp.tryAdvance(first);
List<Integer> newList = Stream.concat(first.build(), StreamSupport.stream(sp, false))
    //many other filters, flatmaps, etc...
    .collect(Collectors.toList());

请注意,在这些情况下,如果flagfalse,则可以进行快捷处理,因为结果只能是一个空列表:

注:原文中的代码标记已经被保留。

List<Integer> newList = !flag? Collections.emptyList():
/*
   subsequent stream operation
 */;

我在流式代码中添加了另一条注释行,这会影响你的答案。条件在处理链的中间进行评估,而不是最初发布的代码中的开头。 - mitch
1
嗯,即便如此。将中间过滤结果存储在一个中间列表中,检查它是否为空,并从该列表继续流式处理。 - Louis Wasserman
有道理。@Holger 如果您更新您的答案,我会接受它。 - mitch

2

编辑:(基于下面Holger的评论

我只是把这个答案留在这里作为历史记录;)这是我尝试使用 Iterator 解决问题的方法,但Spliterator更好。这个答案并不完全错误,但是支持流的分裂器的特性(即SIZEDORDERED等)在将流转换为Iterator时会丢失。请参见Holger的精彩回答,了解最佳方法以及其他替代方案和关于是否值得付出努力的简要讨论。


如果您需要知道在流管道的中间是否有匹配的过滤条件,您可能希望考虑将流转换为Iterator,检查迭代器是否具有下一个元素,将该值存储为您的标志,然后从迭代器创建新的流,最后继续进行流管道。

代码如下:

Iterator<Whatever> iterator = list.stream()
    // many other filters, flatmaps, etc...
    .filter(i -> condition(i))
    .iterator();

boolean flag = iterator.hasNext();

然后,从迭代器创建一个新的 Stream

Stream<Whatever> stream = StreamSupport.stream(
    Spliterators.spliteratorUnknownSize(
        iterator, 
        Spliterator.NONNULL), // maybe Spliterator.ORDERED?
    false);

最后,继续使用流水线:

List<Integer> newList = stream
    // many other filters, flatmaps, etc...
    .collect(Collectors.toList());

现在您拥有newListflag可供使用。

我知道你不同意,但我只是把它作为“副作用”放入CHM中... - Eugene
1
我已经将类似的方法纳入我的答案中作为一种替代方案,不过它使用的是Spliterator而不是Iterator,因此不需要猜测所需的适当特性,而且如果有大小估计,则不会丢失。 - Holger
@Eugene 我知道,我知道... :D - fps
@Holger,你在Spliterator上做得很好。我没有使用它,因为tryAdvance也会消耗第一个元素,所以我认为在这种情况下使用Iterator会更好。现在显然我错了,Spliterator要好得多。我没有想到Stream.BuilderStream.concat的方法,真是聪明。 - fps

1

单独检查布尔标志

List<Integer> list = Arrays.asList(1,2,3,4,5);
List<Integer> newList = list.stream()
                            .filter(i -> condition(i))
                            //many other filters, flatmaps, etc...
                            .collect(Collectors.toList());

boolean flag = list.stream()
                     .filter(i -> condition(i))
                     .findAny()
                     .isPresent();

我在流式代码中添加了另一条注释行,这会影响你的答案。条件在处理链的中间进行评估,而不是最初发布的代码中的开头。 - mitch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接