Java 中findFirst()和findAny()的使用指南

3,616 阅读11分钟

简介

findFirst()findAny() 方法是Stream API的终端操作(终止和返回结果)。然而,它们有一些特别之处--它们不仅可以终止一个流,还可以将其短路。

 List<String> people = List.of("John", "Janette", "Maria", "Chris");

Optional<String> person = people.stream()
                .filter(x -> x.length() > 4)
                .findFirst();
        
Optional<String> person2 = people.stream()
                .filter(x -> x.length() > 4)
                .parallel()
                .findAny();

person.ifPresent(System.out::println);
person2.ifPresent(System.out::println);
Janette
Chris

那么,这两者之间有什么区别,你如何有效地使用它们?

在本指南中,我们将深入探讨Java中的findFirst()findAny() 方法,以及它们的应用和最佳实践。

终端短路?

另一个常用的终端操作是 forEach()方法,但它除了是一种不同的操作外,仍然有本质上的不同。

为了了解为什么findFirst()findAny() 操作与其他终端设施(如forEach() )不同,假设你有一个具有无限多元素的流。

当你在这样一个流上调用forEach() ,该操作将遍历该流中的所有元素。

对于一个无限多的元素,你的forEach() 调用将需要无限长的时间来完成处理。

然而,findFirst()findAny() 并不需要检查流中的所有元素,只要找到它们要搜索的元素就会短路。因此,如果你从一个无限的流中调用它们,一旦它们找到你指示的内容,它们就会立即终止这个流。

这表明这两个操作将总是在有限时间内结束。

注意:值得注意的是,它们会缩短中间操作,比如执行过程中filter() 方法,因为如果找到了匹配的东西,根本不需要进一步过滤。

因此,当你想退出可能无休止地运行的流处理时,findFirst()findAny() 操作是非常必要的。作为一个类比,可以把这两个操作看作是类似于你杀死一个经典的whilefor 循环的操作,这个循环的递归是无限的。

本指南将详细探讨这两个操作的工作原理。首先,我们将从它们的官方定义开始。其次,我们将把它们应用于简单的用例。然后,我们将审视它们之间错综复杂的差异。

最后,我们将利用这些发现来确定如何在要求更高的用例中最好地使用它们;尤其是那些需要精心设计代码以提高处理速度的用例。

findFirst()和findAny()的定义

findFirst() 和 返回值--它们不象 或 等中间操作那样返回流的实例。findAny() forEach() filter()

然而,findFirst()findAny() 返回的值总是一个Optional<T> 类型。

一个可选项是一个。

[...] 容器对象,它可能包含也可能不包含一个非空值。

这就是所说的--这些的查找操作会返回一个空的安全值,以防该值不存在于流中。

findFirst() 方法返回一个流的第一个元素或一个空的Optional。如果流中没有遇到的顺序,任何元素都会被返回,因为无论如何哪个是第一个元素都是模糊的。

findAny() 方法返回流中的任何元素--与没有相遇顺序的findFirst() 很相似。

findFirst()findAny()的用例

让我们来看看这些方法的一些使用情况,以及什么时候你可能更喜欢其中一个。由于有Strings的例子通常不会变得复杂,比如你有一个Person 对象的流。

Stream<Person> people = Stream.of(
        new Person("Lailah", "Glass"),
        new Person("Juliette", "Cross"),
        new Person("Sawyer", "Bonilla"),
        new Person("Madilynn", "Villa"),
        new Person("Nia", "Nolan"),
        new Person("Chace", "Simmons"),
        new Person("Ari", "Patrick"),
        new Person("Luz", "Gallegos"),
        new Person("Odin", "Buckley"),
        new Person("Paisley", "Chen")
);

其中一个Person

public class Person implements Comparable<Person> {

    private final String firstName;
    private final String lastName;

    // Constructor, getters
    // equals() and hashCode()
	// compareTo(Person otherPerson)

    @Override
    public String toString() {
        return String.format("Person named: %s %s", firstName, lastName);
    }
    
    @Override 
    public int compareTo(Person otherPerson) {        
        return Comparator.comparing(Person::getFirstName)
                .thenComparing(Person::getLastName)
                .compare(this, otherPerson);
    }
}

比较器使用人们的firstName 字段进行比较,然后通过他们的lastName 字段进行比较。

而且,你想知道哪个人有一个相当长的名字。也就是说--你可能想找到一个有长名字的,或者第一个有长名字的

比方说,任何超过7个字母的名字都是一个长名字。

private static boolean isFirstNameLong(Person person) {
    return person.getFirstName().length() > 7;
}

使用Person 流,让我们使用isFirstNameLong() 谓词过滤对象并找到一个人

people
    .filter(FindTests::isFirstNameLong) // (1)
    .findFirst() // (2)
    .ifPresentOrElse( // (3)
            System.out::println, // (3.1)
            () -> System.out.println("No person was found") // (3.2)
    );

第一行过滤人的流,a返回一个新的流,该流只包含Person 对象,其firstName 有7个以上的字母。

第二行是如果findFirst() 操作发现一个超过7个字母的firstName ,则终止该流。

第三行询问findFirst() 操作所返回的Optional<Person> 。其中,它可能(或可能不)包含一个具有长名字的Person

  1. firstName如果Optional 包含一个带有长名字的Person ,打印其详细信息到控制台。
  2. 如果没有,则打印一个信息。"没有找到人。"

因此,当你运行上面的代码时,你会得到输出。

Person named: Juliette Cross

现在,让我们尝试用findAny() 操作来实现这个用例。这很容易,只需将上面的findFirst() 调用换成findAny()

people
    .filter(FindTests::isFirstNameLong) // (1)
    .findAny() // (2)
    .ifPresentOrElse( // (3)
            System.out::println, // (3.1)
            () -> System.out.println("No person was found") // (3.2)
    );

然而,当我们运行这段代码时,我们会得到同样的输出,即使你多次运行该代码。

Person named: Juliette Cross

这是为什么呢?

好吧,这两个方法都是在遇到名字为*"Juliette Cross "*的Person 时,就将filter() 的操作短路,所以返回的结果是一样的。findAny() 方法不能在她和其他人之间做出选择,因为在她之后甚至没有人被允许进入该流。

这个结果表明,在这种设置下,我们没有充分地利用findFirst()findAny() 的能力。让我们来看看我们如何改变这些方法的环境来获得我们所期望的结果。

findFirst()findAny()之间做出选择

findFirst() 操作中包含术语 "第一",这意味着有一个特定的元素顺序,你只对处于第一位置的元素感兴趣。

正如前面所暗示的那样--这些方法是相同的,取决于你是否用相遇的顺序启动你的流。

如果没有顺序,两者的行为都像findAny() ,如果有顺序,两者的行为都像findFirst()

因此,让我们重新审视这个用例,以改进设计解决方案的方法。我们需要找到一个Person ,有一个冗长的firstName ;一个有七个以上的字母。

因此,我们应该进一步阐述我们的要求,不仅要寻找一个长的firstName ,而且要寻找一个在这些长的名字排序中排在前面的名字。

这样一来,我们就可以将代码改为。

people.sorted() //(1)
     .peek(person -> System.out.printf("Traversing stream with %s\n", person)) //(2)
     .filter(FindTests::isFirstNameLong) //(3)
     .findFirst() //(4)
     .ifPresentOrElse( //(5)
         System.out::println, //(5.1)
         () -> System.out.println("No person was found") //(5.2)
 );

与之前的代码片段相比,我们在这个代码片段中增加了两个步骤。

首先,我们使用自然顺序对Person 对象进行排序。记住,Person 类实现了Comparable 接口。因此,你应该在实现Comparable 的时候指定Person 对象应该如何排序。

然后,我们peek() 到流中,以了解操作对流的影响,接着使用我们的谓词进行过滤,只接受Person 对象,其firstName 字段超过七个字母。

最后,我们调用findFirst() ,处理findFirst() 操作的Optional 结果。

当我们检查之前使用sorted() 对我们的流操作所做的事情时,我们得到以下输出。

在调用peek() 之后。

Traversing stream with Person named: Ari Patrick
Traversing stream with Person named: Chace Simmons
Traversing stream with Person named: Juliette Cross

在审问了findFirst() 返回的Optional

Person named: Juliette Cross

我们调用findFirst() 的最终结果与之前的另外两次尝试相似,因为我们是以同样的顺序遍历同一个列表。

然而,关于findFirst() 的操作,有些东西开始变得有点意义了。firstName 当这些对象按照升序的字母顺序排序时,它返回了第一个有长条的Person 对象。

firstName 为了进一步说明这一方面,让我们在字母排序相反的情况下,返回第一个具有冗长的Person 对象。

我们不要在people 流上调用一个普通的sorted() 操作,而是使用一个采取自定义Comparator 函数的排序操作。

people.sorted(Comparator.comparing(Person::getFirstName).reversed()) //(1)
         .peek(person -> System.out.printf("Traversing stream with %s\n", person))//(2)
         .filter(x -> x.getFirstName().length() > 7)//(3)
         .findFirst()//(4)
         .ifPresentOrElse(//(5)
             System.out::println,//(5.1)
             () -> System.out.println("No person was found")//(5.2)
);

我们提供一个类似于Person 类提供的Comparator 。唯一的区别是,我们上面实现的那个只使用firstName 字段进行比较。然后它改变了排序顺序,将名字按相反的字母顺序排列--通过Comparator 调用中的reversed() 操作。

使用自定义的sort 操作,我们得到以下输出。

在调用peek():

Traversing stream with Person named: Sawyer Bonilla
Traversing stream with Person named: Paisley Chen
Traversing stream with Person named: Odin Buckley
Traversing stream with Person named: Nia Nolan
Traversing stream with Person named: Madilynn Villa

在询问了findFirst() 返回的Optional 之后。

Person named: Madilynn Villa

所以,你有它。我们对findFirst() 的最新使用充分满足了我们更新的用例。firstName 它从几个可能性的选择中找到了第一个带有冗长的Person

什么时候使用findAny()

有些情况下,你有一个流,但你只想选择一个随机的元素;只要它符合某些条件,而且操作本身需要尽可能短的时间。

因此,鉴于我们正在进行的用例,你可能只想检索一个Person 对象,他有一个冗长的firstName 。这个人的名字是按字母顺序排在第一位还是最后一位,可能也不重要。你只是想找到有一个长名字的人。

这就是findAny() ,效果最好

然而,对于一个普通的尝试(如下面),你可能看不到findFirst()findAny() 之间的任何区别。

people.peek(person -> System.out.printf("Traversing stream with %s\n", person))
        .filter(FindTests::isFirstNameLong)
        .findAny()
        .ifPresentOrElse(
                System.out::println,
                () -> System.out.println("No person was found")
        );

例如,peek() 操作的输出结果是这样的

Traversing stream with Person named: Lailah Glass
Traversing stream with Person named: Juliette Cross

findAny() 后的输出返回

Person named: Juliette Cross

这意味着我们的findAny() 操作只是以顺序的方式遍历了流。然后,它挑选了第一个Person 对象,其firstName 的字母超过7个。

简而言之,它所做的并没有什么特别之处,因为findFirst() 不可能做到这一点。

然而,当你将流并行化时,你会开始注意到findAny() 工作方式的一些变化。因此,在前面的代码中,我们可以在流上添加一个简单的调用parallel() 操作。

people.peek(person -> System.out.printf("Traversing stream with %s\n", person))
        .parallel()
        .filter(FindTests::isFirstNameLong)
        .findAny()
        .ifPresentOrElse(
                System.out::println,
                () -> System.out.println("No person was found")
        );

而当你运行该代码时,你可能会得到一个peek() 的输出

Traversing stream with Person named: Ari Patrick
Traversing stream with Person named: Juliette Cross
Traversing stream with Person named: Sawyer Bonilla
Traversing stream with Person named: Odin Buckley
Traversing stream with Person named: Chace Simmons

随着最终的findAny() 输出为

Person named: Juliette Cross

诚然,由于纯粹的偶然性,这个findAny() 的输出与之前的输出相匹配。但是,你是否注意到,在这种情况下,流检查了更多的元素?而且,遇到的顺序也不是连续的?

另外,如果我们再次运行这段代码,你可能会得到另一个像这样的输出,在peek()

Traversing stream with Person named: Ari Patrick
Traversing stream with Person named: Chace Simmons
Traversing stream with Person named: Sawyer Bonilla
Traversing stream with Person named: Odin Buckley
Traversing stream with Person named: Luz Gallegos
Traversing stream with Person named: Paisley Chen
Traversing stream with Person named: Nia Nolan
Traversing stream with Person named: Madilynn Villa
Traversing stream with Person named: Juliette Cross
Traversing stream with Person named: Lailah Glass

而这里,findAny() 的输出是

Person named: Madilynn Villa

因此,现在不言而喻,findAny() 是如何工作的。它从一个流中选择任何元素,而不考虑任何相遇的顺序。

如果你要处理非常多的元素,那么这实际上是一件好事。这意味着你的代码可能比你按顺序检查元素时更快结束操作,例如。

结论

正如我们所看到的,findFirst()findAny() 操作是Stream API的短路终端操作。它们甚至可以在你用其他中间操作(比如,filter() )遍历整个流之前就终止一个流。

当你处理一个有非常多元素的流时,这种行为是非常重要的。或者,一个有无穷无尽的元素的流。

如果没有这种能力,就意味着你的流操作可能会无限地运行;因此,会导致像StackOverflowError 这样的错误。再次,把这种findFirst()firstAny() 的短路行为看作是解决与设计不良的forwhile 循环有关的可怕的错误,这些循环会无休止地递归。

否则,请记住,findFirst()findAny() 非常适用于不同的使用情况。

当你有一个元素流,其相遇顺序是事先知道的,更喜欢findFirst() 操作。但是,在需要并行化的情况下,你并不关心需要选择哪一个特定的元素,那么就选择findAny()

但要注意不要断章取义地理解 "不在乎你选择哪个元素 "这句话。这句话的意思是,在众多元素中,有几个符合你所设定的条件。然而,你的目的是在这几个元素中选择任何符合你要求的元素。