Estou lendo sobre fluxos Java e descobrindo coisas novas à medida que avança. Uma das coisas novas que encontrei foi a peek()
função. Quase tudo o que li na espiada diz que deve ser usado para depurar seus Streams.
E se eu tivesse um Stream em que cada Conta tivesse um nome de usuário, um campo de senha e um método de login () e logIn ().
eu também tenho
Consumer<Account> login = account -> account.login();
e
Predicate<Account> loggedIn = account -> account.loggedIn();
Por que isso seria tão ruim?
List<Account> accounts; //assume it's been setup
List<Account> loggedInAccount =
accounts.stream()
.peek(login)
.filter(loggedIn)
.collect(Collectors.toList());
Agora, até onde sei, isso faz exatamente o que se pretende fazer. Isto;
- Leva uma lista de contas
- Tenta fazer login em cada conta
- Filtra qualquer conta que não esteja logada
- Coleta as contas conectadas em uma nova lista
Qual é a desvantagem de fazer algo assim? Algum motivo para eu não prosseguir? Por fim, se não esta solução, então o que?
A versão original disso usava o método .filter () da seguinte maneira;
.filter(account -> {
account.login();
return account.loggedIn();
})
java
java-8
java-stream
peek
Adam.J
fonte
fonte
forEach
pode ser a operação que você deseja, ao contráriopeek
. Só porque está na API, não significa que não esteja aberto a abusos (comoOptional.of
)..peek(Account::login)
e.filter(Account::loggedIn)
; não há razão para escrever um Consumidor e Predicado que apenas chame outro método como esse.forEach()
epeek()
, pode operar apenas com efeitos colaterais; estes devem ser usados com cuidado. ”. Minha observação foi mais para lembrar que apeek
operação (projetada para fins de depuração) não deve ser substituída fazendo a mesma coisa dentro de outra operação comomap()
oufilter()
.Respostas:
A principal conclusão disso:
Não use a API de maneira não intencional, mesmo que ela atinja seu objetivo imediato. Essa abordagem pode quebrar no futuro e também não está clara para futuros mantenedores.
Não há mal nenhum em dividir isso em várias operações, pois são operações distintas. Não é mal em usar a API de forma pouco clara e não intencional, o que pode ter ramificações se este comportamento particular é modificado em futuras versões do Java.
O uso
forEach
dessa operação deixaria claro para o mantenedor que existe um efeito colateral pretendido em cada elemento deaccounts
e que você está executando alguma operação que pode modificá-lo.Também é mais convencional no sentido de que
peek
é uma operação intermediária que não opera em toda a coleção até que a operação do terminal seja executada, masforEach
é de fato uma operação do terminal. Dessa forma, você pode argumentar fortemente sobre o comportamento e o fluxo do seu código, em vez de fazer perguntas sobre sepeek
o mesmo se comportariaforEach
nesse contexto.fonte
forEach
diretamente na coleção de fontes:accounts.forEach(a -> a.login());
login()
método retornou umboolean
valor que indica o status de sucesso ...login()
retornar aboolean
, você poderá usá-lo como predicado, que é a solução mais limpa. Ainda tem um efeito colateral, mas tudo bem, desde que não interfira, ou seja, o processologin
`de umAccount
não influencia o processo de login 'de outroAccount
.O importante que você precisa entender é que os fluxos são acionados pela operação do terminal . A operação do terminal determina se todos os elementos devem ser processados ou algum. O mesmo
collect
acontece com uma operação que processa cada item, enquantofindAny
pode parar de processar itens depois de encontrar um elemento correspondente.E
count()
pode não processar nenhum elemento quando pode determinar o tamanho do fluxo sem processar os itens. Como essa é uma otimização não feita no Java 8, mas que será no Java 9, pode haver surpresas quando você alterna para o Java 9 e possui um código que depende docount()
processamento de todos os itens. Isso também está conectado a outros detalhes dependentes da implementação, por exemplo, mesmo no Java 9, a implementação de referência não poderá prever o tamanho de uma fonte de fluxo infinita combinada,limit
embora não exista uma limitação fundamental que impeça essa previsão.Como
peek
permite “executar a ação fornecida em cada elemento à medida que os elementos são consumidos no fluxo resultante ”, ele não exige o processamento de elementos, mas executará a ação dependendo do que a operação do terminal precisar. Isso implica que você deve usá-lo com muito cuidado se precisar de um processamento específico, por exemplo, deseja aplicar uma ação em todos os elementos. Funciona se é garantido que a operação do terminal processa todos os itens, mas mesmo assim, você deve ter certeza de que nem o próximo desenvolvedor alterará a operação do terminal (ou você esquecerá esse aspecto sutil).Além disso, enquanto os fluxos garantem manter a ordem de encontro para uma certa combinação de operações, mesmo para fluxos paralelos, essas garantias não se aplicam
peek
. Ao coletar em uma lista, a lista resultante terá a ordem correta para fluxos paralelos ordenados, mas apeek
ação pode ser invocada em uma ordem arbitrária e simultaneamente.Portanto, a coisa mais útil com a qual você pode fazer
peek
é descobrir se um elemento de fluxo foi processado, exatamente o que a documentação da API diz:fonte
Talvez uma regra prática seja que, se você usar a espiada fora do cenário "debug", deverá fazê-lo apenas se tiver certeza de quais são as condições de filtragem intermediária e final. Por exemplo:
parece ser um caso válido em que você deseja, em uma operação, transformar todos os Foos em barras e dizer a todos.
Parece mais eficiente e elegante do que algo como:
e você não itera duas vezes uma coleção.
fonte
Eu diria que
peek
fornece a capacidade de descentralizar código que pode alterar objetos de fluxo ou modificar o estado global (com base neles), em vez de colocar tudo em uma função simples ou composta passada para um método terminal.Agora, a pergunta pode ser: devemos alterar objetos de fluxo ou alterar o estado global de dentro das funções na programação java de estilo funcional ?
Se a resposta a qualquer um dos acima de 2 perguntas é sim (ou: em alguns casos, sim), então
peek()
é definitivamente não só para fins de depuração , pela mesma razão queforEach()
não é apenas para fins de depuração .Para mim, ao escolher entre
forEach()
epeek()
, é o seguinte: Desejo que partes do código que modificam objetos de fluxo sejam anexadas a um composable ou que elas sejam anexadas diretamente ao fluxo?Eu acho que
peek()
será melhor emparelhar com métodos java9. por exemplo,takeWhile()
pode ser necessário decidir quando parar a iteração com base em um objeto já mutado, portanto, compará-loforEach()
não teria o mesmo efeito.PS: Eu não mencionei em
map()
nenhum lugar porque, no caso de querermos modificar objetos (ou estado global), em vez de gerar novos objetos, ele funciona exatamente comopeek()
.fonte
Embora eu concorde com a maioria das respostas acima, tenho um caso em que o uso de espiar parece realmente o caminho mais limpo a seguir.
Semelhante ao seu caso de uso, suponha que você queira filtrar apenas em contas ativas e, em seguida, efetue um logon nessas contas.
A espiada é útil para evitar a chamada redundante sem precisar repetir a coleção duas vezes:
fonte
A solução funcional é tornar o objeto de conta imutável. Portanto, account.login () deve retornar um novo objeto de conta. Isso significa que a operação do mapa pode ser usada para login, em vez de espiar.
fonte