Por que devo usar "operações funcionais" em vez de um loop for?

39
for (Canvas canvas : list) {
}

O NetBeans sugere que eu use "operações funcionais":

list.stream().forEach((canvas) -> {
});

Mas por que isso é preferido ? Se alguma coisa, é mais difícil de ler e entender. Você está chamando stream(), em seguida, forEach()usando uma expressão lambda com o parâmetro canvas. Não vejo como isso é melhor do que o forloop no primeiro trecho.

Obviamente, estou falando apenas de estética. Talvez haja uma vantagem técnica aqui que estou perdendo. O que é isso? Por que eu deveria usar o segundo método?

Ómega
fonte
15
No seu exemplo em particular, não seria preferido.
Robert Harvey
1
Contanto que a única operação seja uma única para cada, tenho a tendência de concordar com você. Assim que você adiciona outras operações ao pipeline ou produz uma sequência de saída, a abordagem de fluxo se torna preferível.
JacquesB
@RobertHarvey não seria? Por que não?
Sara
@RobertHarvey Bem, eu concordo que a resposta aceita realmente mostra como a versão inicial sai da água para casos mais complicados, mas não vejo o motivo de "vitórias" no caso trivial. você afirma que é evidente, mas eu não vejo, então perguntei.
Sara

Respostas:

45

Os fluxos fornecem uma abstração muito melhor para a composição das diferentes operações que você deseja realizar sobre as coleções ou os fluxos de dados recebidos. Especialmente quando você precisa mapear elementos, filtrar e convertê-los.

Seu exemplo não é muito prático. Considere o seguinte código do site Oracle .

List<Transaction> groceryTransactions = new Arraylist<>();
for(Transaction t: transactions){
  if(t.getType() == Transaction.GROCERY){
    groceryTransactions.add(t);
  }
}
Collections.sort(groceryTransactions, new Comparator(){
  public int compare(Transaction t1, Transaction t2){
    return t2.getValue().compareTo(t1.getValue());
  }
});
List<Integer> transactionIds = new ArrayList<>();
for(Transaction t: groceryTransactions){
  transactionsIds.add(t.getId());
}

pode ser escrito usando fluxos:

List<Integer> transactionsIds = 
    transactions.stream()
                .filter(t -> t.getType() == Transaction.GROCERY)
                .sorted(comparing(Transaction::getValue).reversed())
                .map(Transaction::getId)
                .collect(toList());

A segunda opção é muito mais legível. Portanto, quando você tem loops aninhados ou vários loops fazendo processamento parcial, é um candidato muito bom para o uso da API Streams / Lambda.

luboskrnac
fonte
5
Existem técnicas de otimização, como fusão de mapas ( stream.map(f).map(g)stream.map(f.andThen(g))), construção / redução de fusão (ao criar um fluxo em um método e depois passá-lo para outro método que o consome, o compilador pode eliminar o fluxo) e fusão de fluxo (que pode fundir muitas operações de fluxo juntos em um único loop imperativo), o que pode tornar as operações de fluxo muito mais eficientes. Eles são implementados no compilador GHC Haskell, e também em alguns outros compiladores Haskell e outras linguagens funcionais, e existem implementações de pesquisa experimental para o Scala.
Jörg W Mittag
Provavelmente, o desempenho não é um fator ao considerar o loop funcional vs, pois o compilador pode / deve fazer a conversão de um loop for para uma operação funcional se o Netbeans puder e estiver determinado como o caminho ideal.
Ryan
Eu discordo que o segundo é mais legível. Demora um pouco para descobrir o que está acontecendo. Existe uma vantagem de desempenho em fazer o segundo método porque, caso contrário, eu não o vejo?
Bok McDonagh
A experiência do @BokMcDonagh, Me é que é menos legível para desenvolvedores que não se incomodaram em se familiarizar com novas abstrações. Eu sugeriria usar mais essas APIs, para se familiarizar, porque é o futuro. Não apenas no mundo Java.
luboskrnac 18/01
16

Outra vantagem do uso da API de streaming funcional é que ele oculta os detalhes da implementação. Apenas descreve o que deve ser feito, não como. Essa vantagem se torna óbvia ao observar as alterações que precisam ser feitas, para alterar a execução de código de thread único para paralelo. Apenas mude o .stream()para .parallelStream().

Stefan Dollase
fonte
13

Se alguma coisa, é mais difícil de ler e entender.

Isso é altamente subjetivo. Acho a segunda versão muito mais fácil de ler e entender. Ele corresponde à maneira como outras linguagens (por exemplo, Ruby, Smalltalk, Clojure, Io, Ioke, Seph) o fazem, requer menos conceitos para entender (é apenas uma chamada de método normal como qualquer outra, enquanto o primeiro exemplo é uma sintaxe especializada).

Se alguma coisa, é uma questão de familiaridade.

Jörg W Mittag
fonte
6
Sim, é verdade. Mas isso parece mais um comentário do que uma resposta para mim.
Omega
Os usos de operação funtional sintaxe especializada também: o "->" é novo
Ryan