web-dev-qa-db-fra.com

Ajout d'éléments à une collection lors de l'itération

Est-il possible d'ajouter des éléments à une collection en itérant dessus?

Plus précisément, je voudrais parcourir une collection, et si un élément satisfait à une certaine condition, je souhaite ajouter d'autres éléments à la collection et m'assurer que ces éléments ajoutés sont également itérés. (Je me rends compte que ceci pourrait conduire à une boucle sans issue, mais je suis sûr que ce ne sera pas le cas dans mon cas.)

Le Java Tutorial de Sun suggère que ce n’est pas possible: "Notez que Iterator.remove est le seul moyen sûr de modifier une collection pendant l’itération; le comportement n’est pas spécifié si la collection sous-jacente est modifiée manière pendant que l'itération est en cours ".

Donc, si je ne peux pas faire ce que je veux faire en utilisant des itérateurs, que suggérez-vous que je fasse?

71
grifaton

Pourquoi ne pas créer une file d’attente avec les éléments sur lesquels vous souhaitez effectuer une itération; lorsque vous souhaitez ajouter des éléments, mettez-les en file d'attente à la fin de la file d'attente et continuez à supprimer des éléments jusqu'à ce que la file d'attente soit vide. Voici comment fonctionne généralement une recherche par largeur.

59
Avi

Il y a deux problèmes ici:

Le premier problème est, l’ajout à Collection après le retour de Iterator. Comme mentionné, il n'y a pas de comportement défini lorsque la variable Collection sous-jacente est modifiée, comme indiqué dans la documentation de Iterator.remove:

... Le comportement d'un itérateur est non spécifié si le sous-jacent la collection est modifiée alors que le l'itération est en cours de quelque manière que ce soit autrement qu'en appelant cette méthode.

Le deuxième problème est que, même si une Iterator peut être obtenue, puis revenir au même élément où se trouvait Iterator, il n'y a aucune garantie quant à l'ordre de l'itération, comme indiqué dans la documentation de la méthode Collection.iterator :

... Il n'y a aucune garantie concernant le ordre dans lequel les éléments sont retourné (sauf si cette collection est une instance d'une classe qui fournit une garantie ).

Par exemple, supposons que nous avons la liste [1, 2, 3, 4].

Supposons que 5 a été ajouté lorsque la Iterator était à 3 et que, d'une manière ou d'une autre, nous obtenons une Iterator pouvant reprendre l'itération à partir de 4. Cependant, rien ne garantit que 5 viendra après 4. L'ordre d'itération peut être [5, 1, 2, 3, 4] - alors l'itérateur manquera toujours l'élément 5.

Comme il n’ya aucune garantie de comportement, on ne peut pas supposer que les choses se passeront d’une certaine manière.

Une alternative pourrait être d’avoir une Collection à laquelle les éléments nouvellement créés peuvent être ajoutés, puis itérée sur ces éléments:

Collection<String> list = Arrays.asList(new String[]{"Hello", "World!"});
Collection<String> additionalList = new ArrayList<String>();

for (String s : list) {
    // Found a need to add a new element to iterate over,
    // so add it to another list that will be iterated later:
    additionalList.add(s);
}

for (String s : additionalList) {
    // Iterate over the elements that needs to be iterated over:
    System.out.println(s);
}

Modifier

En élaborant sur La réponse d'Avi , il est possible de mettre en file d'attente les éléments que nous voulons itérer et de les supprimer alors que la file contient des éléments. Cela permettra "l'itération" sur les nouveaux éléments en plus des éléments d'origine.

Regardons comment cela fonctionnerait.

Conceptuellement, si nous avons les éléments suivants dans la file d'attente:

[1, 2, 3, 4]

Et, lorsque nous supprimons 1, nous décidons d'ajouter 42, la file d'attente sera la suivante:

[2, 3, 4, 42]

Comme la file d'attente est une structure de données FIFO (premier entré, premier sorti), cet ordre est typique. (Comme indiqué dans la documentation de l'interface Queue , il ne s'agit pas d'une nécessité de Queue. Prenez le cas de PriorityQueue qui ordonne les éléments en fonction de leur ordre naturel, ce n'est donc pas FIFO.)

Vous trouverez ci-dessous un exemple utilisant LinkedList (qui est un Queue ) afin de parcourir tous les éléments, ainsi que des éléments supplémentaires ajoutés lors de la décapage. Semblable à l'exemple ci-dessus, l'élément 42 est ajouté lorsque l'élément 2 est supprimé:

Queue<Integer> queue = new LinkedList<Integer>();
queue.add(1);
queue.add(2);
queue.add(3);
queue.add(4);

while (!queue.isEmpty()) {
    Integer i = queue.remove();
    if (i == 2)
        queue.add(42);

    System.out.println(i);
}

Le résultat est le suivant:

1
2
3
4
42

Comme espéré, l'élément 42 qui a été ajouté lorsque nous avons cliqué sur 2 est apparu.

46
coobird

Vous voudrez peut-être aussi examiner certains types plus spécialisés, comme ListIterator , NavigableSet et (si les cartes vous intéressent) NavigableMap .

8
McDowell

En fait, c'est plutôt facile. Pensez simplement pour le moyen optimal . Je crois que le moyen optimal est:

for (int i=0; i<list.size(); i++) {
   Level obj = list.get(i);

   //Here execute yr code that may add / or may not add new element(s)
   //...

   i=list.indexOf(obj);
}

L'exemple suivant fonctionne parfaitement dans le cas le plus logique - lorsque vous n'avez pas besoin d'itérer les nouveaux éléments ajoutés avant l'élément d'itération. À propos des éléments ajoutés après l'élément d'itération, vous voudrez peut-être également ne pas les itérer. Dans ce cas, vous devriez simplement ajouter/ou étendre votre objet avec un drapeau qui les marquera pour ne pas les itérer.

4
PatlaDJ

Utilisez ListIterator comme suit:

List<String> l = new ArrayList<>();
l.add("Foo");
ListIterator<String> iter = l.listIterator(l.size());
while(iter.hasPrevious()){
    String prev=iter.previous();
    if(true /*You condition here*/){
        iter.add("Bah");
        iter.add("Etc");
    }
}

La clé consiste à itérer dans reverse order - puis les éléments ajoutés apparaissent à la prochaine itération.

2
SteveR
public static void main(String[] args)
{
    // This array list simulates source of your candidates for processing
    ArrayList<String> source = new ArrayList<String>();
    // This is the list where you actually keep all unprocessed candidates
    LinkedList<String> list = new LinkedList<String>();

    // Here we add few elements into our simulated source of candidates
    // just to have something to work with
    source.add("first element");
    source.add("second element");
    source.add("third element");
    source.add("fourth element");
    source.add("The Fifth Element"); // aka Milla Jovovich

    // Add first candidate for processing into our main list
    list.addLast(source.get(0));

    // This is just here so we don't have to have helper index variable
    // to go through source elements
    source.remove(0);

    // We will do this until there are no more candidates for processing
    while(!list.isEmpty())
    {
        // This is how we get next element for processing from our list
        // of candidates. Here our candidate is String, in your case it
        // will be whatever you work with.
        String element = list.pollFirst();
        // This is where we process the element, just print it out in this case
        System.out.println(element);

        // This is simulation of process of adding new candidates for processing
        // into our list during this iteration.
        if(source.size() > 0) // When simulated source of candidates dries out, we stop
        {
            // Here you will somehow get your new candidate for processing
            // In this case we just get it from our simulation source of candidates.
            String newCandidate = source.get(0);
            // This is the way to add new elements to your list of candidates for processing
            list.addLast(newCandidate);
            // In this example we add one candidate per while loop iteration and 
            // zero candidates when source list dries out. In real life you may happen
            // to add more than one candidate here:
            // list.addLast(newCandidate2);
            // list.addLast(newCandidate3);
            // etc.

            // This is here so we don't have to use helper index variable for iteration
            // through source.
            source.remove(0);
        }
    }
}
1
csd

Je sais que c'est assez vieux. Mais pensons à son utilité pour quiconque. Récemment, je suis tombé sur ce problème similaire où il me fallait une file d’attente modifiable pendant l’itération. J'ai utilisé listIterator pour mettre en œuvre la même chose dans les mêmes lignes que ce qu'avait suggéré Avi -> La réponse d'Avi . Voyez si cela conviendrait à votre besoin.

ModifyWhileIterateQueue.Java

import Java.util.ArrayList;
import Java.util.List;
import Java.util.ListIterator;

public class ModifyWhileIterateQueue<T> {
        ListIterator<T> listIterator;
        int frontIndex;
        List<T> list;

        public ModifyWhileIterateQueue() {
                frontIndex = 0;
                list =  new ArrayList<T>();
                listIterator = list.listIterator();
        }

        public boolean hasUnservicedItems () {
                return frontIndex < list.size();  
        }

        public T deQueue() {
                if (frontIndex >= list.size()) {
                        return null;
                }
                return list.get(frontIndex++);
        }

        public void enQueue(T t) {
                listIterator.add(t); 
        }

        public List<T> getUnservicedItems() {
                return list.subList(frontIndex, list.size());
        }

        public List<T> getAllItems() {
                return list;
        }
}

ModifyWhileIterateQueueTest.Java

    @Test
    public final void testModifyWhileIterate() {
            ModifyWhileIterateQueue<String> queue = new ModifyWhileIterateQueue<String>();
            queue.enQueue("one");
            queue.enQueue("two");
            queue.enQueue("three");

            for (int i=0; i< queue.getAllItems().size(); i++) {
                    if (i==1) {
                            queue.enQueue("four");
                    }
            }

            assertEquals(true, queue.hasUnservicedItems());
            assertEquals ("[one, two, three, four]", ""+ queue.getUnservicedItems());
            assertEquals ("[one, two, three, four]", ""+queue.getAllItems());
            assertEquals("one", queue.deQueue());

    }
1
Raj

Par exemple, nous avons deux listes:

  public static void main(String[] args) {
        ArrayList a = new ArrayList(Arrays.asList(new String[]{"a1", "a2", "a3","a4", "a5"}));
        ArrayList b = new ArrayList(Arrays.asList(new String[]{"b1", "b2", "b3","b4", "b5"}));
        merge(a, b);
        a.stream().map( x -> x + " ").forEach(System.out::print);
    }
   public static void merge(List a, List b){
        for (Iterator itb = b.iterator(); itb.hasNext(); ){
            for (ListIterator it = a.listIterator() ; it.hasNext() ; ){
                it.next();
                it.add(itb.next());

            }
        }

    }

a1 b1 a2 b2 a3 b3 a4 b4 a5 b5 

1
Alexander Smirnov

Utiliser des itérateurs ... non, je ne le pense pas. Vous devrez pirater quelque chose comme ça:

    Collection< String > collection = new ArrayList< String >( Arrays.asList( "foo", "bar", "baz" ) );
    int i = 0;
    while ( i < collection.size() ) {

        String curItem = collection.toArray( new String[ collection.size() ] )[ i ];
        if ( curItem.equals( "foo" ) ) {
            collection.add( "added-item-1" );
        }
        if ( curItem.equals( "added-item-1" ) ) {
            collection.add( "added-item-2" );
        }

        i++;
    }

    System.out.println( collection );

Quels ans:
[foo, bar, baz, added-item-1, added-item-2]

1
javamonkey79

Même si nous ne pouvons pas ajouter d'éléments à la même liste pendant l'itération, nous pouvons utiliser le flatMap de Java 8 pour ajouter de nouveaux éléments à un flux. Cela peut être fait à condition. Après cela, l'élément ajouté peut être traité.

Voici un exemple Java qui montre comment ajouter au flux en cours un objet en fonction d'une condition qui est ensuite traitée avec une condition:

List<Integer> intList = new ArrayList<>();
intList.add(1);
intList.add(2);
intList.add(3);

intList = intList.stream().flatMap(i -> {
    if (i == 2) return Stream.of(i, i * 10); // condition for adding the extra items
    return Stream.of(i);
}).map(i -> i + 1)
        .collect(Collectors.toList());

System.out.println(intList);

La sortie de l'exemple de jouet est:

[2, 3, 21, 4]

0
gil.fernandes

C’est ce que je fais habituellement, avec des collections comme des ensembles:

Set<T> adds = new HashSet<T>, dels = new HashSet<T>;
for ( T e: target )
  if ( <has to be removed> ) dels.add ( e );
  else if ( <has to be added> ) adds.add ( <new element> )

target.removeAll ( dels );
target.addAll ( adds );

Cela crée des extra-mémoires (les pointeurs pour les ensembles intermédiaires, mais il n’ya pas d’éléments dupliqués) et des extra-étapes (réitération après modifications), mais ce n’est généralement pas grave et cela peut être mieux que de travailler avec une copie de collection initiale.

0
zakmck

Oubliez les itérateurs, ils ne travaillent pas pour ajouter, mais seulement pour supprimer. Ma réponse ne concerne que les listes, alors ne me punissez pas de ne pas résoudre le problème des collections. S'en tenir à l'essentiel:

    List<ZeObj> myList = new ArrayList<ZeObj>();
    // populate the list with whatever
            ........
    int noItems = myList.size();
    for (int i = 0; i < noItems; i++) {
        ZeObj currItem = myList.get(i);
        // when you want to add, simply add the new item at last and
        // increment the stop condition
        if (currItem.asksForMore()) {
            myList.add(new ZeObj());
            noItems++;
        }
    }
0
Victor Ionescu

Je préfère traiter les collections de manière fonctionnelle plutôt que de les muter en place. Cela évite complètement ce genre de problème, ainsi que les problèmes d'aliasing et autres sources de bogues difficiles.

Donc, je le mettrais en œuvre comme:

List<Thing> expand(List<Thing> inputs) {
    List<Thing> expanded = new ArrayList<Thing>();

    for (Thing thing : inputs) {
        expanded.add(thing);
        if (needsSomeMoreThings(thing)) {
            addMoreThingsTo(expanded);
        }
    }

    return expanded;
}
0
Nat

Étant donné la liste List<Object> que vous souhaitez parcourir, la méthode la plus simple est la suivante:

while (!list.isEmpty()){
   Object obj = list.get(0);

   // do whatever you need to
   // possibly list.add(new Object obj1);

   list.remove(0);
}

Ainsi, vous parcourez une liste, en prenant toujours le premier élément, puis en le supprimant. De cette façon, vous pouvez ajouter de nouveaux éléments à la liste en itérant.

0
Alina

J'ai fatigué ListIterator mais cela n'a pas aidé mon cas, où vous devez utiliser la liste tout en l'ajoutant. Voici ce qui fonctionne pour moi:

Utilisez LinkedList .

LinkedList<String> l = new LinkedList<String>();
l.addLast("A");

while(!l.isEmpty()){
    String str = l.removeFirst();
    if(/* Condition for adding new element*/)
        l.addLast("<New Element>");
    else
        System.out.println(str);
}

Cela pourrait donner une exception ou courir dans des boucles infinies. Cependant, comme vous l'avez mentionné 

Je suis sûr que ça ne va pas dans mon cas

la vérification des cas de coin dans un tel code est de votre responsabilité.

0
Vigya Sharma

IMHO le moyen le plus sûr serait de créer une nouvelle collection, d'itérer sur votre collection donnée, en ajoutant chaque élément de la nouvelle collection et d'ajouter des éléments supplémentaires si nécessaire dans la nouvelle collection, pour finalement renvoyer la nouvelle collection.

0
Michael Zilbermann

Outre la solution consistant à utiliser une liste supplémentaire et à appeler addAll pour insérer les nouveaux éléments après l'itération (comme par exemple la solution de l'utilisateur Nat), vous pouvez également utiliser des collections simultanées telles que/ CopyOnWriteArrayList

La méthode d'itérateur de style "instantané" utilise une référence à l'état du tableau au moment où l'itérateur a été créé. Ce tableau ne change jamais pendant la durée de vie de l'itérateur, de sorte que toute interférence est impossible et il est garanti à l'itérateur de ne pas lancer concurrentModificationException.

Avec cette collection spéciale (généralement utilisée pour les accès simultanés), il est possible de manipuler la liste sous-jacente tout en la parcourant. Cependant, l'itérateur ne reflétera pas les changements.

Est-ce mieux que l'autre solution? Probablement pas, je ne connais pas les frais généraux introduits par l'approche de copie sur écriture.

0
dmeister