¿En las secuencias de Java, el vistazo es realmente solo para depurar?
Estoy leyendo sobre transmisiones de Java y descubro 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 dice que debería usarse para depurar tus Streams.
¿Qué pasaría si tuviera una secuencia donde cada cuenta tiene un nombre de usuario, un campo de contraseña y un método de inicio de sesión() y de 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 pretende hacer. Él;
- Toma una lista de cuentas.
- Intenta iniciar sesión en cada cuenta.
- Filtra cualquier cuenta que no haya iniciado sesión
- Recopila las cuentas iniciadas en una nueva lista
¿Cuál es la desventaja de hacer algo como esto? ¿Alguna razón por la que no debería continuar? Por último, si no es esta solución, ¿qué?
La versión original de esto usaba el método .filter() de la siguiente manera;
.filter(account -> {
account.login();
return account.loggedIn();
})
Lo importante que debe comprender es que las transmisiones son impulsadas por la operación del terminal . La operación del terminal determina si se deben procesar todos los elementos o alguno. También lo collect
es una operación que procesa cada elemento, mientras que findAny
puede detener el procesamiento de elementos una vez que encuentra un elemento coincidente.
Y count()
puede que no procese ningún elemento en absoluto cuando puede determinar el tamaño de la secuencia sin procesar los elementos. Dado que se trata de una optimización que no se realizó en Java 8, pero que sí se realizará 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á relacionado con otros detalles que dependen 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 infinita combinada con limit
mientras no exista una limitación fundamental que impida dicha predicción.
Dado que peek
permite "realizar la acción proporcionada en cada elemento a medida que los elementos se consumen del flujo 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 particular, por ejemplo, si desea aplicar una acción a todos los elementos. Funciona si se garantiza que la operación del terminal procese todos los elementos, pero incluso entonces, debe asegurarse de que el próximo desarrollador no cambie la operación del terminal (o se olvidará de ese aspecto sutil).
Además, si bien los flujos garantizan mantener el orden de encuentro para una determinada combinación de operaciones incluso para flujos paralelos, estas garantías no se aplican a peek
. Al recopilar en una lista, la lista resultante tendrá el orden correcto para flujos paralelos ordenados, pero la peek
acción puede invocarse en un orden arbitrario y al mismo tiempo.
Entonces, lo más útil que puedes hacer peek
es averiguar si un elemento de flujo ha sido procesado, 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 un cierto punto en una tubería.
La conclusión clave de esto:
No utilice la API de forma no deseada, incluso si logra su objetivo inmediato. Ese enfoque puede fallar en el futuro y tampoco está claro para los futuros mantenedores.
No hay nada de malo en dividir esto en múltiples operaciones, ya que son operaciones distintas. Es perjudicial utilizar la API de forma poco clara y no intencionada, lo que puede tener ramificaciones si este comportamiento particular se modifica en futuras versiones de Java.
El uso forEach
de esta operación le dejaría claro al mantenedor que existe un efecto secundario previstoaccounts
en cada elemento de y que está realizando alguna operación que puede mutarlo.
También es más convencional en el sentido de que peek
es una operación intermedia que no opera en toda la colección hasta que se ejecuta la operación terminal, pero forEach
de hecho es una operación terminal. De esta manera, puede presentar argumentos sólidos sobre el comportamiento y el flujo de su código en lugar de hacer preguntas sobre si peek
se comportaría igual que forEach
en este contexto.
accounts.forEach(a -> a.login());
List<Account> loggedInAccounts = accounts.stream()
.filter(Account::loggedIn)
.collect(Collectors.toList());
Quizás una regla general debería ser que si usa el vistazo fuera del escenario de "depuración", sólo debería hacerlo si está seguro de cuáles son las condiciones de filtrado intermedias y de terminación. Por ejemplo:
return list.stream().map(foo->foo.getBar())
.peek(bar->bar.publish("HELLO"))
.collect(Collectors.toList());
Parece ser un caso válido en el que desea, en una sola operación, transformar todos los Foos en Bars y saludarlos a todos.
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.