Encuentra el primer elemento por predicado


504

Acabo de comenzar a jugar con Java 8 lambdas y estoy tratando de implementar algunas de las cosas a las que estoy acostumbrado en lenguajes funcionales.

Por ejemplo, la mayoría de los lenguajes funcionales tienen algún tipo de función de búsqueda que opera en secuencias o listas que devuelve el primer elemento, para el cual está el predicado true. La única forma en que puedo ver para lograr esto en Java 8 es:

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

Sin embargo, esto me parece ineficiente, ya que el filtro escaneará toda la lista, al menos a mi entender (lo que podría estar mal). ¿Hay una mejor manera?


53
No es ineficiente, la implementación de Java 8 Stream se evalúa de manera diferida, por lo que el filtro se aplica solo a la operación del terminal. La misma pregunta aquí: stackoverflow.com/questions/21219667/stream-and-lazy-evaluation
Marek Gregor

1
Frio. Eso es lo que esperaba que hiciera. Hubiera sido un fracaso de diseño importante de lo contrario.
siki

2
Si su intención es realmente verificar si la lista contiene dicho elemento (no solo el primero de posiblemente varios), .findAny () en teoría puede ser más eficiente en un entorno paralelo y, por supuesto, comunica esa intención más claramente.
Joachim Lous

En comparación con un ciclo simple para cada ciclo, esto crearía muchos objetos en el montón y docenas de llamadas a métodos dinámicos. Si bien esto puede no afectar siempre el resultado final en sus pruebas de rendimiento, en los puntos críticos hace una diferencia abstenerse del uso trivial de Stream y construcciones de peso pesado similares.
Agoston Horvath

Respuestas:


720

No, el filtro no escanea todo el flujo. Es una operación intermedia, que devuelve una secuencia diferida (en realidad, todas las operaciones intermedias devuelven una secuencia diferida). Para convencerte, simplemente puedes hacer la siguiente prueba:

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);

Qué salidas:

will filter 1
will filter 10
10

Verá que solo los dos primeros elementos de la secuencia se procesan realmente.

Entonces puedes seguir con tu enfoque, que está perfectamente bien.


37
Como nota, utilicé get();aquí porque sé qué valores le doy a la tubería de transmisión y, por lo tanto, habrá un resultado. En la práctica, no debe usar get();, pero orElse()/ orElseGet()/ orElseThrow()(para un error más significativo en lugar de un NSEE), ya que es posible que no sepa si las operaciones aplicadas a la tubería de flujo darán como resultado un elemento.
Alexis C.

31
.findFirst().orElse(null);por ejemplo
Gondy

20
No use o Else null. Eso debería ser un antipatrón. Todo está incluido en el Opcional, ¿por qué debería arriesgarse a un NPE? Creo que tratar con Opcional es la mejor manera. Simplemente pruebe el Opcional con isPresent () antes de usarlo.
BeJay

@BeJay no entiendo. ¿Qué debo usar en lugar de orElse?
John Henckel

3
@JohnHenckel Creo que lo que quiere decir BeJay es que debes dejarlo como un Optionaltipo, que es lo que .findFirstregresa. Uno de los usos de Opcional es ayudar a los desarrolladores a evitar tener que lidiar con nullseg en lugar de verificar myObject != null, puede verificar myOptional.isPresent()o usar otras partes de la interfaz Opcional. ¿Eso lo hizo más claro?
AMTerp

102

Sin embargo, esto me parece ineficiente, ya que el filtro escaneará toda la lista

No, no se "romperá" tan pronto como se encuentre el primer elemento que satisfaga el predicado. Puede leer más sobre la pereza en el paquete de transmisión javadoc , en particular (énfasis mío):

Muchas operaciones de flujo, como el filtrado, el mapeo o la eliminación de duplicados, se pueden implementar de manera perezosa, exponiendo oportunidades para la optimización. Por ejemplo, "encontrar la primera cadena con tres vocales consecutivas" no necesita examinar todas las cadenas de entrada. Las operaciones de flujo se dividen en operaciones intermedias (producción de flujo) y operaciones terminales (producción de valor o efecto secundario). Las operaciones intermedias son siempre flojas.


55
Esta respuesta fue más informativa para mí y explica el por qué, no solo cómo. Nunca nuevas operaciones intermedias son siempre perezosas; Las secuencias de Java continúan sorprendiéndome.
kevinarpe

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

Tuve que filtrar solo un objeto de una lista de objetos. Así que usé esto, espero que ayude.


MEJOR: dado que buscamos un valor de retorno booleano, podemos hacerlo mejor agregando verificación nula: return dataSource.getParkingLots (). Stream (). Filter (parkingLot -> Objects.equals (parkingLot.getId (), id)) .findFirst (). orElse (null)! = null;
shreedhar bhat

1
@shreedharbhat No deberías necesitar hacer .orElse(null) != null. En su lugar, utilice las API opcionales, .isPresentes decir .findFirst().isPresent().
AMTerp

@shreedharbhat antes que nada OP no estaba buscando un valor de retorno booleano. En segundo lugar, si lo fueran, sería más limpio escribir.stream().map(ParkingLot::getId).anyMatch(Predicate.isEqual(id))
AjaxLeung

13

Además de la respuesta de Alexis C , si está trabajando con una lista de matriz, en la que no está seguro de si el elemento que está buscando existe, utilícelo.

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

Entonces puedes simplemente verificar si un es null.


1
Deberías arreglar tu ejemplo. No puede asignar nulo a un int simple. stackoverflow.com/questions/2254435/can-an-int-be-null-in-java
RubioRic

He editado tu publicación. 0 (cero) puede ser un resultado válido cuando está buscando en una lista de enteros. Tipo de variable reemplazado por entero y valor predeterminado por nulo.
RubioRic

0

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);
        }
    }
}

0

Respuesta mejorada de One-Liner: si está buscando un valor de retorno booleano, podemos hacerlo mejor agregando isPresent:

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

Si desea un valor de retorno booleano, debe usar anyMatch
AjaxLeung
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.