Додавання елементів до колекції під час ітерації


82

Чи можна додавати елементи до колекції під час ітерації над нею?

Більш конкретно, я хотів би переглядати колекцію, і якщо елемент задовольняє певній умові, я хочу додати до колекції деякі інші елементи та переконатися, що ці додані елементи також повторюються. (Я усвідомлюю, що це може призвести до неперервної петлі, але я майже впевнений, що в моєму випадку це не стане.)

Java Tutorial від Sun пропонує це неможливо: «Зверніть увагу , що Iterator.removeце тільки безпечний спосіб змінити колекцію під час ітерації, поведінка не визначено , якщо базова колекція була змінена будь-яким іншим способом , в той час як ітерація триває.»

Тож якщо я не можу робити те, що хочу, використовуючи ітератори, що ви пропонуєте мені робити?

Відповіді:


62

Як щодо побудови черги з елементами, які потрібно переглядати; коли ви хочете додати елементи, поставте їх у кінці черги і продовжуйте видаляти елементи, поки черга не порожня. Ось як зазвичай працює пошук із широтою.


2
Це хороший спосіб зробити щось, якщо це відповідає моделі, для якої кодується OP. Таким чином, ви не використовуєте ітератор - лише цикл while. поки в черзі є елементи, обробіть перший елемент. Однак ви можете зробити це і зі Списком.
Едді

31
ListIterator iter = list.listIterator() має add()і remove()методи, і ви можете додавати та видаляти елементи під час ітерації
soulmachine

4
@soulmachine Ви впевнені в цьому? Якщо я спробую це зробити, я отримую ConcurrentModificationException.
Nieke Aerts

Думаю, ви праві, але є ще один варіант, скористайтеся LinkedBlockingQueue
нитко

Я вважаю, що тут все ще є прогалина. Наприклад, якщо у мене є алгоритм зворотного відстеження, то я не зможу (або як?) Обробляти його за допомогою Set, якщо мені не потрібно використовувати братів і сестер інтерфейсу List, як запропонував @soulmachine.
stdout

46

Тут є дві проблеми:

Перше питання полягає в додаванні до an Collectionпісля Iteratorповернення. Як уже згадувалося, немає визначеної поведінки, коли базовий матеріал Collectionмодифікований, як зазначено в документації для Iterator.remove:

... Поведінка ітератора не визначена, якщо основна колекція модифікується під час виконання ітерації будь-яким іншим способом, крім виклику цього методу.

Друге питання полягає в тому, що навіть якщо Iteratorможна було отримати та повернутись до того самого елемента, в якому Iteratorбув, немає гарантії щодо порядку ітерації, як зазначено в Collection.iteratorдокументації до методу:

... Немає гарантій щодо порядку повернення елементів (якщо ця колекція не є екземпляром якогось класу, що забезпечує гарантію).

Наприклад, скажімо, у нас є список [1, 2, 3, 4].

Скажімо, 5було додано, коли Iteratorбуло в 3, і якимось чином ми отримуємо Iteratorте, що може відновити ітерацію з 4. Однак немає гарантії, яка 5настане після 4. Порядок ітерацій може бути [5, 1, 2, 3, 4]- тоді ітератор все одно буде пропускати елемент 5.

Оскільки гарантії поведінки немає, не можна припускати, що все відбуватиметься певним чином.

Однією з альтернатив може бути окремий Collectionрозділ, до якого можна додавати щойно створені елементи, а потім перебирати ці елементи:

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

Редагувати

Розробляючи відповідь Avi , можна поставити в чергу елементи, які ми хочемо перетворити в чергу, і видалити елементи, поки в черзі є елементи. Це дозволить зробити "ітерацію" над новими елементами на додаток до вихідних елементів.

Давайте подивимося, як це могло б працювати.

Концептуально, якщо в черзі є такі елементи:

[1, 2, 3, 4]

І коли ми видалимо 1, ми вирішимо додати 42, черга буде такою:

[2, 3, 4, 42]

Оскільки черга являє собою структуру даних FIFO (first-in, first-out), це впорядкування є типовим. (Як зазначено в документації до Queueінтерфейсу, це не є необхідністю a Queue. Візьмемо випадок, PriorityQueueколи елементи впорядковуються за їх природним упорядкуванням, тож це не FIFO.)

Далі наведено приклад використання LinkedList(що є a Queue) для того, щоб пройти всі елементи разом з додатковими елементами, доданими під час видалення. Подібно до прикладу вище, елемент 42додається при 2видаленні елемента :

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

Результат такий:

1
2
3
4
42

Як і сподівалося, елемент, 42який був доданий, коли ми вдарили, 2з’явився.


Я думаю, що сенс Avi полягав у тому, що якщо у вас є черга, вам не потрібно перебирати її. Ви просто декеруєте елементи спереду, поки він не порожній, і виставляєте нові елементи ззаду.
Нат

@Nat: Ви маєте рацію, дякую, що вказали на це. Я відредагував свою відповідь, щоб це відобразити.
coobird

1
@coobird З якихось причин ваша відповідь усічена. [...] для того, щоб пройти всі елементи разом із додатковими el - і це все, що я бачу, однак, якщо я спробую відредагувати відповідь, все є. Будь-яка ідея про те, що відбувається?
Кохані Руберт


4

Насправді це досить просто. Подумайте лише про оптимальний спосіб. Я вважаю, що оптимальним способом є:

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

Наступний приклад чудово працює в найбільш логічному випадку - коли вам не потрібно повторювати додані нові елементи перед елементом ітерації. Про додані елементи після елемента ітерації - там, можливо, ви також не хочете їх повторювати. У цьому випадку вам слід просто додати / або розширити yr-об'єкт прапором, який позначить їх, щоб не повторювати.


IndexOf не потрібен для додавання і може заплутати, якщо у вас є дублікати.
Пітер Лорі

Так, справді, дублікати - це проблема. Подяка за додавання цього.
PatlaDJ

Слід додати, що залежно від фактичної реалізації списку list.get (i) може бути набагато дорожчим, ніж використання ітератора. Можливо, буде застосовано значне покарання за продуктивність, принаймні, для більших пов'язаних списків, наприклад)
Стефан Вінклер

4

Використовуйте ListIteratorнаступне:

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

Ключ полягає в ітерації у зворотному порядку - тоді додані елементи з’являться на наступній ітерації.


2

Я знаю, що це було досить давно. Але думав про його користь для когось іншого. Нещодавно я зіткнувся з подібною проблемою, коли мені потрібна черга, яку можна змінювати під час ітерації. Я використовував listIterator, щоб реалізувати те ж саме в тих самих рядках, що й те, що запропонував Avi -> Відповідь Avi . Подивіться, чи підходить це для ваших потреб.

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

Використання ітераторів ... ні, я не думаю. Вам доведеться зламати щось на зразок цього:

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

Які молоді:
[foo, bar, baz, added-item-1, added-item-2]


1

Окрім рішення використання додаткового списку та виклику addAll для вставки нових елементів після ітерації (як, наприклад, рішення користувача Nat), ви також можете використовувати паралельні колекції, такі як CopyOnWriteArrayList .

Метод ітератора стилю "моментальний знімок" використовує посилання на стан масиву в момент створення ітератора. Цей масив ніколи не змінюється протягом життя ітератора, тому перешкоди неможливі, ітератор гарантовано не викине ConcurrentModificationException.

За допомогою цієї спеціальної колекції (зазвичай використовується для одночасного доступу) можна маніпулювати базовим списком, переглядаючи його. Однак ітератор не відображатиме змін.

Це краще за інше рішення? Можливо, ні, я не знаю накладних витрат, введених підходом Copy-On-Write.


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

1

Для прикладу ми маємо два списки:

  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


0

Я вважаю за краще обробляти колекції функціонально, а не мутувати їх на місці. Це дозволяє уникнути такого роду проблем взагалі, а також псевдонімів та інших хитрих джерел помилок.

Отже, я б реалізував це як:

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

IMHO найбезпечнішим способом було б створити нову колекцію, переглядати дану колекцію, додаючи кожен елемент до нової колекції, а також додаючи додаткові елементи за необхідністю до нової колекції, нарешті, повертаючи нову колекцію.


0

Враховуючи список, List<Object>який ви хочете повторити, простий спосіб:

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

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

   list.remove(0);
}

Отже, ви переглядаєте список, завжди берете перший елемент, а потім видаляєте його. Таким чином ви можете додавати нові елементи до списку під час ітерації.


0

Забудьте про ітератори, вони не працюють для додавання, лише для видалення. Моя відповідь стосується лише списків, тому не карайте мене за те, що я не вирішив проблему для колекцій. Дотримуйтесь основ:

    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

Я втомився ListIterator, але це не допомогло моєму випадку, коли вам потрібно використовувати список під час додавання до нього. Ось, що мені підходить:

Використовуйте 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);
}

Це може дати виняток або натрапити на нескінченні цикли. Однак, як ви вже згадували

Я майже впевнений, що в моєму випадку цього не буде

перевірка кутових справ у такому коді - ваша відповідальність.


0

Це те, що я зазвичай роблю з такими колекціями, як набори:

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

Це створює деяку додаткову пам’ять (вказівники на проміжні набори, але не повторюються елементи) і додаткові кроки (повторне повторення змін), однак зазвичай це не є великою проблемою, і це може бути краще, ніж робота з початковою копією колекції.


0

Незважаючи на те, що ми не можемо додати елементи до одного списку під час ітерації, ми можемо використовувати flatMap Java 8, щоб додати нові елементи до потоку. Це можна зробити за умови. Після цього доданий елемент можна обробити.

Ось приклад Java, який показує, як додати до поточного потоку об’єкт залежно від умови, яка потім обробляється умовою:

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

Результатом прикладу іграшки є:

[2, 3, 21, 4]


-1

В загальному , це не безпечно, хоча для деяких колекцій може бути. Очевидною альтернативою є використання якогось циклу for. Але ви не сказали, яку колекцію ви використовуєте, так що це може бути, а може - неможливо.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.