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?

siki
fuente
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.

Alexis C.
fuente
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.

lo que sea
fuente
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.

CodeShadow
fuente
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.

Ifesinachi Bryan
fuente
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);
        }
    }
}
ausiones
fuente
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();
shreedhar bhat
fuente
Si desea un valor de retorno booleano, debe usar anyMatch
AjaxLeung