Java: добавление элементов в коллекцию во время итерации

Можно ли добавлять элементы в коллекцию, итерации по ней?

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

Java Tutorial от Sun предполагает, что это невозможно: "Обратите внимание, что Iterator.remove - единственный безопасный способ изменения коллекции во время итерации, поведение неуказано, если базовая коллекция модифицируется каким-либо другим способом, пока выполняется итерация."

Итак, если я не могу делать то, что хочу делать с помощью итераторов, что вы предлагаете мне сделать?

Ответ 1

Как создать очередь с элементами, которые вы хотите перебрать; когда вы хотите добавить элементы, запишите их в конце очереди и продолжайте удалять элементы до тех пор, пока очередь не будет пустой. Вот как обычно работает поиск по ширине.

Ответ 2

Здесь есть две проблемы:

Первая проблема заключается в добавлении к 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);
}

Edit

Разрабатывая Avi answer, можно поставить в очередь элементы, которые мы хотим перебрать в очередь, и удалить элементы, в то время как в очереди есть элементы. Это позволит "итерации" над новыми элементами в дополнение к исходным элементам.

Посмотрим, как это будет работать.

Концептуально, если в очереди есть следующие элементы:

[1, 2, 3, 4]

И, когда мы удаляем 1, мы решили добавить 42, очередь будет следующей:

[2, 3, 4, 42]

Поскольку очередь представляет собой структуру данных FIFO (first-in, first-out), это упорядочение является типичным. (Как указано в документации для интерфейса Queue, это не является необходимостью Queue. Возьмем случай PriorityQueue, который упорядочивает элементы по их естественному порядку, так что не FIFO.)

Ниже приведен пример использования LinkedList (который является 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.

Ответ 3

Вы также можете посмотреть некоторые из более специализированных типов, например ListIterator, NavigableSet и (если вас интересуют карты) NavigableMap.

Ответ 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 с помощью флага, который будет отмечать их не для их повторения.

Ответ 5

Использование итераторов... нет, я так не думаю. Вам придется взломать что-то вроде этого:

    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]

Ответ 6

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

Ответ 7

Для примера у нас есть два списка:

  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

Ответ 8

Я предпочитаю обрабатывать коллекции функционально, а не мутировать их. Это позволяет избежать такой проблемы в целом, а также проблемы с псевдонимом и другие сложные ошибки.

Итак, я бы выполнил его как:

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

Ответ 9

IMHO более безопасным способом было бы создание новой коллекции, повторение этой коллекции, добавление каждого элемента в новую коллекцию и добавление дополнительных элементов по мере необходимости в новую коллекцию, а также возвращение новой коллекции.

Ответ 10

Помимо решения использования дополнительного списка и вызова addAll для вставки новых элементов после итерации (например, для решения пользователем Nat), вы также можете использовать параллельные коллекции, такие как CopyOnWriteArrayList.

Метод итератора стиля "моментальный снимок" использует ссылку на состояние массива в точке, в которой был создан итератор. Этот массив никогда не изменяется в течение жизни итератора, поэтому помехи невозможны, и итератору гарантировано не бросать ConcurrentModificationException.

С помощью этой специальной коллекции (обычно используемой для одновременного доступа) можно манипулировать базовым списком, итерации по нему. Тем не менее, итератор не будет отражать изменения.

Это лучше, чем другое решение? Наверное, нет, я не знаю накладных расходов, введенных с помощью подхода Copy-On-Write.

Ответ 11

Учитывая список List<Object>, который вы хотите перебрать, простой способ:

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

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

   list.remove(0);
}

Итак, вы перебираете список, всегда беря первый элемент и удаляя его. Таким образом, вы можете добавлять новые элементы в список во время итерации.

Ответ 12

Забудьте об итераторах, они не работают для добавления, только для удаления. Мой ответ относится только к спискам, поэтому не наказывайте меня за то, что вы не решили проблему для коллекций. Придерживайтесь основ:

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

Ответ 13

Я устал 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);
}

Это может привести к исключению или запуску в бесконечные циклы. Однако, как вы уже упоминали

Я уверен, что это не будет в моем случае

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

Ответ 14

Это то, что я обычно делаю, с наборами, подобными наборам:

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

Это создает некоторую дополнительную память (указатели для промежуточных наборов, но не дублируются элементы) и дополнительные шаги (повторяющиеся снова по изменениям), однако обычно это не очень важно, и это может быть лучше, чем работа с начальным сбор копия.

Ответ 15

Несмотря на то, что мы не можем добавлять элементы в один и тот же список во время итерации, мы можем использовать Java 8 flatMap для добавления новых элементов в поток. Это можно сделать при условии. После этого можно обрабатывать добавленный элемент.

Вот пример 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]

Ответ 16

Используйте 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");
    }
}

Ключ к итерации в обратном порядке - тогда добавленные элементы появляются на следующей итерации.

Ответ 17

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