根据条件查找第一个元素

689

我刚开始尝试使用Java 8 lambda表达式,并试图实现一些我在函数式语言中习惯的东西。

例如,大多数函数式语言都有一些基于序列或列表操作的查找函数,返回满足谓词条件的第一个元素。在Java 8中,我唯一能想到的实现方式是:

lst.stream()
    .filter(x -> x > 5)
    .findFirst()

然而,这对我来说似乎是低效的,因为过滤器将会扫描整个列表,至少在我的理解中是这样(我可能是错误的)。是否有更好的方法?


76
Java 8的Stream实现是惰性求值的,因此筛选操作(filter)仅应用于终止操作(terminal operation),并不会使程序变得低效。同样的问题在这里可以找到:https://dev59.com/42Ei5IYBdhLWcg3wfsbI - Marek Gregor
1
很好,这正是我希望它能做到的。否则它将成为一个重大的设计失败。 - siki
2
如果你的意图是检查列表是否包含这样的元素(而不是单独选择可能有多个的第一个元素),.findAny() 在并行设置中理论上可以更有效,并且当然更清晰地传达了这个意图。 - Joachim Lous
2
与简单的forEach循环相比,这将在堆上创建大量对象和数十个动态方法调用。虽然这可能不总是影响性能测试底线,但在热点区域中,避免使用Stream和类似的重型结构对性能有所影响。 - Agoston Horvath
我建议删除这个问题,因为他们不在乎这里的文档,而这些文档是正确的。 - undefined
8个回答

919

不,过滤器不会扫描整个流。它是一个中间操作,返回一个延迟流(实际上所有中间操作都返回延迟流)。为了让您信服,您可以简单地进行以下测试:

List<Integer> list = Arrays.asList(1, 10, 3, 7, 5);
int a = list.stream()
            .peek(num -> System.out.println("will filter " + num))
            .filter(x -> x > 5)
            .findFirst()
            .get();
System.out.println(a);

输出结果为:

will filter 1
will filter 10
10

你会发现,流中只有前两个元素被实际处理了。

所以你可以采用你的方法,完全没有问题。


53
注意,我在这里使用了get();是因为我知道我向流管道提供了哪些值,因此会有结果。实际上,您不应使用get();,而应使用orElse() / orElseGet() / orElseThrow()(以获取更有意义的错误而不是NSEE),因为您可能不知道应用于流管道的操作是否会产生元素。 - Alexis C.
47
例如:.findFirst().orElse(null); - Gondy
26
不要使用orElse null。 这应该是一种反模式。 因为所有内容都包含在Optional中,你为什么要冒NPE的风险呢?我认为处理Optional是更好的方法。 在使用之前,只需使用isPresent()测试Optional即可。 - BeJay
@BeJay 我不明白。我应该使用什么来代替 orElse - John Henckel
8
@JohnHenckel,我认为BeJay的意思是你应该将其保留为Optional类型,因为这是.findFirst返回的类型。Optional的一个用途是帮助开发人员避免处理null。例如,您可以检查myOptional.isPresent()而不是检查myObject != null,或者使用Optional接口的其他部分。这样说清楚了吗? - Alexander Terp
显示剩余2条评论

127
然而,这对我来说似乎效率不高,因为过滤器将扫描整个列表。

不会的 - 只要找到第一个满足谓词的元素,它就会“中断”。您可以在stream package javadoc中阅读有关惰性的更多信息,特别是(重点是我的):

许多流操作(例如过滤、映射或重复删除)都可以实现惰性计算,从而为优化提供机会。例如,“查找具有三个连续元音字母的第一个字符串”不需要检查所有输入字符串。流操作分为中间(生成流)操作和终端(值或副作用生成)操作。中间操作总是惰性的。


8
这个回答对我来说更具信息量,不仅解释了如何,还解释了为什么。我从未意识到中间操作始终是惰性的;Java 流仍然让我感到惊讶。 - kevinarpe

63
return dataSource.getParkingLots()
                 .stream()
                 .filter(parkingLot -> Objects.equals(parkingLot.getId(), id))
                 .findFirst()
                 .orElse(null);

我需要从一个对象列表中筛选出一个对象。所以我使用了这个方法,希望能帮到你。


更好的做法是,由于我们正在寻找布尔返回值,因此可以通过添加空值检查来实现更好的效果:return dataSource.getParkingLots().stream().filter(parkingLot -> Objects.equals(parkingLot.getId(), id)).findFirst().orElse(null) != null; - shreedhar bhat
3
@shreedharbhat 您不需要执行 .orElse(null) != null。相反,可以利用 Optional API 的.isPresent, 即 .findFirst().isPresent(). - Alexander Terp
2
@shreedharbhat 首先,OP并不是在寻找布尔返回值。其次,即使他们需要,写成.stream().map(ParkingLot::getId).anyMatch(Predicate.isEqual(id))会更加简洁。 - Ozymandias
这个很好用,而且运行良好。你可以在列表中获取整个对象,但是我想进行一些更正。你写的是 equals,但实际上应该是 equal,所以代码如下: .filter(parkingLot -> Objects.equal(parkingLot.getId(), id)) - Vibran
完美的答案! - Gaurav

22

除了 Alexis C 的回答之外,如果你正在使用一个数组列表,并且你不确定你要搜索的元素是否存在,请使用以下方法。

Integer a = list.stream()
                .peek(num -> System.out.println("will filter " + num))
                .filter(x -> x > 5)
                .findFirst()
                .orElse(null);

那么您可以简单地检查 a 是否为 null


2
你应该修复你的示例。你不能将null赋值给普通int。https://dev59.com/vHE95IYBdhLWcg3wkeuq - RubioRic
我已经编辑了你的帖子。当你在整数列表中搜索时,0(零)可能是一个有效的结果。将变量类型替换为Integer,并将默认值替换为null。 - RubioRic
完美无瑕的答案! - Gaurav

16

已经由@AjaxLeung回答,但是在评论中很难找到。
仅供检查

lst.stream()
    .filter(x -> x > 5)
    .findFirst()
    .isPresent()

被简化为

lst.stream()
    .anyMatch(x -> x > 5)

4

import org.junit.Test;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

// Stream is ~30 times slower for same operation...
public class StreamPerfTest {

    int iterations = 100;
    List<Integer> list = Arrays.asList(1, 10, 3, 7, 5);


    // 55 ms
    @Test
    public void stream() {

        for (int i = 0; i < iterations; i++) {
            Optional<Integer> result = list.stream()
                    .filter(x -> x > 5)
                    .findFirst();

            System.out.println(result.orElse(null));
        }
    }

    // 2 ms
    @Test
    public void loop() {

        for (int i = 0; i < iterations; i++) {
            Integer result = null;
            for (Integer walk : list) {
                if (walk > 5) {
                    result = walk;
                    break;
                }
            }
            System.out.println(result);
        }
    }
}


这就是我避免在简单任务中使用流的原因。通常情况下,它比使用简单迭代要慢得多。(如果您使用数组操作,情况甚至更糟。但是谁会这么做呢...) - Vankog

1
一种通用的循环实用函数对我来说更加简洁:
static public <T> T find(List<T> elements, Predicate<T> p) {
    for (T item : elements) if (p.test(item)) return item;
    return null;
}

static public <T> T find(T[] elements, Predicate<T> p) {
    for (T item : elements) if (p.test(item)) return item;
    return null;
}

使用中:

List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5);
Integer[] intArr = new Integer[]{1, 2, 3, 4, 5};

System.out.println(find(intList, i -> i % 2 == 0)); // 2
System.out.println(find(intArr, i -> i % 2 != 0)); // 1
System.out.println(find(intList, i -> i > 5)); // null

-1

改进的一行代码:如果你想要一个布尔返回值,我们可以通过添加isPresent来更好地实现:

return dataSource.getParkingLots().stream().filter(parkingLot -> Objects.equals(parkingLot.getId(), id)).findFirst().isPresent();

8
如果您想要一个布尔返回值,应该使用anyMatch。 - Ozymandias

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