¿Cuándo debo usar las transmisiones?

99

Me encontré con una pregunta al usar un Listy su stream()método. Si bien sé cómo usarlos, no estoy muy seguro de cuándo usarlos.

Por ejemplo, tengo una lista que contiene varias rutas a diferentes ubicaciones. Ahora, me gustaría comprobar si una única ruta determinada contiene alguna de las rutas especificadas en la lista. Me gustaría devolver un en booleanfunción de si se cumplió o no la condición.

Esto, por supuesto, no es una tarea difícil en sí misma. Pero me pregunto si debería usar streams o un bucle for (-each).

La lista

private static final List<String> EXCLUDE_PATHS = Arrays.asList(new String[]{
    "my/path/one",
    "my/path/two"
});

Ejemplo: Stream

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream()
                        .map(String::toLowerCase)
                        .filter(path::contains)
                        .collect(Collectors.toList())
                        .size() > 0;
}

Ejemplo: bucle para cada uno

private boolean isExcluded(String path){
    for (String excludePath : EXCLUDE_PATHS) {
        if(path.contains(excludePath.toLowerCase())){
            return true;
        }
    }
    return false;
}

Tenga en cuenta que el pathparámetro siempre está en minúsculas .

Mi primera suposición es que el enfoque para cada uno es más rápido, porque el ciclo volvería inmediatamente, si se cumple la condición. Mientras que la secuencia seguiría recorriendo todas las entradas de la lista para completar el filtrado.

¿Es correcta mi suposición? Si es así, ¿ por qué (o más bien cuándo ) usaría stream()entonces?

mcuenez
fuente
11
Las transmisiones son más expresivas y legibles que los bucles for tradicionales. En el último, debe tener cuidado con los elementos intrínsecos de if-then y condiciones, etc. La expresión de la secuencia es muy clara: convierta los nombres de archivo a minúsculas, luego filtre por algo y luego cuente, recopile, etc. el resultado: un muy iterativo expresión del flujo de cálculos.
Jean-Baptiste Yunès
12
No hay necesidad de new String[]{…}aquí. Solo useArrays.asList("my/path/one", "my/path/two")
Holger
4
Si su fuente es un String[], no es necesario llamar Arrays.asList. Puede simplemente transmitir sobre la matriz usando Arrays.stream(array). Por cierto, tengo dificultades para comprender isExcludedpor completo el propósito de la prueba. ¿Es realmente interesante si un elemento de EXCLUDE_PATHSestá literalmente contenido en algún lugar dentro de la ruta? Es decir isExcluded("my/path/one/foo/bar/baz"), volveré true, así como isExcluded("foo/bar/baz/my/path/one/")
Holger
3
Genial, no conocía el Arrays.streammétodo, gracias por señalarlo. De hecho, el ejemplo que publiqué parece bastante inútil para cualquier otra persona además de mí. Soy consciente del comportamiento del isExcludedmétodo, pero en realidad es algo que necesito para mí, por lo tanto, para responder a su pregunta: , es interesante por razones que me gustaría no mencionar, ya que no encajaría en el alcance. de la pregunta original.
mcuenez
1
¿Por qué se toLowerCaseaplica a la constante que ya es minúscula? ¿No debería aplicarse al pathargumento?
Sebastian Redl

Respuestas:

78

Tu suposición es correcta. La implementación de su transmisión es más lenta que el bucle for.

Sin embargo, este uso de transmisión debe ser tan rápido como el bucle for:

EXCLUDE_PATHS.stream()  
                               .map(String::toLowerCase)
                               .anyMatch(path::contains);

Esto itera a través de los elementos, aplicando String::toLowerCaseun filtro a los elementos uno por uno y terminando en el primer elemento que coincide.

Ambos collect()& anyMatch()son operaciones terminales. anyMatch()sin embargo, sale en el primer elemento encontrado, mientras que collect()requiere que se procesen todos los elementos.

Stefan Pries
fuente
2
Impresionante, no sabía findFirst()en combinación con filter(). Aparentemente, no sé cómo usar las transmisiones tan bien como pensaba.
mcuenez
4
Hay algunos artículos de blog y presentaciones en la web realmente interesantes sobre el rendimiento de la API de transmisión, que encontré muy útil para comprender cómo funciona esto bajo el capó. Definitivamente puedo recomendar que investigue un poco, si está interesado en eso.
Stefan Pries
Después de su edición, siento que su respuesta es la que debería ser aceptada, ya que también respondió a mi pregunta en los comentarios de la otra respuesta. Sin embargo, me gustaría darle algo de crédito a @ rvit34 por publicar el código :-)
mcuenez
34

La decisión de utilizar Streams o no no debe basarse en la consideración del rendimiento, sino más bien en la legibilidad. Cuando se trata realmente de rendimiento, hay otras consideraciones.

Con su .filter(path::contains).collect(Collectors.toList()).size() > 0enfoque, está procesando todos los elementos y reuniéndolos en un temporal List, antes de comparar el tamaño, aún así, esto casi nunca importa para un Stream que consta de dos elementos.

El uso .map(String::toLowerCase).anyMatch(path::contains)puede ahorrar ciclos de CPU y memoria, si tiene una cantidad sustancialmente mayor de elementos. Aún así, esto convierte cada uno Stringa su representación en minúsculas, hasta que se encuentra una coincidencia. Obviamente, tiene sentido usar

private static final List<String> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .collect(Collectors.toList());

private boolean isExcluded(String path) {
    return EXCLUDE_PATHS.stream().anyMatch(path::contains);
}

en lugar. Por lo tanto, no tiene que repetir la conversión a minúsculas en cada invocación de isExcluded. Si el número de elementos EXCLUDE_PATHSo la longitud de las cadenas se vuelve muy grande, puede considerar usar

private static final List<Predicate<String>> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .map(s -> Pattern.compile(s, Pattern.LITERAL).asPredicate())
          .collect(Collectors.toList());

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream().anyMatch(p -> p.test(path));
}

Compilar una cadena como patrón de expresiones regulares con la LITERALbandera, hace que se comporte como operaciones de cadena ordinarias, pero permite que el motor dedique algún tiempo a la preparación, por ejemplo, utilizando el algoritmo de Boyer Moore, para ser más eficiente cuando se trata de la comparación real.

Por supuesto, esto solo vale la pena si hay suficientes pruebas posteriores para compensar el tiempo dedicado a la preparación. Determinar si este será el caso, es una de las consideraciones reales de desempeño, además de la primera pregunta de si esta operación alguna vez será crítica para el desempeño. No es la cuestión de si usar Streams o forLoops.

Por cierto, los ejemplos de código anteriores mantienen la lógica de su código original, que me parece cuestionable. Su isExcludedmétodo devuelve true, si la ruta especificada contiene alguno de los elementos de la lista, por lo que devuelve truepara /some/prefix/to/my/path/one, así como my/path/one/and/some/suffixo incluso /some/prefix/to/my/path/one/and/some/suffix.

Incluso dummy/path/onerousse considera que cumple los criterios ya que containsla cadena my/path/one...

Holger
fuente
Buenas ideas sobre la posible optimización del rendimiento, gracias. Con respecto a la última parte de su respuesta: si mi respuesta a su comentario no fue satisfactoria, considere mi código de ejemplo como una mera ayuda para que otros entiendan lo que estoy preguntando, en lugar de ser un código real. Además, siempre puede editar la pregunta, si tiene un mejor ejemplo en mente.
mcuenez
3
Acepto tu comentario de que esta operación es la que realmente quieres, por lo que no es necesario cambiarla. Solo guardaré la última sección para los futuros lectores, para que sean conscientes de que esta no es una operación típica, pero también, que ya se ha discutido y no necesita más comentarios ...
Holger
En realidad, las transmisiones son perfectas para optimizar la memoria cuando la cantidad de memoria de trabajo
supera el
21

Si. Tienes razón. Su enfoque de corriente tendrá algunos gastos generales. Pero puedes usar tal construcción:

private boolean isExcluded(String path) {
    return  EXCLUDE_PATHS.stream().map(String::toLowerCase).anyMatch(path::contains);
}

La razón principal para usar streams es que hacen que su código sea más simple y fácil de leer.

rvit34
fuente
3
¿Es anyMatchun atajo para filter(...).findFirst().isPresent()?
mcuenez
6
¡Sí lo es! Eso es incluso mejor que mi primera sugerencia.
Stefan Pries
8

El objetivo de las secuencias en Java es simplificar la complejidad de escribir código paralelo. Está inspirado en la programación funcional. El flujo en serie es solo para limpiar el código.

Si queremos rendimiento, deberíamos usar paraleloStream, que fue diseñado para. El serial, en general, es más lento.

Hay un buen artículo para leer acerca de , y rendimiento . ForLoopStreamParallelStream

En su código, podemos utilizar métodos de terminación para detener la búsqueda en la primera coincidencia. (anyMatch ...)

Paulo Ricardo Almeida
fuente
5
Tenga en cuenta que para transmisiones pequeñas y en algunos otros casos, una transmisión paralela puede ser más lenta debido al costo de inicio. Y si tiene una operación de terminal ordenada, en lugar de una no ordenada en paralelo, resincronización al final.
CAD97
0

Como otros han mencionado muchos puntos buenos, solo quiero mencionar la evaluación perezosa en la evaluación de transmisiones. Cuando lo hacemos map()para crear una secuencia de rutas en minúsculas, no estamos creando la secuencia completa inmediatamente, sino que la secuencia se construye de forma perezosa , por lo que el rendimiento debería ser equivalente al bucle for tradicional. No está haciendo un escaneo completo map()y anyMatch()se ejecutan al mismo tiempo. Una vez que anyMatch()devuelve verdadero, se cortocircuitará.

Kaicheng Hu
fuente