Java流:查找匹配项还是最后一个?

23

如何使用Java Stream查找列表中的第一个匹配项或最后一个元素?

这意味着如果没有任何元素满足条件,则返回最后一个元素。

例如:

OptionalInt i = IntStream.rangeClosed(1,5)
                         .filter(x-> x == 7)
                         .findFirst();
System.out.print(i.getAsInt());

我该怎么做才能让它返回5;

5个回答

16

给定列表

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

你可以这样做:

int value = list.stream().filter(x -> x == 2)
                         .findFirst()
                         .orElse(list.get(list.size() - 1));

如果过滤器计算结果为true,则检索该元素,否则返回列表中最后一个元素。

如果列表是空的,您可以返回一个默认值,例如-1。

int value = list.stream().filter(x -> x == 2)
                         .findFirst()
                         .orElse(list.isEmpty() ? -1 : list.get(list.size() - 1));

7
你可以像这样使用reduce()函数:
OptionalInt i = IntStream.rangeClosed(1, 5)
        .reduce((first, second) -> first == 7 ? first : second);
System.out.print(i.getAsInt());

1
太棒了。不过你可能想使用 reducing() 收集器,它允许你提供一个身份元素,这样你就能处理空的和只有一个元素的流了。 - daniu
15
@daniu 唯一的问题是它不是短路计算,所以如果你的第一个匹配恰好在第一个元素上发生,即使你已经知道结果,仍需要遍历整个流的源。 - Eugene
@daniu,我们工作中没有这样的“reduce”可以短路。在我们的代码库中,我们会抛出一个没有堆栈跟踪的异常来退出“reduce”,但在我们的情况下遍历源代码会更加昂贵,这就是我们这样做的原因。 - Eugene
5
reduce 不是用于执行搜索,而是用于关联操作,例如对元素求和。在 Stream 中有称为 findAnyfindFirst 的方法来执行搜索操作。此外,如果流是并行的,这种操作将无法正常工作,而 findFirst 可以胜任此项工作,即使流具有不同的特征。正如其他人所说,这也不会进行短路处理... - fps
4
这个函数是可结合的,因此在并行执行时不会出现任何问题。所以唯一的问题就是它效率低下。 - Holger
显示剩余4条评论

5
基本上,我会使用以下两种方法或其变体之一:
流变体:
<T> T getFirstMatchOrLast(List<T> list, Predicate<T> filter, T defaultValue) {
    return list.stream()
            .filter(filter)
            .findFirst()
            .orElse(list.isEmpty() ? defaultValue : list.get(list.size() - 1));
}

非流式变体:

<T> T getFirstMatchOrLast(Iterable<T> iterable, Predicate<T> filter, T defaultValue) {
    T relevant = defaultValue;
    for (T entry : iterable) {
        relevant = entry;
        if (filter.test(entry))
            break;
    }
    return relevant;
}

正如Ilmari Karonen在评论中建议的那样,使用Iterable<T>,如果你真的处理的是一个Stream而不是一个List,你甚至可以调用stream::iterator。调用所示方法的方式如下:
getFirstMatchOrLast(Arrays.asList(1, 20, 3), i -> i == 20, 1); // returns 20
getFirstMatchOrLast(Collections.emptyList(), i -> i == 3, 20); // returns 20
getFirstMatchOrLast(Arrays.asList(1, 2, 20), i -> i == 7, 30); // returns 20
// only non-stream variant: having a Stream<Integer> stream = Stream.of(1, 2, 20)
getFirstMatchOrLast(stream::iterator, i -> i == 7, 30); // returns 20

我不会在这里使用reduce,因为我认为它在意义上听起来不对,即使第一个条目已经匹配,它也会遍历整个条目,即它不再进行短路。此外,对我来说,它不如filter.findFirst.orElse可读性好...(但这可能只是我的观点)。
我可能最终会得到以下内容:
<T> Optional<T> getFirstMatchOrLast(Iterable<T> iterable, Predicate<T> filter) {
    T relevant = null;
    for (T entry : iterable) {
        relevant = entry;
        if (filter.test(entry))
            break;
    }
    return Optional.ofNullable(relevant);
}
// or transform the stream variant to somethinng like that... however I think that isn't as readable anymore...

所以调用应该更像这样:

getFirstMatchOrLast(Arrays.asList(1, 2, 3, 5), i -> i == 7).orElseThrow(...)
getFirstMatchOrLast(Arrays.asList(1, 2, 3, 5), i -> i == 7).orElse(0);
getFirstMatchOrLast(Arrays.asList(1, 2, 3, 5), i -> i == 7).orElseGet(() -> /* complex formula */);
getFirstMatchOrLast(stream::iterator, i -> i == 5).ifPresent(...)

2
我喜欢你的“非流变体”(带或不带Optional),但我建议将其泛化为接受任何Iterable<T>而不仅仅是List<T>。这样,如果你确实需要处理一个流,你可以将stream::iterator传递给这个方法。 - Ilmari Karonen

3

如果您想在一个管道中完成此操作,则可以执行以下操作:

int startInc = 1;
int endEx = 5;
OptionalInt first = 
       IntStream.concat(IntStream.range(startInc, endEx)
                .filter(x -> x == 7), endEx > 1 ? IntStream.of(endEx) : IntStream.empty())
                .findFirst();

但是最好将生成的数字收集到一个列表中,然后按照以下方式进行操作:

// first collect the numbers into a list
List<Integer> result = IntStream.rangeClosed(startInc,endEx)
                                   .boxed()
                                   .collect(toList());
    // then operate on it 
int value = result.stream()
                  .filter(x -> x == 7)
                  .findFirst()
                  .orElse(result.get(result.size() - 1)); 

另外,如果您希望在源为空时(如果这是可能的情况)使后者返回一个空的 Optional 而不是抛出异常,则可以执行以下操作:

List<Integer> result = IntStream.rangeClosed(startInc,endEx)
                                .boxed()
                                .collect(toList());

Optional<Integer> first = 
         Stream.concat(result.stream().filter(x -> x == 7), result.isEmpty() ? 
                Stream.empty() : Stream.of(result.get(result.size() - 1)))
                .findFirst();

2

我不确定您为什么想要使用流来实现,一个简单的 for 循环就足够了:

public static <T> T getFirstMatchingOrLast(List<? extends T> source, Predicate<? super T> predicate){
    // handle empty case
    if(source.isEmpty()){
        return null;
    }
    for(T t : source){
        if(predicate.test(t)){
            return t;
        }
    }
    return source.get(source.size() -1);
} 

然后可以像这样调用:

Integer match = getFirstMatchingOrLast(ints, i -> i == 7);

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