Operações intermediárias de fluxo não avaliadas na contagem

33

Parece que estou tendo problemas para entender como o Java compõe operações de fluxo em um pipeline de fluxo.

Ao executar o seguinte código

public
 static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

O console apenas imprime 4. O StringBuilderobjeto ainda tem o valor "".

Quando adiciono a operação de filtro: filter(s -> true)

public static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .filter(s -> true)
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

A saída muda para:

4
1234

Como essa operação de filtro aparentemente redundante altera o comportamento do pipeline de fluxo composto?

atalantus
fonte
2
Interessante !!!
uneq95 04/01
3
Eu imagino que esse seja um comportamento específico da implementação; talvez seja porque o primeiro fluxo tem um tamanho conhecido, mas o segundo não, e o tamanho determina se as operações intermediárias são executadas.
Andy Turner
Fora de interesse, o que acontece se você reverter o filtro e o mapa?
Andy Turner
Tendo programado um pouco em Haskell, cheira um pouco como uma avaliação preguiçosa acontecendo aqui. Uma pesquisa no Google retornou, que os fluxos realmente têm alguma preguiça. Pode ser esse o caso? E sem um filtro, se o java for inteligente o suficiente, não será necessário executar o mapeamento.
Frederik
@AndyTurner Dá o mesmo resultado, mesmo na reversão
uneq95 04/01

Respostas:

39

A count()operação do terminal, na minha versão do JDK, acaba executando o seguinte código:

if (StreamOpFlag.SIZED.isKnown(helper.getStreamAndOpFlags()))
    return spliterator.getExactSizeIfKnown();
return super.evaluateSequential(helper, spliterator);

Se houver uma filter()operação no pipeline de operações, o tamanho do fluxo, que é conhecido inicialmente, não poderá mais ser conhecido (pois filterpoderá rejeitar alguns elementos do fluxo). Portanto, o ifbloco não é executado, as operações intermediárias são executadas e o StringBuilder é modificado.

Por outro lado, se você tiver apenas map()no pipeline, é garantido que o número de elementos no fluxo seja o mesmo que o número inicial de elementos. Portanto, o bloco if é executado e o tamanho é retornado diretamente sem avaliar as operações intermediárias.

Observe que o lambda passado para map()viola o contrato definido na documentação: deveria ser uma operação sem estado e sem interferências, mas não é sem estado. Portanto, ter um resultado diferente nos dois casos não pode ser considerado um bug.

JB Nizet
fonte
Como flatMap()poderia alterar o número de elementos, foi por isso que estava inicialmente ansioso (agora preguiçoso)? Portanto, a alternativa seria usar forEach()e contar separadamente se, map()na sua forma atual, violar o contrato, eu acho.
Frederik
3
Em relação ao flatMap, acho que não. Foi, AFAIK, porque era mais simples inicialmente torná-lo ansioso. Sim, usar uma transmissão com map () para produzir efeitos colaterais é uma má ideia.
JB Nizet 04/01
Você gostaria de sugerir como obter a saída total 4 1234sem utilizar o filtro extra ou produzir efeitos colaterais na operação map ()?
atalantus 04/01
11
int count = array.length; String result = String.join("", array);
JB Nizet 04/01
11
ou você pode usar o forEach se você realmente quiser usar um StringBuilder, ou você pode usarCollectors.joining("")
njzk2
19

No jdk-9 , foi claramente documentado nos documentos java

A eliminação de efeitos colaterais também pode ser surpreendente. Com exceção das operações de terminal forEach e forEachOrdered, os efeitos colaterais dos parâmetros comportamentais nem sempre podem ser executados quando a implementação do fluxo pode otimizar a execução dos parâmetros comportamentais sem afetar o resultado da computação. (Para um exemplo específico, consulte a nota da API documentada na operação de contagem .)

Nota da API:

Uma implementação pode optar por não executar o pipeline de fluxo (sequencialmente ou em paralelo) se for capaz de calcular a contagem diretamente da fonte de fluxo. Nesses casos, nenhum elemento de origem será percorrido e nenhuma operação intermediária será avaliada. Parâmetros comportamentais com efeitos colaterais, que são fortemente desencorajados, exceto em casos inofensivos, como depuração, podem ser afetados. Por exemplo, considere o seguinte fluxo:

 List<String> l = Arrays.asList("A", "B", "C", "D");
 long count = l.stream().peek(System.out::println).count();

O número de elementos cobertos pela origem do fluxo, uma Lista, é conhecido e a operação intermediária, espiada, não injeta nem remove elementos do fluxo (como pode ser o caso das operações flatMap ou de filtro). Portanto, a contagem é do tamanho da lista e não há necessidade de executar o pipeline e, como efeito colateral, imprimir os elementos da lista.

Piscina morta
fonte
0

Não é para isso que serve o .map. É suposto ser usado para transformar um fluxo de "Something" em um fluxo de "Something Else". Nesse caso, você está usando o mapa para anexar uma string a um Stringbuilder externo, após o qual você tem um fluxo de "Stringbuilder", cada um dos quais foi criado pela operação de mapa anexando um número ao Stringbuilder original.

Seu fluxo não faz nada com os resultados mapeados no fluxo, portanto, é perfeitamente razoável supor que a etapa possa ser ignorada pelo processador de fluxo. Você está contando com efeitos colaterais para fazer o trabalho, o que quebra o modelo funcional do mapa. Você seria melhor atendido usando o forEach para fazer isso. Faça a contagem inteiramente como um fluxo separado ou coloque um contador usando AtomicInt no forEach.

O filtro obriga a executar o conteúdo do fluxo, já que agora ele tem que fazer algo notoriamente significativo com cada elemento do fluxo.

DaveB
fonte