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

163

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

codefx
la source
1
Il y a déjà une réponse ici . Recherchez l'élément "Ajouter à un élément existant Collection"
Holger

Réponses:

206

REMARQUE: la réponse de nosid montre comment ajouter à une collection existante en utilisant forEachOrdered(). C'est une technique utile et efficace pour faire muter des collections existantes. Ma réponse explique pourquoi vous ne devriez pas utiliser a Collectorpour muter une collection existante.

La réponse courte est non , du moins, pas en général, vous ne devriez pas utiliser a Collectorpour 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. La façon dont chaque thread obtient sa propre collection est d'appeler le Collector.supplier()qui est requis pour renvoyer une nouvelle collection à chaque fois.

Ces collections de résultats intermédiaires sont ensuite fusionnées, à nouveau de manière confinée au thread, jusqu'à ce qu'il n'y ait qu'une seule collection de résultats. C'est le résultat final de l' collect()opération.

Quelques réponses de Balder et d' assylias ont suggéré d'utiliser Collectors.toCollection()puis de transmettre un fournisseur qui renvoie une liste existante au lieu d'une nouvelle liste. Cela enfreint l'exigence du fournisseur, à savoir qu'il retourne une nouvelle collection vide à chaque fois.

Cela fonctionnera pour des cas simples, comme le montrent les exemples de leurs réponses. Cependant, il échouera, en particulier si le flux est exécuté en parallèle. (Une future version de la bibliothèque pourrait changer d'une manière imprévue 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 j'exécute ce programme, j'obtiens souvent un fichier ArrayIndexOutOfBoundsException. Cela est dû au fait que plusieurs threads fonctionnent sur ArrayList, une structure de données thread-unsafe. OK, faisons-le synchronisé:

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 étranges 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 au thread que j'ai décrites ci-dessus. Avec un flux parallèle, chaque thread appelle le fournisseur pour obtenir sa propre collection pour l'accumulation intermédiaire. Si vous passez un fournisseur qui renvoie la même collection, chaque thread ajoute ses résultats à cette collection. Puisqu'il n'y a aucun ordre parmi les threads, les résultats seront ajoutés dans un ordre arbitraire.

Ensuite, lorsque ces collections intermédiaires sont fusionnées, cela fusionne essentiellement la liste avec elle-même. Les listes sont fusionnées en utilisant List.addAll(), ce qui indique que les résultats ne sont pas définis si la collection source est modifiée pendant l'opération. Dans ce cas, ArrayList.addAll()fait une opération de copie de tableau, donc il finit par se dupliquer, ce qui est en quelque sorte 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 juste m'assurer d'exécuter mon flux de manière séquentielle" et continuer et écrire un code comme celui-ci

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

en tous cas. Je recommande de ne pas faire cela. Si vous contrôlez le flux, bien sûr, vous pouvez garantir qu'il ne fonctionnera pas en parallèle. Je m'attends à ce qu'un style de programmation émerge où les flux sont transmis au lieu des collections. Si quelqu'un vous remet un flux et que vous utilisez ce code, il échouera si le flux se trouve être parallèle. Pire encore, quelqu'un pourrait vous remettre un flux séquentiel et ce code fonctionne très bien pendant un certain temps, passer tous les tests, etc. Ensuite, une certaine quantité arbitraire de temps plus tard, le code ailleurs dans le système pourrait changer d'utiliser des flux parallèles qui causeront votre code de casser.

OK, alors n'oubliez pas 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 de performance se demandera pourquoi toutes leurs implémentations parallèles soigneusement conçues ne fournissent aucune accélération. Et une fois de plus, ils le retraceront jusqu'à votre code, ce qui oblige tout le flux à s'exécuter de manière séquentielle.

Ne fais pas ça.

Marques Stuart
la source
Excellente explication! - merci d'avoir clarifié cela. Je vais modifier ma réponse pour recommander de ne jamais faire cela avec d'éventuels flux parallèles.
Balder
3
Si la question est, s'il existe une ligne unique pour ajouter des éléments d'un flux dans une liste existante, la réponse courte est oui . Voyez ma réponse. Cependant, je suis d'accord avec vous, que l'utilisation de Collectors.toCollection () en combinaison avec une liste existante n'est pas la bonne manière.
nosid
Vrai. Je suppose que le reste d'entre nous pensait tous aux collectionneurs.
Stuart marque
Très bonne réponse! Je suis très tenté d'utiliser la solution séquentielle même si vous déconseillez clairement car comme indiqué elle doit bien fonctionner. Mais le fait que le javadoc exige que l'argument fournisseur de la toCollectionméthode renvoie une nouvelle collection vide à chaque fois me convainc de ne pas le faire. Je veux vraiment briser le contrat javadoc des classes Java de base.
zoom
1
@AlexCurvers Si vous voulez que le flux ait des effets secondaires, vous voudrez certainement l'utiliser forEachOrdered. Les effets secondaires incluent l'ajout d'éléments à une collection existante, qu'elle en ait déjà ou non. Si vous souhaitez que les éléments d'un flux soient placés dans une nouvelle collection, utilisez collect(Collectors.toList())ou toSet()ou toCollection().
Stuart marque le
176

Pour autant que je sache, toutes les autres réponses jusqu'à présent utilisaient un collecteur pour ajouter des éléments à un flux existant. Cependant, il existe une solution plus courte et elle 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 la source et la cible sont des listes différentes, car vous n'êtes pas autorisé à apporter des modifications à 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 concurrence. La référence de méthode passée à forEachOrdered sera toujours exécutée séquentiellement.

nosid
la source
7
+1 C'est drôle de voir combien de personnes prétendent qu'il n'y a aucune possibilité quand il y en a une. Btw. J'ai inclus forEach(existing::add)comme possibilité dans un réponse il y a deux mois . J'aurais dû ajouter forEachOrderedaussi…
Holger
5
Y a-t-il une raison pour laquelle vous avez utilisé forEachOrderedau lieu deforEach ?
membresound
7
@membersound: forEachOrderedfonctionne pour les flux séquentiels et parallèles . En revanche, forEachpeut exécuter l'objet de fonction passé simultanément pour les flux parallèles. Dans ce cas, l'objet de fonction doit être correctement synchronisé, par exemple en utilisant un Vector<Integer>.
nosid
@BrianGoetz: Je dois admettre que la documentation de Stream.forEachOrdered est un peu imprécise. Cependant, je ne vois aucune interprétation raisonnable de ce cahier des charges , dans lequel on ne se produit, avant la relation entre deux appels target::add. Quels que soient les threads à partir desquels la méthode est appelée, il n'y a pas de course aux données . Je m'attendais à ce que vous le sachiez.
nosid le
C'est la réponse la plus utile, en ce qui me concerne. Il montre en fait un moyen pratique d'insérer des éléments dans une liste existante à partir d'un flux, ce que la question a posée (malgré le mot trompeur "collect")
Wheezil
13

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 en Java 8 est d'introduire certains 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 sont créées à partir des anciennes au moyen de transformations telles que mapper, filtrer, replier / 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());

puis faites list.addAll(newList)- à nouveau: si vous devez vraiment

(ou construisez une nouvelle liste concaténant l'ancienne et la nouvelle, et attribuez-la au list variable - c'est un peu plus dans l'esprit de FP que addAll)

Quant à l'API: même si l'API le permet (encore une fois, voir la réponse d'assylias), vous devriez essayer d'éviter de faire cela quoi qu'il en soit, 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 de ne recourir à des tactiques «plus sales» que si c'est absolument nécessaire.

La réponse très longue: (c'est-à-dire si vous incluez l'effort de trouver et de lire une intro / livre de PF comme suggéré)

Découvrir pourquoi la modification de listes existantes est en général une mauvaise idée et conduit à un code moins maintenable - à moins que vous ne modifiiez une variable locale et que votre algorithme soit court et / ou trivial, ce qui est hors de la portée 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 "aperçu" serait quelque chose comme: il est plus mathématiquement sain et plus facile à raisonner de ne pas modifier les données (dans la plupart des parties de votre programme) et conduit à un niveau plus élevé et moins technique (ainsi que plus convivial pour les humains, une fois votre cerveau transitions loin de la pensée impérative à l'ancienne) définitions de la logique du programme.

Erik Kaplun
la source
@assylias: logiquement, ce n'était pas faux car il y avait la partie ou ; quoi qu'il en soit, a ajouté une note.
Erik Kaplun
1
La réponse courte est juste. Les one-liners proposés réussiront dans les cas simples mais échoueront dans le cas général.
Stuart marque
La réponse la plus longue est généralement juste, mais la conception de l'API concerne principalement le parallélisme et moins la programmation fonctionnelle. Bien que, bien sûr, il y ait beaucoup de choses à propos de la PF qui se prêtent au parallélisme, ces deux concepts sont donc bien alignés.
Stuart marque
@StuartMarks: Intéressant: dans quels cas la solution fournie dans la réponse d'assylias échoue-t-elle? (et de bons points sur le parallélisme - je suppose que je suis devenu trop désireux de défendre la PF)
Erik Kaplun
@ErikAllik J'ai ajouté une réponse qui couvre ce problème.
Stuart marque
11

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

Quoi qu'il en soit, vous pouvez utiliser le one-liner suivant, si vous avez vraiment besoin de cette fonctionnalité.

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

list.stream().collect(Collectors.toCollection(() -> myExistingList));
Balder
la source
ahh, c'est dommage: P
Erik Kaplun
2
Cette technique échouera horriblement si le flux est exécuté en parallèle.
Stuart marque
1
Il serait de la responsabilité du fournisseur de collecte de s'assurer qu'il n'échoue pas - par exemple en fournissant une collecte simultanée.
Balder
2
Non, ce code enfreint l'exigence de toCollection (), à savoir que le fournisseur renvoie une nouvelle collection vide du type approprié. Même si la destination est thread-safe, la fusion effectuée pour le cas parallèle donnera lieu à des résultats incorrects.
Stuart marque
1
@Balder J'ai ajouté une réponse qui devrait clarifier cela.
Stuart marque
4

Il vous suffit de vous référer à votre liste d'origine pour être celle que le Collectors.toList()retourne.

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 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.

Aman Agnihotri
la source
Je voulais dire comment ajouter / collecter dans une liste existante et non simplement réattribuer.
codefx
1
Eh bien, techniquement, vous ne pouvez pas faire ce genre de choses dans le paradigme de la programmation fonctionnelle, dont les flux sont tout. Dans la programmation fonctionnelle, l'état n'est pas modifié, au lieu de cela, de nouveaux états sont créés dans des structures de données persistantes, ce qui le rend sûr à des fins de concurrence et plus fonctionnel. L'approche que j'ai mentionnée est ce que vous pouvez faire, ou vous pouvez recourir à l'ancienne approche orientée objet dans laquelle vous itérez sur chaque élément et conservez ou supprimez les éléments comme bon vous semble.
Aman Agnihotri
0

Je concaténerais l'ancienne liste et la nouvelle liste en tant que flux et enregistrerais les résultats dans la liste de destination. Fonctionne bien en parallèle aussi.

J'utiliserai l'exemple de réponse acceptée donnée par Stuart Marks:

List<String> destList = Arrays.asList("foo");
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");

destList = Stream.concat(destList.stream(), newList.stream()).parallel()
            .collect(Collectors.toList());
System.out.println(destList);

//output: [foo, 0, 1, 2, 3, 4, 5]

J'espère que cela aide.

Nikos Stais
la source
0

Disons que nous avons une liste existante, et que nous allons utiliser java 8 pour cette activité `

import java.util.*;
import java.util.stream.Collectors;

public class AddingArray {

    public void addArrayInList(){
        List<Integer> list = Arrays.asList(3, 7, 9);

   // And we have an array of Integer type 

        int nums[] = {4, 6, 7};

   //Now lets add them all in list
   // converting array to a list through stream and adding that list to previous list
        list.addAll(Arrays.stream(nums).map(num -> 
                                       num).boxed().collect(Collectors.toList()));
     }
}

»

Dheeraj Kumar
la source
0

targetList = sourceList.stream().flatmap(List::stream).collect(Collectors.toList());

AS Ranjan
la source