Em fluxos Java, é realmente espiada apenas para depuração?

137

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();
    })
Adam.J
fonte
38
Sempre que me vejo precisando de um lambda com várias linhas, movo as linhas para um método privado e passo a referência do método em vez do lambda.
VGR 10/11
1
Qual é o objetivo - você está tentando fazer logon de todas as contas e filtrá-las com base em se elas estão conectadas (o que pode ser trivialmente verdadeiro)? Ou você deseja fazer login e filtrá-los com base no fato de terem ou não feito login? Estou perguntando isso nesta ordem, porque forEachpode ser a operação que você deseja, ao contrário peek. Só porque está na API, não significa que não esteja aberto a abusos (como Optional.of).
Makoto
8
Observe também que seu código pode ser apenas .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.
Joshua Taylor
2
Observe também que a API do fluxo desencoraja explicitamente os efeitos colaterais nos parâmetros comportamentais .
Didier L
6
Consumidores úteis sempre têm efeitos colaterais, eles não são desencorajados, é claro. Na verdade, isso é mencionado na mesma seção: “ Um pequeno número de operações de fluxo, como forEach()e peek(), pode operar apenas com efeitos colaterais; estes devem ser usados ​​com cuidado. ”. Minha observação foi mais para lembrar que a peekoperação (projetada para fins de depuração) não deve ser substituída fazendo a mesma coisa dentro de outra operação como map()ou filter().
Didier L

Respostas:

77

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 forEachdessa operação deixaria claro para o mantenedor que existe um efeito colateral pretendido em cada elemento de accountse 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, mas forEaché 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 se peeko mesmo se comportaria forEachnesse contexto.

accounts.forEach(a -> a.login());
List<Account> loggedInAccounts = accounts.stream()
                                         .filter(Account::loggedIn)
                                         .collect(Collectors.toList());
Makoto
fonte
3
Se você realizar o logon em uma etapa de pré-processamento, não precisará de um fluxo. Você pode executar forEachdiretamente na coleção de fontes:accounts.forEach(a -> a.login());
Holger
1
@ Holger: Excelente ponto. Eu incorporei isso na resposta.
Makoto
2
@ Adam.J: Certo, minha resposta se concentrou mais na questão geral contida no seu título, ou seja, esse método é realmente apenas para depuração, explicando os aspectos desse método. Esta resposta está mais concentrada no seu caso de uso real e em como fazê-lo. Então, você poderia dizer que juntos eles fornecem a imagem completa. Primeiro, a razão pela qual esse não é o uso pretendido, depois a conclusão, para não aderir a um uso não intencional e o que fazer. Este último terá um uso mais prático para você.
Holger
2
Claro, era muito mais fácil se o login()método retornou um booleanvalor que indica o status de sucesso ...
Holger
3
Era para isso que eu estava visando. Se login()retornar a boolean, 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 processo login`de um Accountnão influencia o processo de login 'de outro Account.
Holger
111

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 collectacontece com uma operação que processa cada item, enquanto findAnypode 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 do count()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, limitembora não exista uma limitação fundamental que impeça essa previsão.

Como peekpermite “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 a peekaçã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:

Esse método existe principalmente para oferecer suporte à depuração, onde você deseja ver os elementos à medida que eles fluem após um certo ponto em um pipeline

Holger
fonte
haverá algum problema, futuro ou presente, no caso de uso do OP? O código dele sempre faz o que ele quer?
ZhongYu 10/11/2015
9
@ bayou.io: tanto quanto posso ver, não há nenhum problema nesta forma exata . Mas, como tentei explicar, usá-lo dessa maneira implica que você tenha que se lembrar desse aspecto, mesmo se voltar ao código um ou dois anos depois para incorporar a «solicitação de recurso 9876» ao código ...
Holger
1
"a ação de espiada pode ser invocada em uma ordem arbitrária e simultaneamente". Esta declaração não contraria a regra de como a espiada funciona, por exemplo, "como os elementos são consumidos"?
Jose Martinez
5
@ Jose Martinez: Diz “como os elementos são consumidos do fluxo resultante ”, que não é a ação terminal, mas o processamento, embora até a ação terminal possa consumir elementos fora de ordem, desde que o resultado final seja consistente. Mas também acho que, a frase da nota da API, " vê os elementos à medida que passam por um determinado ponto do pipeline " faz um trabalho melhor ao descrevê-lo.
Holger
23

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:

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

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:

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

e você não itera duas vezes uma coleção.

chimera8
fonte
4

Eu diria que peekfornece 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 que forEach()não é apenas para fins de depuração .

Para mim, ao escolher entre forEach()e peek(), é 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á-lo forEach()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 como peek().

Marinos An
fonte
3

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.

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

A espiada é útil para evitar a chamada redundante sem precisar repetir a coleção duas vezes:

accounts.stream()
    .filter(Account::isActive)
    .map(account -> {
        account.login();
        return account;
    })
    .collect(Collectors.toList());
UltimaWeapon
fonte
3
Tudo que você precisa fazer é acertar esse método de login. Realmente não vejo como a espiada é o caminho mais limpo a seguir. Como alguém que está lendo seu código sabe que você realmente está usando mal a API. Um código bom e limpo não força o leitor a fazer suposições sobre o código.
Kaa713
1

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.

Solubris
fonte