Agregar elementos a una colección durante la iteración

81

¿Es posible agregar elementos a una colección mientras se itera sobre ella?

Más específicamente, me gustaría iterar sobre una colección, y si un elemento satisface una determinada condición, quiero agregar algunos otros elementos a la colección y asegurarme de que estos elementos agregados también se repitan. (Me doy cuenta de que esto podría llevar a un bucle indeterminado, pero estoy bastante seguro de que no lo hará en mi caso).

El Tutorial de Java de Sun sugiere que esto no es posible: "Tenga en cuenta que Iterator.removees la única forma segura de modificar una colección durante la iteración; el comportamiento no se especifica si la colección subyacente se modifica de cualquier otra manera mientras la iteración está en curso".

Entonces, si no puedo hacer lo que quiero hacer usando iteradores, ¿qué sugieres que haga?

grifaton
fuente

Respuestas:

62

¿Qué tal construir una cola con los elementos sobre los que desea iterar? cuando desee agregar elementos, colóquelos al final de la cola y siga eliminando elementos hasta que la cola esté vacía. Así es como suele funcionar una búsqueda en amplitud.

Avi
fuente
2
Esta es una buena manera de hacer las cosas si se ajusta al modelo para el que está codificando el OP. De esta manera, no usa un iterador, solo un ciclo while. mientras haya elementos en la cola, procese el primer elemento. Sin embargo, también puede hacer esto con una lista.
Eddie
31
ListIterator iter = list.listIterator() tiene ambos métodos add()y remove(), por lo que puede agregar y eliminar elementos durante la iteración
soulmachine
4
@soulmachine ¿Estás seguro de esto? Si intento hacerlo, obtengo una ConcurrentModificationException.
Nieke Aerts
Creo que tienes razón, pero hay otra opción, usa colecciones seguras para subprocesos comoLinkedBlockingQueue
soulmachine
Creo que todavía hay una brecha aquí. Por ejemplo, si tengo un algoritmo de retroceso, entonces no podré (¿o cómo?) Manejarlo usando un Conjunto a menos que necesite usar hermanos de la interfaz List como sugirió @soulmachine.
salida
46

Hay dos problemas aquí:

El primer problema es agregar a un Collectiondespués de que Iteratorse devuelve un. Como se mencionó, no hay un comportamiento definido cuando Collectionse modifica el subyacente , como se indica en la documentación para Iterator.remove:

... El comportamiento de un iterador no se especifica si la colección subyacente se modifica mientras la iteración está en progreso de cualquier otra forma que no sea llamando a este método.

El segundo problema es que, incluso si se Iteratorpudiera obtener un, y luego regresar al mismo elemento en el que Iteratorestaba, no hay garantía sobre el orden de la iteración, como se indica en la Collection.iteratordocumentación del método:

... No hay garantías con respecto al orden en que se devuelven los elementos (a menos que esta colección sea una instancia de alguna clase que proporcione una garantía).

Por ejemplo, digamos que tenemos la lista [1, 2, 3, 4].

Digamos que 5se agregó cuando Iteratorestaba en 3, y de alguna manera, obtenemos un Iteratorque puede reanudar la iteración 4. Sin embargo, no hay garantía de que 5vendrá después 4. El orden de iteración puede ser [5, 1, 2, 3, 4], entonces el iterador aún perderá el elemento 5.

Como no hay garantía para el comportamiento, no se puede asumir que las cosas sucederán de cierta manera.

Una alternativa podría ser tener un elemento separado Collectional que se puedan agregar los elementos recién creados y luego iterar sobre esos elementos:

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);
}

Editar

Abundando en la respuesta de Avi , es posible hacer cola los elementos que queremos para repetir en una cola, y retirar los elementos, mientras que la cola tiene elementos. Esto permitirá la "iteración" sobre los nuevos elementos además de los elementos originales.

Veamos cómo funcionaría.

Conceptualmente, si tenemos los siguientes elementos en la cola:

[1, 2, 3, 4]

Y, cuando eliminemos 1, decidamos agregar 42, la cola quedará como la siguiente:

[2, 3, 4, 42]

Como la cola es una estructura de datos FIFO (primero en entrar , primero en salir), este orden es típico. (Como se indica en la documentación de la Queueinterfaz, esto no es una necesidad de a Queue. Tomemos el caso de PriorityQueueque ordena los elementos por su orden natural, por lo que no es FIFO).

El siguiente es un ejemplo que usa a LinkedList(que es a Queue) para pasar por todos los elementos junto con los elementos adicionales agregados durante la eliminación de la cola. Al igual que en el ejemplo anterior, el elemento 42se agrega cuando 2se elimina el elemento :

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);
}

El resultado es el siguiente:

1
2
3
4
42

Como era de esperar, apareció el elemento 42que se agregó cuando golpeamos 2.

coobird
fuente
Creo que el punto de Avi fue que si tienes una cola no necesitas iterar sobre ella. Simplemente quita los elementos de la cola del frente mientras no está vacío y agrega nuevos elementos en la cola en la parte posterior.
Nat
@Nat: Tienes razón, gracias por señalar eso. Edité mi respuesta para reflejar eso.
coobird
1
@coobird Por alguna razón, su respuesta está truncada. [...] para pasar por todos los elementos junto con el adicional el— y eso es todo lo que puedo ver, sin embargo, si intento editar la respuesta, todo está ahí. ¿Alguna idea de lo que está pasando?
Kohányi Róbert
4

De hecho, es bastante fácil. Piense en la forma óptima. Creo que la forma óptima es:

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);
}

El siguiente ejemplo funciona perfectamente en el caso más lógico, cuando no necesita iterar los elementos nuevos agregados antes del elemento de iteración. Acerca de los elementos agregados después del elemento de iteración, es posible que tampoco desee iterarlos. En este caso, simplemente debe agregar / o extender el objeto yr con una bandera que los marcará para no iterarlos.

PatlaDJ
fuente
El indexOf no es necesario para agregar y podría resultar confuso si tiene duplicados.
Peter Lawrey
Sí, de hecho, los duplicados son un problema. Gracias por agregar eso.
PatlaDJ
Debe agregarse que, dependiendo de la implementación real de la lista, list.get (i) puede ser mucho más costoso que usar un iterador. Podría haber una penalización de rendimiento considerable al menos para listas vinculadas más grandes, por ejemplo)
Stefan Winkler
4

Úselo de la ListIteratorsiguiente manera:

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 clave es iterar en orden inverso ; luego, los elementos agregados aparecen en la siguiente iteración.

SteveR
fuente
2

Sé que ha sido bastante antiguo. Pero pensé que sería útil para cualquier otra persona. Recientemente me encontré con este problema similar en el que necesito una cola que se pueda modificar durante la iteración. Usé listIterator para implementar lo mismo en las mismas líneas que lo que sugirió Avi -> Respuesta de Avi . Vea si esto se adapta a sus necesidades.

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());

    }
Raj
fuente
1

Usando iteradores ... no, no lo creo. Tendrás que hackear juntos algo como esto:

    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 );

Qué resultados:
[foo, bar, baz, added-item-1, added-item-2]

javamonkey79
fuente
1

Además de la solución de usar una lista adicional y llamar a addAll para insertar los nuevos elementos después de la iteración (como, por ejemplo, la solución del usuario Nat), también puede usar colecciones concurrentes como CopyOnWriteArrayList .

El método de iterador de estilo "instantánea" utiliza una referencia al estado de la matriz en el punto en que se creó el iterador. Esta matriz nunca cambia durante la vida útil del iterador, por lo que la interferencia es imposible y se garantiza que el iterador no lanzará ConcurrentModificationException.

Con esta colección especial (generalmente utilizada para acceso concurrente) es posible manipular la lista subyacente mientras se itera sobre ella. Sin embargo, el iterador no reflejará los cambios.

¿Es esto mejor que la otra solución? Probablemente no, no conozco la sobrecarga introducida por el enfoque Copy-On-Write.

dmeister
fuente
1
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);
        }
    }
}
csd
fuente
1

Por ejemplo, tenemos dos listas:

  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

Alexander Smirnov
fuente
0

Prefiero procesar colecciones funcionalmente en lugar de mutarlas en su lugar. Eso evita este tipo de problema por completo, así como los problemas de aliasing y otras fuentes complicadas de errores.

Entonces, lo implementaría como:

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;
}
Nat
fuente
0

En mi humilde opinión, la forma más segura sería crear una nueva colección, iterar sobre su colección dada, agregar cada elemento en la nueva colección y agregar elementos adicionales según sea necesario en la nueva colección también, finalmente devolviendo la nueva colección.

Michael Zilbermann
fuente
0

Dada una lista sobre la List<Object>que desea iterar, la forma más sencilla es:

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

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

   list.remove(0);
}

Entonces, iteras a través de una lista, siempre tomando el primer elemento y luego eliminándolo. De esta manera, puede agregar nuevos elementos a la lista mientras itera.

Alina
fuente
0

Olvídese de los iteradores, no sirven para agregar, solo para eliminar. Mi respuesta se aplica solo a las listas, así que no me castigue por no resolver el problema de las colecciones. Cíñete a lo básico:

    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++;
        }
    }
Victor Ionescu
fuente
Gracias Stefan. Arreglado.
Victor Ionescu
0

Cansé ListIterator pero no ayudó en mi caso, donde tienes que usar la lista mientras agregas. Esto es lo que funciona para mí:

Utilice 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);
}

Esto podría dar una excepción o encontrarse con bucles infinitos. Sin embargo, como has mencionado

Estoy bastante seguro de que no lo hará en mi caso

comprobar los casos de las esquinas en dicho código es su responsabilidad.

Vigya Sharma
fuente
0

Esto es lo que suelo hacer, con colecciones como conjuntos:

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 );

Esto crea algo de memoria adicional (los punteros para conjuntos intermedios, pero no ocurren elementos duplicados) y pasos adicionales (iterando nuevamente sobre los cambios), sin embargo, generalmente eso no es un gran problema y podría ser mejor que trabajar con una copia de colección inicial.

zakmck
fuente
0

Aunque no podemos agregar elementos a la misma lista durante la iteración, podemos usar flatMap de Java 8 para agregar nuevos elementos a una secuencia. Esto se puede hacer con una condición. Después de esto, se puede procesar el artículo agregado.

Aquí hay un ejemplo de Java que muestra cómo agregar un objeto a la transmisión en curso dependiendo de una condición que luego se procesa con una condición:

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);

El resultado del ejemplo del juguete es:

[2, 3, 21, 4]

gil.fernandes
fuente
-1

En general , no es seguro, aunque para algunas colecciones puede serlo. La alternativa obvia es utilizar algún tipo de bucle for. Pero no dijiste qué colección estás usando, por lo que puede que sea posible o no.

Matthew Flaschen
fuente