web-dev-qa-db-fra.com

Comment ajouter des éléments d'un flux Java8 dans une liste existante

Javadoc of Collector montre comment collecter des éléments d'un flux dans une nouvelle liste. Existe-t-il une ligne qui ajoute les résultats dans une ArrayList existante?

134
codefx

NOTE:la réponse de nosid montre comment ajouter des éléments à une collection existante à l'aide de forEachOrdered(). C'est une technique utile et efficace pour la mutation de collections existantes. Ma réponse explique pourquoi vous ne devriez pas utiliser un Collector pour transformer une collection existante.

La réponse courte est no, du moins, pas en général, vous ne devriez pas utiliser un Collector pour modifier une collection existante.

La raison en est que les collecteurs sont conçus pour prendre en charge le parallélisme, même sur des collections qui ne sont pas thread-safe. Pour ce faire, chaque thread fonctionne indépendamment sur sa propre collection de résultats intermédiaires. Chaque thread obtient sa propre collection en appelant la Collector.supplier() qui est requise pour renvoyer une collection new à chaque fois.

Ces collections de résultats intermédiaires sont ensuite fusionnées, à nouveau de manière confinée dans un thread, jusqu'à obtenir une seule collection de résultats. C'est le résultat final de l'opération collect().

Quelques réponses de Balder et assylias ont suggéré d'utiliser Collectors.toCollection(), puis de passer à un fournisseur renvoyant une liste existante à la place d'une nouvelle liste. Cela enfreint l'exigence imposée au fournisseur, à savoir qu'il renvoie à chaque fois une nouvelle collection vide.

Cela fonctionnera pour des cas simples, comme le montrent les exemples de leurs réponses. Cependant, cela échouera, en particulier si le flux est exécuté en parallèle. (Une future version de la bibliothèque pourrait changer de manière imprévue, ce qui entraînerait son échec, même dans le cas séquentiel.)

Prenons un exemple simple:

List<String> destList = new ArrayList<>(Arrays.asList("foo"));
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");
newList.parallelStream()
       .collect(Collectors.toCollection(() -> destList));
System.out.println(destList);

Lorsque je lance ce programme, je reçois souvent un ArrayIndexOutOfBoundsException. En effet, plusieurs threads fonctionnent sur ArrayList, une structure de données thread-unsafe. OK, synchronisons-le:

List<String> destList =
    Collections.synchronizedList(new ArrayList<>(Arrays.asList("foo")));

Cela n'échouera plus avec une exception. Mais au lieu du résultat attendu:

[foo, 0, 1, 2, 3]

cela donne des résultats bizarres comme celui-ci:

[foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0]

C’est le résultat des opérations d’accumulation/fusion confinées par thread que j’ai décrites ci-dessus. Avec un flux parallèle, chaque thread appelle le fournisseur pour obtenir sa propre collection pour une accumulation intermédiaire. Si vous passez devant un fournisseur qui retourne la collection same, chaque thread ajoute ses résultats à cette collection. Comme il n'y a pas d'ordre dans les threads, les résultats seront ajoutés dans un ordre quelconque.

Ensuite, lorsque ces collections intermédiaires sont fusionnées, la liste est fusionnée avec elle-même. Les listes sont fusionnées à l'aide de List.addAll(), qui indique que les résultats ne sont pas définis si la collection source est modifiée au cours de l'opération. Dans ce cas, ArrayList.addAll() effectue une opération de copie sur tableau, de sorte qu'elle se duplique elle-même, ce qui correspond à ce à quoi on pourrait s'attendre, je suppose. (Notez que d'autres implémentations de List peuvent avoir un comportement complètement différent.) Quoi qu'il en soit, cela explique les résultats étranges et les éléments dupliqués dans la destination.

Vous pourriez dire: "Je vais simplement m'assurer de lancer mon flux séquentiellement" et continuer à écrire du code comme celui-ci.

stream.collect(Collectors.toCollection(() -> existingList))

en tous cas. Je recommande de ne pas le faire. Si vous contrôlez le flux, vous pouvez vous assurer qu'il ne fonctionnera pas en parallèle. Je m'attends à un style de programmation qui émerge, où les flux sont transmis au lieu des collections. Si quelqu'un vous tend un flux et que vous utilisez ce code, il échouera si le flux est parallèle. Pire encore, quelqu'un pourrait vous remettre un flux séquentiel et ce code fonctionnerait correctement pendant un certain temps, il réussirait tous les tests, etc. votre code à casser.

OK, alors assurez-vous de ne pas oublier d'appeler sequential() sur n'importe quel flux avant d'utiliser ce code:

stream.sequential().collect(Collectors.toCollection(() -> existingList))

Bien sûr, vous vous souviendrez de le faire à chaque fois, non? :-) Disons que vous faites. Ensuite, l’équipe des performances se demandera pourquoi toutes leurs implémentations parallèles soigneusement conçues ne fournissent aucune accélération. Et encore une fois, ils vont le retrouver jusqu'à votre code, ce qui force tout le flux à s'exécuter de manière séquentielle.

Ne le fais pas.

162
Stuart Marks

Autant que je sache, toutes les autres réponses jusqu'ici utilisaient un collecteur pour ajouter des éléments à un flux existant. Cependant, la solution est plus courte et fonctionne pour les flux séquentiels et parallèles. Vous pouvez simplement utiliser la méthode forEachOrdered en combinaison avec une référence de méthode.

List<String> source = ...;
List<Integer> target = ...;

source.stream()
      .map(String::length)
      .forEachOrdered(target::add);

La seule restriction est que source et cible sont des listes différentes, car vous n'êtes pas autorisé à modifier la source d'un flux tant qu'il est traité.

Notez que cette solution fonctionne pour les flux séquentiels et parallèles. Cependant, il ne bénéficie pas de la simultanéité. La référence de la méthode passée à forEachOrdered sera toujours exécutée de manière séquentielle.

148
nosid

La réponse courte est non (ou devrait être non). EDIT: Oui, c'est possible (voir la réponse d'Assylias ci-dessous), mais continuez à lire. EDIT2: mais voyez la réponse de Stuart Marks pour une autre raison pour laquelle vous ne devriez toujours pas le faire!

La réponse la plus longue:

Le but de ces constructions dans Java 8 est d’introduire quelques concepts de programmation fonctionnelle dans le langage; Dans la programmation fonctionnelle, les structures de données ne sont généralement pas modifiées. Au lieu de cela, de nouvelles structures sont créées à l'aide de transformations telles que map, filter, fold/réduire et bien d'autres.

Si vous devez modifier l'ancienne liste, rassemblez simplement les éléments mappés dans une nouvelle liste:

final List<Integer> newList = list.stream()
                                  .filter(n -> n % 2 == 0)
                                  .collect(Collectors.toList());

et ensuite faites list.addAll(newList) - de nouveau: si vous en avez vraiment besoin.

(ou construisez une nouvelle liste concaténant l'ancienne et la nouvelle, et attribuez-la à la variable list. Il s'agit d'un petit bit ​​de plus dans l'esprit de FP que addAll)

Pour ce qui est de l’API: même si l’API le permet (voir la réponse d’assylias à nouveau), vous devriez éviter de le faire, du moins en général. Il est préférable de ne pas combattre le paradigme (FP) et d'essayer de l'apprendre plutôt que de le combattre (même si Java n'est généralement pas un langage FP, et que vous utilisiez uniquement "plus sale" "tactique si absolument nécessaire.

La réponse vraiment longue: (c'est-à-dire si vous incluez l'effort de rechercher et de lire un FP intro/livre comme suggéré)

Comprendre pourquoi la modification de listes existantes est en général une mauvaise idée et conduit à un code moins gérable - à moins que vous ne modifiiez une variable locale et que votre algorithme soit court et/ou trivial, ce qui sort du cadre de la question de la maintenabilité du code. - trouvez une bonne introduction à la programmation fonctionnelle (il y en a des centaines) et commencez à lire. Une explication "de prévisualisation" pourrait ressembler à quelque chose du genre: plus mathématiquement valable et plus facile à raisonner sur le point de ne pas modifier les données (dans la plupart des parties de votre programme), elle conduit à un niveau supérieur et moins technique (ainsi qu’à une plus grande convivialité, une fois que votre cerveau transitions loin de la pensée impérative de style ancien) définitions de la logique de programme.

11
Erik Allik

Erik Allik a déjà donné de très bonnes raisons pour lesquelles vous ne souhaiterez probablement pas collecter d'éléments d'un flux dans une liste existante.

Quoi qu'il en soit, vous pouvez utiliser l'un des doublons suivants si vous avez vraiment besoin de cette fonctionnalité.

Mais comme Stuart Marks explique dans sa réponse, vous devriez ne jamais faire cela, si les flux peuvent être des flux parallèles - utilisez at vos propres risques ...

list.stream().collect(Collectors.toCollection(() -> myExistingList));
9
Balder

Vous devez simplement vous référer à votre liste d'origine pour être celle que la Collectors.toList() renvoie.

Voici une démo:

import Java.util.Arrays;
import Java.util.List;
import Java.util.stream.Collectors;

public class Reference {

  public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    System.out.println(list);

    // Just collect even numbers and start referring the new list as the original one.
    list = list.stream()
               .filter(n -> n % 2 == 0)
               .collect(Collectors.toList());
    System.out.println(list);
  }
}

Et voici comment vous pouvez ajouter les éléments nouvellement créés à votre liste d'origine en une seule ligne.

List<Integer> list = ...;
// add even numbers from the list to the list again.
list.addAll(list.stream()
                .filter(n -> n % 2 == 0)
                .collect(Collectors.toList())
);

C'est ce que propose ce paradigme de programmation fonctionnelle.

4
Aman Agnihotri