En las secuencias de Java, ¿el vistazo es realmente solo para la depuración?

137

Estoy leyendo sobre las secuencias de Java y descubriendo cosas nuevas a medida que avanzo. Una de las cosas nuevas que encontré fue la peek()función. Casi todo lo que he leído en el vistazo dice que debería usarse para depurar sus Streams.

¿Qué pasaría si tuviera una transmisión en la que cada cuenta tenga un nombre de usuario, un campo de contraseña y un método de inicio de sesión () e inicio de sesión ()?

tambien tengo

Consumer<Account> login = account -> account.login();

y

Predicate<Account> loggedIn = account -> account.loggedIn();

¿Por qué sería esto tan malo?

List<Account> accounts; //assume it's been setup
List<Account> loggedInAccount = 
accounts.stream()
    .peek(login)
    .filter(loggedIn)
    .collect(Collectors.toList());

Ahora, por lo que puedo decir, esto hace exactamente lo que está destinado a hacer. Eso;

  • Toma una lista de cuentas
  • Intenta iniciar sesión en cada cuenta
  • Filtra cualquier cuenta que no haya iniciado sesión
  • Recopila las cuentas registradas en una nueva lista

¿Cuál es la desventaja de hacer algo como esto? ¿Alguna razón por la que no debería proceder? Por último, si no esta solución, ¿entonces qué?

La versión original de esto utilizó el método .filter () de la siguiente manera;

.filter(account -> {
        account.login();
        return account.loggedIn();
    })
Adam.J
fuente
38
Cada vez que necesito una lambda de varias líneas, muevo las líneas a un método privado y paso la referencia del método en lugar de la lambda.
VGR
1
¿Cuál es la intención? ¿Estás intentando iniciar sesión en todas las cuentas y filtrarlas en función de si están conectadas (lo que puede ser trivialmente cierto)? ¿O desea iniciar sesión y luego filtrarlos en función de si han iniciado sesión o no? Estoy preguntando esto en este orden porque forEachpuede ser la operación que desea en lugar de peek. El hecho de que esté en la API no significa que no esté abierto al abuso (como Optional.of).
Makoto
8
También tenga en cuenta que su código podría ser simplemente .peek(Account::login)y .filter(Account::loggedIn); no hay razón para escribir un consumidor y un predicado que simplemente llame a otro método como ese.
Joshua Taylor
2
También tenga en cuenta que la API de flujo desalienta explícitamente los efectos secundarios en los parámetros de comportamiento .
Didier L
66
Los consumidores útiles siempre tienen efectos secundarios, por supuesto, no se desaniman. En realidad, esto se menciona en la misma sección: “ Un pequeño número de operaciones de transmisión, como forEach()y peek(), solo puede operar a través de efectos secundarios; estos deben usarse con cuidado. ". Mi comentario fue más para recordar que la peekoperación (que está diseñada para fines de depuración) no debe reemplazarse haciendo lo mismo dentro de otra operación como map()o filter().
Didier L

Respuestas:

77

El punto clave de esto:

No use la API de forma no intencionada, incluso si cumple su objetivo inmediato. Ese enfoque puede romperse en el futuro, y tampoco está claro para los futuros mantenedores.


No hay daño en dividir esto en múltiples operaciones, ya que son operaciones distintas. No es malo en el uso de la API de una manera poco clara y no intencional, que puede tener consecuencias si este comportamiento particular, se modifica en futuras versiones de Java.

El uso forEachen esta operación dejaría en claro para el mantenedor que hay un efecto secundario previsto en cada elemento de accounts, y que está realizando alguna operación que puede mutarlo.

También es más convencional en el sentido de que peekes una operación intermedia que no opera en toda la colección hasta que se ejecuta la operación de la terminal, sino que forEaches una operación de la terminal. De esta manera, puede hacer argumentos sólidos sobre el comportamiento y el flujo de su código en lugar de hacer preguntas sobre si peekse comportaría igual forEachque en este contexto.

accounts.forEach(a -> a.login());
List<Account> loggedInAccounts = accounts.stream()
                                         .filter(Account::loggedIn)
                                         .collect(Collectors.toList());
Makoto
fuente
3
Si realiza el inicio de sesión en un paso de preprocesamiento, no necesita una transmisión en absoluto. Puede realizar forEachdirectamente en la colección de origen:accounts.forEach(a -> a.login());
Holger
1
@Holger: Excelente punto. Lo he incorporado a la respuesta.
Makoto
2
@ Adam.J: Correcto, mi respuesta se centró más en la pregunta general contenida en su título, es decir, este método es realmente solo para la depuración, explicando los aspectos de ese método. Esta respuesta está más fusionada en su caso de uso real y cómo hacerlo en su lugar. Por lo tanto, se podría decir que juntos proporcionan la imagen completa. Primero, la razón por la cual este no es el uso previsto, segundo la conclusión, no apegarse a un uso no intencionado y qué hacer en su lugar. Este último tendrá un uso más práctico para usted.
Holger
2
Por supuesto, sería mucho más fácil si el login()método devolviera un booleanvalor que indicara el estado de éxito ...
Holger
3
A eso estaba apuntando. Si login()devuelve a boolean, puede usarlo como un predicado, que es la solución más limpia. Todavía tiene un efecto secundario, pero está bien siempre y cuando no interfiera, es decir, el loginproceso 'de uno Accountno tiene influencia en el proceso' de inicio de sesión 'de otro Account.
Holger
111

Lo importante que debe comprender es que los flujos son controlados por la operación del terminal . La operación del terminal determina si todos los elementos tienen que ser procesados ​​o alguno. Entonces, collectes una operación que procesa cada elemento, mientras que findAnypuede detener el procesamiento de elementos una vez que encuentra un elemento coincidente.

Y es count()posible que no procese ningún elemento cuando pueda determinar el tamaño de la secuencia sin procesar los elementos. Dado que esta es una optimización no realizada en Java 8, pero que sí lo estará en Java 9, puede haber sorpresas cuando cambie a Java 9 y tenga un código que dependa del count()procesamiento de todos los elementos. Esto también está conectado a otros detalles dependientes de la implementación, por ejemplo, incluso en Java 9, la implementación de referencia no podrá predecir el tamaño de una fuente de flujo infinito combinada con limitninguna limitación fundamental que impida dicha predicción.

Ya que peek permite "realizar la acción proporcionada en cada elemento a medida que los elementos se consumen de la secuencia resultante ", no exige el procesamiento de elementos, pero realizará la acción dependiendo de lo que necesite la operación del terminal. Esto implica que debe usarlo con mucho cuidado si necesita un procesamiento en particular, por ejemplo, si desea aplicar una acción en todos los elementos. Funciona si se garantiza que la operación del terminal procesará todos los elementos, pero incluso así, debe asegurarse de que no el próximo desarrollador cambie la operación del terminal (u olvide ese aspecto sutil).

Además, si bien las transmisiones garantizan mantener el orden de encuentro para una determinada combinación de operaciones incluso para transmisiones paralelas, estas garantías no se aplican a peek . Al recopilar en una lista, la lista resultante tendrá el orden correcto para las secuencias paralelas ordenadas, pero la peekacción puede invocarse en un orden arbitrario y al mismo tiempo.

Entonces, lo más útil que puedes hacer con peek es averiguar si se ha procesado un elemento de flujo, que es exactamente lo que dice la documentación de la API:

Este método existe principalmente para admitir la depuración, donde desea ver los elementos a medida que fluyen más allá de cierto punto en una tubería

Holger
fuente
¿Habrá algún problema, futuro o presente, en el caso de uso de OP? ¿Su código siempre hace lo que quiere?
ZhongYu
9
@ bayou.io: por lo que puedo ver, no hay problema en esta forma exacta . Pero como traté de explicar, usarlo de esta manera implica que debe recordar este aspecto, incluso si vuelve al código uno o dos años después para incorporar la "solicitud de función 9876" en el código ...
Holger
1
"la acción de espiar puede invocarse en un orden arbitrario y al mismo tiempo". ¿Esta afirmación no va en contra de su regla de cómo funciona el vistazo, por ejemplo, "a medida que se consumen los elementos"?
Jose Martinez
55
@Jose Martinez: Dice "a medida que los elementos se consumen de la secuencia resultante ", que no es la acción terminal sino el procesamiento, aunque incluso la acción terminal podría consumir elementos fuera de orden siempre que el resultado final sea consistente. Pero también pienso, la frase de la nota API, " ver los elementos a medida que fluyen más allá de cierto punto en una tubería " hace un mejor trabajo al describirlo.
Holger
23

Tal vez una regla general debería ser que si usa un vistazo fuera del escenario de "depuración", solo debe hacerlo si está seguro de cuáles son las condiciones de filtrado final e intermedio. Por ejemplo:

return list.stream().map(foo->foo.getBar())
                    .peek(bar->bar.publish("HELLO"))
                    .collect(Collectors.toList());

parece ser un caso válido donde lo desea, en una operación para transformar todos los Foos en Bars y decirles a todos hola.

Parece más eficiente y elegante que algo como:

List<Bar> bars = list.stream().map(foo->foo.getBar()).collect(Collectors.toList());
bars.forEach(bar->bar.publish("HELLO"));
return bars;

y no terminas iterando una colección dos veces.

quimera8
fuente
4

Diría que peekproporciona la capacidad de descentralizar el código que puede mutar objetos de flujo, o modificar el estado global (basado en ellos), en lugar de incluir todo en una función simple o compuesta que se pasa a un método terminal.

Ahora la pregunta podría ser: ¿deberíamos mutar objetos de flujo o cambiar el estado global desde dentro de las funciones en la programación de Java de estilo funcional ?

Si la respuesta a cualquiera de las anteriores el 2 preguntas es sí (o: en algunos casos sí) a continuación, peek()se ofrecen sin duda no sólo para propósitos de depuración , por la misma razón que forEach()no es sólo para fines de depuración .

Para mí, al elegir entre forEach()y peek(), elegir lo siguiente: ¿Quiero que se adjunten piezas de código que muten los objetos de la secuencia a una composición, o quiero que se adjunten directamente a la secuencia?

Creo que peek()se combinará mejor con los métodos java9. Por ejemplo, es takeWhile()posible que deba decidir cuándo detener la iteración en función de un objeto ya mutado, por lo que emparejarlo forEach()no tendría el mismo efecto.

PD : no he hecho referencia a map()ninguna parte porque en caso de que queramos mutar objetos (o estado global), en lugar de generar nuevos objetos, funciona exactamente igual peek().

Marinos An
fuente
3

Aunque estoy de acuerdo con la mayoría de las respuestas anteriores, tengo un caso en el que usar peek en realidad parece ser el camino más limpio.

Similar a su caso de uso, suponga que desea filtrar solo en cuentas activas y luego iniciar sesión en estas cuentas.

accounts.stream()
    .filter(Account::isActive)
    .peek(login)
    .collect(Collectors.toList());

Peek es útil para evitar la llamada redundante sin tener que repetir la colección dos veces:

accounts.stream()
    .filter(Account::isActive)
    .map(account -> {
        account.login();
        return account;
    })
    .collect(Collectors.toList());
UltimaWeapon
fuente
3
Todo lo que tiene que hacer es acertar con el método de inicio de sesión. Realmente no veo cómo el vistazo es el camino más limpio. ¿Cómo debería alguien que está leyendo su código saber que usted está haciendo un mal uso de la API? Un código bueno y limpio no obliga al lector a hacer suposiciones sobre el código.
kaba713
1

La solución funcional es hacer que el objeto de la cuenta sea inmutable. Entonces account.login () debe devolver un nuevo objeto de cuenta. Esto significará que la operación de mapa se puede usar para iniciar sesión en lugar de echar un vistazo.

Solubris
fuente