Java 8 Streams: Guía definitiva de findFirst() y findAny()

En esta guía detallada, veremos cómo usar los métodos findFirst() y findAny() de la API Stream de Java 8, cómo se usan y algunas de las mejores prácticas.

Introducción

Los métodos findFirst() y findAny() son operaciones terminales (terminar y devolver resultados) de Stream API. Sin embargo, tienen algo especial: no solo terminan una transmisión, sino que también la cortocircuitan:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 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);
1
2
Janette
Chris

Entonces, ¿cuál es la diferencia entre estos dos y cómo los usa de manera eficiente?

En esta guía, profundizaremos y exploraremos los métodos findFirst() y findAny() en Java, así como sus aplicaciones y mejores prácticas.

Terminal y ¿Cortocircuito?

Otra operación de terminal comúnmente utilizada es el método para cada(), pero sigue siendo fundamentalmente diferente, además de ser una operación diferente.

Para tener una idea de por qué las operaciones findFirst() y findAny() difieren de otras funciones de terminal como forEach(), suponga que tiene un flujo con un número infinito de elementos.

Cuando llamas a forEach() en tal flujo, la operación atravesará todos los elementos en ese flujo.

Para un número infinito de elementos, su llamada forEach() tardará una cantidad infinita de tiempo en terminar de procesarse.

Sin embargo, findFirst() y findAny() no tienen que verificar todos los elementos en un flujo y cortocircuitar tan pronto como encuentran un elemento que están buscando. Entonces, si los llama desde un flujo infinito, terminarán ese flujo tan pronto como encuentren lo que les indicó.

Eso sugiere que estas dos operaciones siempre concluirán en tiempo finito.

{.icon aria-hidden=“true”}

Nota: Vale la pena señalar que cortocircuitarán las operaciones intermedias, como el método filter() durante la ejecución, ya que simplemente no hay necesidad de filtrar más si hay una coincidencia. es encontrado.

Las operaciones findFirst() y findAny() son muy necesarias cuando desea salir del procesamiento de secuencias que podría ejecutarse sin fin. Como analogía, considere estas dos operaciones como similares a lo que puede hacer para eliminar un bucle clásico ‘while’ o ‘for’ cuya recursividad es infinita.

Esta guía explorará cómo funcionan estas dos operaciones en detalle. Primero, comenzaremos con sus definiciones oficiales. En segundo lugar, los aplicaremos a casos de uso simples. Luego, interrogaremos sus intrincadas diferencias.

Finalmente, usaremos estos hallazgos para determinar la mejor manera de usarlos en casos de uso más exigentes; especialmente aquellos que exigen un diseño de código cuidadoso para mejorar la velocidad de procesamiento.

findFirst() y findAny() Definiciones

findFirst() y findAny() devuelven valores; no devuelven instancias de flujos como lo hacen las operaciones intermedias como forEach() o filter().

Sin embargo, los valores que devuelven findFirst() y findAny() son siempre del tipo Optional<T>.

If you'd like to read more about Optionals, read our Guía de Opcionales en Java 8.

Un opcional es un:

[...] objeto contenedor que puede contener o no un valor no nulo.

[Credit: Documentación de Java 8]{.small}

Eso es todo para decir: la operación find de estos devuelve un valor nulo seguro, en caso de que el valor no esté presente en la transmisión.

El método findFirst() devuelve el primer elemento de una secuencia o un Opcional vacío. Si la transmisión no tiene un orden de encuentro, se devuelve cualquier elemento, ya que es ambiguo cuál es el primero de todos modos.

El método findAny() devuelve cualquier elemento de la secuencia - muy parecido a findFirst() sin orden de encuentro.

Casos de uso de findFirst() y findAny()

Echemos un vistazo a algunos casos de uso de estos métodos y cuándo podría preferir uno sobre el otro. Dado que los ejemplos con Strings normalmente no se vuelven complejos, supongamos que tiene un flujo de objetos Person:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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")
);

Donde una Persona es:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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);
    }
}

El comparador compara a las personas usando sus campos firstName y luego por sus campos lastName.

Y desea saber qué persona tiene un nombre de pila bastante largo. Dicho esto, es posible que desee encontrar una persona con un nombre largo o la primera persona con un nombre largo.

Digamos que cualquier nombre con más de 7 letras es un nombre largo:

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

Usando el flujo Person, filtremos los objetos usando el predicado isFirstNameLong() y busquemos una persona:

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

La primera línea filtra el flujo de personas y devuelve un nuevo flujo que contiene solo los objetos Person cuyo firstName tiene más de siete letras.

If you'd like to read more about the filter() method, read our Java 8 Streams: Guía del método filter().

La segunda línea finaliza el flujo si la operación findFirst() encuentra un firstName con más de siete letras.

La tercera línea interroga a Optional<Person> que devuelven las operaciones findFirst(). Por lo que, puede (o no) contener una ‘Persona’ con un nombre largo:

  1. Si Opcional contiene una Persona con un nombre largo, imprima sus detalles en la consola.
  2. Si no, imprima un mensaje: "No se encontró ninguna persona."

Por lo tanto, cuando ejecute el código anterior, obtendrá el resultado:

1
Person named: Juliette Cross

Ahora, intentemos implementar este caso de uso con la operación findAny() en su lugar. Esto es tan fácil como simplemente cambiar la llamada findFirst() anterior con findAny():

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

Sin embargo, cuando ejecutamos el código, obtenemos el mismo resultado, incluso si ejecuta el código varias veces:

1
Person named: Juliette Cross

¿Lo que da?

Bueno, ambos cortocircuitan la operación filter() tan pronto como se encuentra la Persona con el nombre "Juliette Cross", por lo que se devuelve el mismo resultado. El método findAny() no puede elegir entre ella y otras personas, ya que nadie después de ella es admitido en la transmisión.

Este resultado indica que no estamos explotando las capacidades de findFirst() y findAny() completamente con esta configuración. Echemos un vistazo a cómo podemos cambiar el entorno de estos métodos para recuperar los resultados que esperábamos.

Elegir entre findFirst() y findAny() {#choosing between findfirstandfindany}

La inclusión del término "first" en la operación findFirst() implica que hay un orden particular de elementos y que solo te interesa el elemento que está en la primera posición.

Como se insinuó anteriormente, estos métodos son los mismos dependiendo de si inicia su transmisión con orden de encuentro o no.

Ambos actúan como findAny() si no hay orden, y ambos actúan como findFirst() si hay orden.

Entonces, revisemos el caso de uso para mejorar el enfoque para diseñar la solución. Necesitábamos encontrar una Persona con un nombre largo; uno que tiene más de siete letras.

Por lo tanto, debemos elaborar nuestro requisito aún más para buscar no solo un ‘firstName’ largo, sino también un nombre que aparece primero cuando esos nombres largos están ordenados.

De esa manera, cambiaríamos el código para que se lea como:

1
2
3
4
5
6
7
8
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)
 );

Con este fragmento de código, hemos agregado dos pasos más en comparación con el fragmento anterior.

Primero, ordenamos los objetos Persona usando su orden natural. Recuerde, la clase Person implementa la interfaz Comparable. Por lo tanto, debe especificar cómo deben ordenarse los objetos Persona a medida que implementa Comparable.

If you'd like to read more about sorting with Streams, read our Java 8 – Cómo usar Stream.sorted()

Luego, “echamos un vistazo ()” a la transmisión para obtener una idea de lo que las operaciones están haciendo en la transmisión, seguido de un filtrado usando nuestro predicado que solo acepta objetos “Persona” cuyos campos “nombre” tienen más de siete letras.

Finalmente, llamamos a findFirst() y manejamos el resultado Opcional de la operación findFirst().

Cuando examinamos qué hizo el uso de ordenado () a nuestra manipulación de flujo anterior, obtenemos los siguientes resultados.

Después de llamar a peek():

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

Después de interrogar a Opcional que findFirst() devolvió:

1
Person named: Juliette Cross

El resultado final de nuestra llamada findFirst() es similar a los otros dos intentos anteriores, ya que estamos recorriendo la misma lista con el mismo orden.

Sin embargo, algo está empezando a tener un poco más de sentido sobre la operación findFirst(). Devolvía el primer objeto Person que tenía un firstName largo cuando esos objetos se ordenaban en orden alfabético ascendente.

Para ilustrar aún más ese aspecto, devolvamos el primer objeto Person con un firstName largo cuando el orden alfabético es inverso.

En lugar de llamar a una simple operación sorted() en el flujo de personas, usemos una operación de clasificación que tome una función Comparator personalizada:

1
2
3
4
5
6
7
8
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)
);

Proporcionamos un ‘Comparador’ que es similar al que proporciona la clase ‘Persona’. Las únicas diferencias son que la que hemos implementado arriba usa solo el campo firstName para la comparación. Luego cambia el orden de clasificación para organizar los nombres en orden alfabético inverso, a través de la operación reversed() en la llamada Comparator.

Usando la operación sort personalizada, obtenemos los siguientes resultados.

Después de llamar a peek():

1
2
3
4
5
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

Después de interrogar a Opcional que findFirst() devolvió:

1
Person named: Madilynn Villa

Ahí lo tienes. Nuestro último uso de findFirst() sirve adecuadamente a nuestro caso de uso actualizado. Encontró la primera ‘Persona’ con un ’nombre’ largo de una selección de varias posibilidades.

¿Cuándo usar findAny()?

Hay instancias en las que tiene una transmisión, pero solo desea seleccionar un elemento aleatorio; siempre que cumpla ciertas condiciones y la operación en sí tome el menor tiempo posible.

Por lo tanto, dado nuestro caso de uso en curso, es posible que solo desee recuperar un objeto Person que tenga un firstName extenso. También puede no importar si el nombre de esa persona aparece primero en orden alfabético o al final. Simplemente desea encontrar a alguien que tenga un nombre largo.

Aquí es donde findAny() funciona mejor.

Sin embargo, con un intento simple (como el siguiente) es posible que no vea ninguna diferencia entre findFirst() y findAny():

1
2
3
4
5
6
7
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")
        );

La salida de la operación peek(), por ejemplo, devuelve esto:

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

Y la salida después de findAny() devuelve:

1
Person named: Juliette Cross

Esto significa que nuestra operación findAny() simplemente atravesó el flujo de manera secuencial. Luego, escogió el primer objeto ‘Persona’ cuyo ’nombre’ tiene más de siete letras.

No hay nada especial que hizo que findFirst() no podría haber hecho, en resumen.

Sin embargo, cuando paralelizas la transmisión, comenzarás a notar algunos cambios en la forma en que funciona findAny(). Entonces, en el código anterior, podríamos agregar una simple llamada a la operación parallel() en la transmisión:

1
2
3
4
5
6
7
8
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")
        );

Y cuando ejecuta el código, puede obtener un resultado peek() como:

1
2
3
4
5
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

Con una eventual salida findAny() de:

1
Person named: Juliette Cross

Cierto, la salida de este findAny() coincide con la anterior debido a la pura casualidad. Pero, ¿se dio cuenta de que la transmisión en este caso verificó más elementos? ¿Y el orden del encuentro no fue secuencial?

Además, si volvemos a ejecutar el código, es posible que obtengas otro resultado como este después de peek():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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

Y aquí, la salida findAny() es:

1
Person named: Madilynn Villa

Por lo tanto, ahora es evidente cómo funciona findAny(). Selecciona cualquier elemento de un flujo sin tener en cuenta ningún orden de encuentro.

Si estabas lidiando con una gran cantidad de elementos, entonces esto es realmente algo bueno. Significa que su código puede terminar de operar antes que cuando verificaría los elementos en un orden secuencial, por ejemplo.

Conclusión

Como hemos visto, las operaciones findFirst() y findAny() son operaciones de terminal de cortocircuito de Stream API. Pueden terminar una secuencia incluso antes de que pueda atravesarla por completo con otras operaciones intermedias (como, filter()).

Este comportamiento es muy importante cuando maneja un flujo que tiene muchos elementos. O bien, una secuencia que tiene un número infinito de elementos.

Sin tal capacidad, significa que sus operaciones de transmisión pueden ejecutarse infinitamente; por lo tanto, provoca errores como StackOverflowError. Una vez más, piense en este comportamiento de cortocircuito findFirst() y firstAny() como uno que soluciona los temidos errores asociados con bucles for y while mal diseñados que se repiten sin fin.

De lo contrario, tenga en cuenta que findFirst() y findAny() se adaptan bien a diferentes casos de uso.

Cuando tiene un flujo de elementos cuyo orden de encuentro se conoce de antemano, prefiera la operación findFirst(). Pero, en el caso de que se necesite la paralelización y no le importe qué elemento en particular necesita seleccionar, elija findAny().

Sin embargo, tenga cuidado de no tomar la frase "no me importa qué elemento seleccione" fuera de contexto. La frase implica que de un flujo de elementos, unos pocos cumplen las condiciones que ha establecido. Sin embargo, su objetivo es seleccionar cualquier elemento de esos pocos que cumplan con sus requisitos.

El código utilizado en el artículo está disponible en GitHub.

Licensed under CC BY-NC-SA 4.0