Java: adicionando elementos a uma coleção durante a iteração

É possível adicionar elementos a uma coleção enquanto iterar sobre ela?

Mais especificamente, gostaria de fazer uma iteração sobre uma coleção e, se um elemento satisfizer uma determinada condição, quero adicionar alguns outros elementos à coleção e certificar-se de que esses elementos adicionados também sejam iterados. (Eu percebo que isso pode levar a um ciclo não terminador, mas tenho certeza que não será no meu caso.)

O Java Tutorial da Sun sugere que isso não é possível: “Observe que o Iterator.remove é a única maneira segura de modificar uma coleção durante a iteração; o comportamento não é especificado se a coleção subjacente for modificada de qualquer outra forma enquanto a iteração estiver em andamento. ”

Então, se eu não posso fazer o que eu quero fazer usando iteradores, o que você sugere que eu faça?

   

Que tal construir uma fila com os elementos que você deseja iterar? quando você deseja adicionar elementos, enfileire-os no final da fila e continue removendo elementos até que a fila esteja vazia. É assim que uma pesquisa inicial geralmente funciona.

Há duas questões aqui:

A primeira questão é, adicionando a uma Collection após um Iterator é retornado. Conforme mencionado, não há nenhum comportamento definido quando a Collection subjacente é modificada, conforme observado na documentação do Iterator.remove :

… O comportamento de um iterador não é especificado se a coleção subjacente for modificada enquanto a iteração estiver em andamento de qualquer outra forma que não seja chamando esse método.

A segunda questão é que, mesmo que um Iterator possa ser obtido e, em seguida, retornar ao mesmo elemento em que o Iterator estava, não há garantia sobre a ordem da iteração, conforme observado na documentação do método Collection.iterator :

… Não há garantias sobre a ordem na qual os elementos são retornados (a menos que essa coleção seja uma instância de alguma class que forneça uma garantia).

Por exemplo, digamos que temos a lista [1, 2, 3, 4] .

Digamos que 5 foi adicionado quando o Iterator estava em 3 e, de alguma forma, obtemos um Iterator que pode retomar a iteração a partir de 4 . No entanto, não há garantia de que 5 virão depois de 4 . A ordem de iteração pode ser [5, 1, 2, 3, 4] – então o iterador ainda perderá o elemento 5 .

Como não há garantia para o comportamento, não se pode supor que as coisas aconteçam de uma determinada maneira.

Uma alternativa poderia ser ter uma Collection separada à qual os elementos recém-criados possam ser adicionados e, em seguida, iterar sobre esses elementos:

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

Elaborando a resposta da Avi , é possível enfileirar os elementos que desejamos fazer uma iteração em uma fila e remover os elementos enquanto a fila possui elementos. Isso permitirá a “iteração” sobre os novos elementos, além dos elementos originais.

Vamos ver como isso funcionaria.

Conceitualmente, se tivermos os seguintes elementos na fila:

[1, 2, 3, 4]

E, quando removemos 1 , decidimos adicionar 42 , a fila será a seguinte:

[2, 3, 4, 42]

Como a fila é uma estrutura de dados FIFO (first-in, first-out), essa ordenação é típica. (Como observado na documentação da interface da Queue , isso não é uma necessidade de uma Queue . Veja o caso de PriorityQueue que ordena os elementos por sua ordem natural, de modo que não é FIFO.)

A seguir, um exemplo usando um LinkedList (que é uma Queue ) para percorrer todos os elementos junto com elementos adicionais adicionados durante o dequeing. Semelhante ao exemplo acima, o elemento 42 é adicionado quando o elemento 2 é removido:

 Queue queue = new LinkedList(); 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); } 

O resultado é o seguinte:

 1 2 3 4 42 

Como esperado, o elemento 42 que foi adicionado quando atingimos 2 apareceu.

Você também pode querer olhar para alguns dos tipos mais especializados, como ListIterator , NavigableSet e (se você estiver interessado em mapas) NavigableMap .

Na verdade, é bastante fácil. Apenas pense pela melhor maneira. Acredito que a melhor maneira é:

 for (int i=0; i 

O exemplo a seguir funciona perfeitamente no caso mais lógico - quando você não precisa iterar os novos elementos adicionados antes do elemento de iteração. Sobre os elementos adicionados após o elemento de iteração - talvez você não queira iterá-los também. Nesse caso, você deve simplesmente adicionar / ou estender o object yr com um sinalizador que os marcará para não iterá-los.

Usando iteradores … não, acho que não. Você terá que hackear algo assim:

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

Quais yeilds:
[foo, bar, baz, adicionado-item-1, adicionado-item-2]

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

Por exemplo, temos duas 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

Eu prefiro processar collections funcionalmente em vez de alterá-las no lugar. Isso evita esse tipo de problema, bem como problemas de aliasing e outras fonts complicadas de bugs.

Então, eu implementaria assim:

 List expand(List inputs) { List expanded = new ArrayList(); for (Thing thing : inputs) { expanded.add(thing); if (needsSomeMoreThings(thing)) { addMoreThingsTo(expanded); } } return expanded; } 

IMHO a maneira mais segura seria criar uma nova coleção, iterar sobre sua coleção, adicionar cada elemento na nova coleção e adicionar elementos extras conforme necessário na nova coleção, retornando finalmente a nova coleção.

Além da solução de usar uma lista adicional e chamar addAll para inserir os novos itens após a iteração (como, por exemplo, a solução pelo usuário Nat), você também pode usar collections simultâneas como o CopyOnWriteArrayList .

O método de iterador de estilo “instantâneo” usa uma referência ao estado da matriz no ponto que o iterador foi criado. Essa matriz nunca muda durante a vida útil do iterador, portanto, a interferência é impossível e o iterador tem a garantia de não lançar ConcurrentModificationException.

Com essa coleção especial (geralmente usada para access simultâneo), é possível manipular a lista subjacente enquanto iterar sobre ela. No entanto, o iterador não refletirá as alterações.

Isso é melhor que a outra solução? Provavelmente não, eu não sei a sobrecarga introduzida pela abordagem Copy-On-Write.

Dada uma lista List que você deseja iterar, a maneira mais fácil é:

 while (!list.isEmpty()){ Object obj = list.get(0); // do whatever you need to // possibly list.add(new Object obj1); list.remove(0); } 

Então, você percorre uma lista, sempre pegando o primeiro elemento e depois removendo-o. Dessa forma, você pode acrescentar novos elementos à lista durante a iteração.

Esqueça os iteradores, eles não funcionam para adicionar, apenas para remover. Minha resposta se aplica apenas às listas, por isso não me castigue por não resolver o problema das collections. Fique com o básico:

  List myList = new ArrayList(); // 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++; } } 

Eu cansei de ListIterator, mas isso não ajudou meu caso, onde você tem que usar a lista enquanto adiciona a ela. Veja o que funciona para mim:

Use LinkedList .

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

Isso poderia dar uma exceção ou funcionar em loops infinitos. No entanto, como você mencionou

Tenho certeza que não será no meu caso

Verificar casos de canto em tal código é de sua responsabilidade.

Isso é o que eu costumo fazer, com collections como conjuntos:

 Set adds = new HashSet, dels = new HashSet; for ( T e: target ) if (  ) dels.add ( e ); else if (  ) adds.add (  ) target.removeAll ( dels ); target.addAll ( adds ); 

Isso cria alguma memory extra (os pointers para conjuntos intermediários, mas não elementos duplicados acontecem) e extra-etapas (iterando novamente sobre alterações), mas geralmente isso não é grande coisa e pode ser melhor do que trabalhar com uma cópia de coleção inicial.

Mesmo que não possamos adicionar itens à mesma lista durante a iteração, podemos usar o flatMap do Java 8, para adicionar novos elementos a um stream. Isso pode ser feito em uma condição. Depois disso, o item adicionado pode ser processado.

Aqui está um exemplo de Java que mostra como adicionar ao stream contínuo um object, dependendo de uma condição que é então processada com uma condição:

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

A saída do exemplo de brinquedo é:

[2, 3, 21, 4]

Use ListIterator seguinte maneira:

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

A chave é fazer uma iteração na ordem inversa – então os elementos adicionados aparecem na próxima iteração.

Em geral , não é seguro, embora algumas collections possam ser. A alternativa óbvia é usar algum tipo de loop. Mas você não disse qual coleção está usando, o que pode ou não ser possível.