Quando devo usar streams?

99

Acabei de me deparar com uma dúvida ao usar um Liste seu stream()método. Embora eu saiba como usá-los, não tenho certeza sobre quando usá-los.

Por exemplo, tenho uma lista contendo vários caminhos para locais diferentes. Agora, gostaria de verificar se um único caminho determinado contém algum dos caminhos especificados na lista. Eu gostaria de retornar um com booleanbase no fato de a condição ter sido atendida ou não.

Isso, claro, não é uma tarefa difícil em si. Mas eu me pergunto se devo usar streams ou um loop for (-each).

A lista

private static final List<String> EXCLUDE_PATHS = Arrays.asList(new String[]{
    "my/path/one",
    "my/path/two"
});

Exemplo - Stream

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream()
                        .map(String::toLowerCase)
                        .filter(path::contains)
                        .collect(Collectors.toList())
                        .size() > 0;
}

Exemplo - For-Each Loop

private boolean isExcluded(String path){
    for (String excludePath : EXCLUDE_PATHS) {
        if(path.contains(excludePath.toLowerCase())){
            return true;
        }
    }
    return false;
}

Observe que o pathparâmetro está sempre em minúsculas .

Meu primeiro palpite é que a abordagem para cada é mais rápida, porque o loop retornaria imediatamente, se a condição fosse atendida. Ao passo que o fluxo ainda faria um loop em todas as entradas da lista para concluir a filtragem.

Minha suposição está correta? Se sim, por que (ou melhor, quando ) eu usaria stream()então?

Mcuenez
fonte
11
Os streams são mais expressivos e legíveis do que os for-loops tradicionais. No último, você precisa ter cuidado com os intrínsecos de se-então e condições, etc. A expressão de fluxo é muito clara: converta nomes de arquivos em minúsculas, depois filtre por algo e depois conte, colete etc. o resultado: um resultado muito iterativo expressão do fluxo de cálculos.
Jean-Baptiste Yunès
12
Não há necessidade de new String[]{…}aqui. Basta usarArrays.asList("my/path/one", "my/path/two")
Holger
4
Se sua fonte for um String[], não há necessidade de ligar Arrays.asList. Você pode simplesmente transmitir sobre a matriz usando Arrays.stream(array). A propósito, tenho dificuldade em entender o propósito do isExcludedteste como um todo. É realmente interessante se um elemento de EXCLUDE_PATHSestá literalmente contido em algum lugar do caminho? Ou seja, isExcluded("my/path/one/foo/bar/baz")vai voltar true, assim como isExcluded("foo/bar/baz/my/path/one/")...
Holger
3
Ótimo, eu não conhecia o Arrays.streammétodo, obrigado por apontar isso. Na verdade, o exemplo que postei parece bastante inútil para qualquer outra pessoa além de mim. Estou ciente do comportamento do isExcludedmétodo, mas na verdade é apenas algo de que preciso para mim, portanto, para responder à sua pergunta: sim , é interessante por motivos que gostaria de não mencionar, pois não caberia no escopo da pergunta original.
mcuenez
1
Por que o é toLowerCaseaplicado à constante que já está em minúsculas? Não deveria ser aplicado ao pathargumento?
Sebastian Redl

Respostas:

78

Sua suposição está correta. Sua implementação de stream é mais lenta do que o loop for.

Este uso de stream deve ser tão rápido quanto o for-loop embora:

EXCLUDE_PATHS.stream()  
                               .map(String::toLowerCase)
                               .anyMatch(path::contains);

Isso itera através dos itens, aplicando String::toLowerCasee o filtro aos itens um por um e terminando no primeiro item que corresponda.

Ambos collect()e anyMatch()são operações de terminal. anyMatch()sai no primeiro item encontrado, entretanto, enquanto collect()requer que todos os itens sejam processados.

Stefan Pries
fonte
2
Incrível, não sabia sobre findFirst()em combinação com filter(). Aparentemente, não sei usar streams tão bem quanto pensei.
mcuenez
4
Existem alguns artigos e apresentações de blog realmente interessantes na web sobre o desempenho da API de fluxo, que achei muito úteis para entender como essas coisas funcionam nos bastidores. Eu definitivamente recomendo pesquisar um pouco, se você estiver interessado nisso.
Stefan Pries
Após sua edição, sinto que sua resposta é a que deveria ser aceita, pois você também respondeu minha pergunta nos comentários da outra resposta. Porém, gostaria de dar a @ rvit34 algum crédito por postar o código :-)
mcuenez
34

A decisão de usar Streams ou não deve ser orientada pela consideração de desempenho, mas sim pela legibilidade. Quando se trata de desempenho, existem outras considerações.

Com a sua .filter(path::contains).collect(Collectors.toList()).size() > 0abordagem, você está processando todos os elementos e coletando-os em um temporário List, antes de comparar o tamanho, ainda assim, isso dificilmente importa para um Stream composto de dois elementos.

O uso .map(String::toLowerCase).anyMatch(path::contains)pode economizar memória e ciclos de CPU, se você tiver um número substancialmente maior de elementos. Ainda assim, isso converte cada um Stringem sua representação em minúsculas, até que uma correspondência seja encontrada. Obviamente, vale a pena usar

private static final List<String> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .collect(Collectors.toList());

private boolean isExcluded(String path) {
    return EXCLUDE_PATHS.stream().anyMatch(path::contains);
}

em vez de. Assim, você não precisa repetir a conversão para letras minúsculas em cada invocação de isExcluded. Se o número de elementos EXCLUDE_PATHSou o comprimento das strings se tornarem muito grandes, você pode considerar o uso

private static final List<Predicate<String>> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .map(s -> Pattern.compile(s, Pattern.LITERAL).asPredicate())
          .collect(Collectors.toList());

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream().anyMatch(p -> p.test(path));
}

Compilar uma string como padrão regex com o LITERALsinalizador faz com que ela se comporte como operações de string comuns, mas permite que o mecanismo passe algum tempo em preparação, por exemplo, usando o algoritmo de Boyer Moore, para ser mais eficiente quando se trata da comparação real.

Claro, isso só compensa se houver testes subsequentes suficientes para compensar o tempo gasto na preparação. Determinar se esse será o caso é uma das considerações reais de desempenho, além da primeira questão se essa operação algum dia será crítica para o desempenho. Não é a questão de usar Streams ou forloops.

A propósito, os exemplos de código acima mantêm a lógica do seu código original, o que me parece questionável. Seu isExcludedmétodo retorna true, se o caminho especificado contiver qualquer um dos elementos na lista, então ele retorna truepara /some/prefix/to/my/path/one, bem como my/path/one/and/some/suffixou até mesmo /some/prefix/to/my/path/one/and/some/suffix.

Even dummy/path/onerousé considerado cumprindo os critérios, pois é containsa string my/path/one...

Holger
fonte
Bons insights sobre a possível otimização de desempenho, obrigado. Com relação à última parte de sua resposta: se minha resposta ao seu comentário não foi satisfatória, considere meu código de exemplo como um mero auxiliar para que outros entendam o que estou perguntando - em vez de ser um código real. Além disso, você sempre pode editar a pergunta, se tiver um exemplo melhor em mente.
mcuenez
3
Aceito seu comentário de que essa operação é o que você realmente deseja, portanto, não há necessidade de alterá-la. Vou apenas manter a última seção para futuros leitores, para que saibam que esta não é uma operação típica, mas também, que já foi discutida e não precisa de mais comentários ...
Holger
Na verdade, os streams são perfeitos para usar para otimização de memória quando a quantidade de memória de trabalho está ultrapassando o limite do servidor
ColacX
21

Sim. Você está certo. Sua abordagem de fluxo terá alguma sobrecarga. Mas você pode usar tal construção:

private boolean isExcluded(String path) {
    return  EXCLUDE_PATHS.stream().map(String::toLowerCase).anyMatch(path::contains);
}

O principal motivo para usar streams é que eles tornam seu código mais simples e fácil de ler.

rvit34
fonte
3
É anyMatchum atalho para filter(...).findFirst().isPresent()?
mcuenez
6
Sim, ele é! Isso é ainda melhor do que minha primeira sugestão.
Stefan Pries
8

O objetivo dos streams em Java é simplificar a complexidade de escrever código paralelo. É inspirado na programação funcional. O fluxo serial serve apenas para tornar o código mais limpo.

Se quisermos desempenho, devemos usar parallelStream, que foi projetado para. O serial, em geral, é mais lento.

Há um bom artigo para ler sobre , e Desempenho ForLoopStreamParallelStream .

Em seu código, podemos usar métodos de terminação para interromper a pesquisa na primeira correspondência. (anyMatch ...)

Paulo Ricardo Almeida
fonte
5
Observe que para fluxos pequenos e em alguns outros casos, um fluxo paralelo pode ser mais lento devido ao custo de inicialização. E se você tiver uma operação de terminal ordenada, em vez de uma operação paralelizável não ordenada, ressincronização no final.
CAD97 de
0

Como outros mencionaram muitos pontos positivos, mas quero apenas mencionar a avaliação preguiçosa na avaliação do fluxo. Quando fazemos map()para criar um fluxo de caminhos em minúsculas, não estamos criando todo o fluxo imediatamente, em vez disso, o fluxo é construído lentamente , razão pela qual o desempenho deve ser equivalente ao tradicional loop for. Não está fazendo uma varredura completa map()e anyMatch()é executado ao mesmo tempo. Quando anyMatch()retornar verdadeiro, ele entrará em curto-circuito.

Kaicheng Hu
fonte