Pourquoi devrais-je utiliser des «opérations fonctionnelles» au lieu d'une boucle for?

39
for (Canvas canvas : list) {
}

NetBeans me suggère d’utiliser des "opérations fonctionnelles":

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

Mais pourquoi est-ce préféré ? Au contraire, il est plus difficile à lire et à comprendre. Vous appelez stream()puis forEach()utilisez une expression lambda avec paramètre canvas. Je ne vois pas en quoi cela est plus agréable que la forboucle du premier extrait.

Évidemment, je parle uniquement d'esthétique. Peut-être me manque-t-il un avantage technique. Qu'Est-ce que c'est? Pourquoi devrais-je utiliser la deuxième méthode à la place?

Oméga
la source
15
Dans votre exemple particulier, cela ne serait pas préféré.
Robert Harvey
1
Tant que la seule opération est unique, chaque fois, j'ai tendance à être d'accord avec vous. Dès que vous ajoutez d'autres opérations au pipeline ou que vous produisez une séquence de sortie, l'approche par flux devient préférable.
JacquesB
@ RobertHarvey serait-ce pas? pourquoi pas?
Sara
@RobertHarvey je conviens que la réponse acceptée montre vraiment comment la version-for est éjectée de l'eau pour les cas plus compliqués, mais je ne vois pas pourquoi pour "gagne" dans le cas trivial. vous dites que cela va de soi, mais je ne le vois pas, alors j'ai demandé.
Sara

Réponses:

45

Les flux fournissent une bien meilleure abstraction pour la composition des différentes opérations que vous souhaitez effectuer en plus des collections ou des flux de données entrants. Surtout lorsque vous devez mapper, filtrer et convertir des éléments.

Votre exemple n'est pas très pratique. Considérez le code suivant du 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());
}

peut être écrit en utilisant des flux:

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

La deuxième option est beaucoup plus lisible. Ainsi, lorsque vous avez des boucles imbriquées ou diverses boucles effectuant un traitement partiel, c'est un très bon candidat pour l'utilisation de l'API Streams / Lambda.

Luboskrnac
la source
5
Il existe des techniques d'optimisation telles que la fusion de carte ( stream.map(f).map(g)stream.map(f.andThen(g))) build / réduire la fusion (lors de la construction d' un ruisseau dans une méthode et passer ensuite à une autre méthode qui consomme, le compilateur peut éliminer le flux) et fusion courant ( ce qui peut fusionner plusieurs opérations de flux ensemble en une seule boucle impérative), ce qui peut rendre les opérations de flux beaucoup plus efficaces. Ils sont implémentés dans le compilateur GHC Haskell, ainsi que dans d'autres compilateurs Haskell et d'autres langages fonctionnels, et il existe des implémentations de recherche expérimentale pour Scala.
Jörg W Mittag le
La performance n’est probablement pas un facteur à prendre en compte lorsqu’on considère une boucle fonctionnelle vs car le compilateur pourrait / devrait faire la conversion d’une boucle for en une opération fonctionnelle si Netbeans le peut et qu’il est déterminé qu’il s’agit du chemin optimal.
Ryan
Je ne suis pas d'accord pour dire que le second est plus lisible. Il faut un certain temps pour comprendre ce qui se passe. Existe-t-il un avantage en termes de performances lors de la deuxième méthode car sinon je ne la vois pas?
Bok McDonagh le
@BokMcDonagh, moi, l'expérience est qu'il est moins lisible pour les développeurs qui ne se sont pas donné la peine de se familiariser avec les nouvelles abstractions. Je suggérerais d'utiliser davantage ces API, de se familiariser davantage, car c'est l'avenir. Pas seulement dans le monde Java.
luboskrnac le
16

L’utilisation de l’API de streaming fonctionnelle présente également l’avantage de cacher des détails d’implémentation. Il décrit uniquement ce qui doit être fait, pas comment. Cet avantage devient évident lorsque l’on examine le changement à effectuer, qui consiste à passer de l’exécution de code unique à l’exécution de code parallèle. Il suffit de changer le .stream()à .parallelStream().

Stefan Dollase
la source
13

Au contraire, il est plus difficile à lire et à comprendre.

C'est très subjectif. Je trouve la deuxième version beaucoup plus facile à lire et à comprendre. Cela correspond à la façon dont les autres langages (ex. Ruby, Smalltalk, Clojure, Io, Ioke, Seph) le fait, il nécessite moins de concepts à comprendre (c'est juste un appel de méthode normal comme un autre, alors que le premier exemple est une syntaxe spécialisée).

Si quelque chose, c'est une question de familiarité.

Jörg W Mittag
la source
6
Ouais c'est vrai. Mais cela ressemble plus à un commentaire qu'à une réponse.
Omega
L'opération fonctionnelle utilise aussi une syntaxe spécialisée: le "->" est nouveau
Ryan