在迭代期间将元素添加到集合中


81

在迭代集合时是否可以向集合中添加元素?

更具体地说,我想遍历一个集合,如果一个元素满足特定条件,我想向集合中添加一些其他元素,并确保也对这些添加的元素进行遍历。(我意识到这可能会导致循环终止,但是我很确定不会发生这种情况。)

Sun的Java教程建议不可能:“请注意,这Iterator.remove是在迭代过程中修改集合的唯一安全方法;如果在迭代过程中对基础集合进行了其他任何修改,则行为未指定。”

因此,如果我不能使用迭代器做我想做的事情,那么你建议我做些什么?

Answers:


62

如何使用要迭代的元素构建队列?当您要添加元素时,将它们放入队列的末尾,并继续删除元素,直到队列为空。广度优先搜索通常是这样工作的。


2
如果它适合OP所编码的模型,那么这是做事的好方法。这样,您就无需使用迭代器,而只是使用while循环。当队列中有元素时,处理第一个元素。但是,您也可以使用列表来执行此操作。
Eddie

31
ListIterator iter = list.listIterator() 同时拥有add()remove()方法,因此您可以在迭代过程中添加和删除元素
soulmachine 2015年

4
@soulmachine您确定吗?如果我尝试这样做,则会收到ConcurrentModificationException。
Nieke Aerts

我认为您是正确的,但还有另一种选择,使用线程安全集合,例如LinkedBlockingQueue
soulmachine

我相信这里仍然有差距。例如,如果我有一个回溯算法,那么除非我确实需要按照@soulmachine建议使用List接口的同级对象,否则我将无法(或如何使用)Set来处理它。
stdout

46

这里有两个问题:

第一个问题是,CollectionIterator返回后添加到中。如上所述,Collection修改基础时没有定义的行为,如文档中所述Iterator.remove

...如果在迭代进行过程中以其他方式(而不是通过调用此方法)修改了基础集合,则迭代器的行为是不确定的。

第二个问题是,即使Iterator可以获取an ,然后返回到Iteratorat所在的相同元素,也不能保证迭代的顺序,如Collection.iterator方法文档中所述:

...没有关于元素返回顺序的保证(除非此集合是某个提供保证的类的实例)。

例如,假设我们有list [1, 2, 3, 4]

假设是在at5时添加的,以某种方式,我们得到了一个可以从继续迭代的对象。但是,没有保证人会追随。迭代顺序可能是-则迭代器仍会丢失该元素。Iterator3Iterator454[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先进先出)数据结构,因此这种排序是典型的。(如Queue接口文档中所述,这不是必需的Queue。以PriorityQueue按自然顺序对元素进行排序的情况为例,因此不是FIFO。)

下面是一个示例(使用a)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


我认为Avi的意思是,如果您有一个队列,则不需要对其进行迭代。您可以在不为空的情况下从前面将元素排出队列,然后将新元素排入后面。
纳特

@Nat:你是对的,谢谢你指出这一点。我已经修改了答案以反映这一点。
coobird

1
@coobird由于某种原因,您的答案被截断了。[...]以了解所有元素以及其他el-,这就是我所能看到的全部,但是,如果我尝试编辑答案,一切都将存在。有什么想法吗?
Kohányi罗伯特·


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 2010年

应该补充的是,取决于实际的列表实现,list.get(i)可能比使用迭代器昂贵得多。至少对于较大的链表,可能会有相当大的性能损失,例如)
Stefan Winkler 2013年

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的Answers相同的行中实现了相同的功能。看看这是否适合您的需求。

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,add -item-1,added-item-2]


1

除了使用附加列表并调用addAll在迭代后插入新项目的解决方案(例如,用户Nat的解决方案)之外,您还可以使用并发集合,如CopyOnWriteArrayList

“快照”样式的迭代器方法在创建迭代器时使用对数组状态的引用。此数组在迭代器的生命周期内永不更改,因此不会发生干扰,并且保证迭代器不会引发ConcurrentModificationException。

使用此特殊集合(通常用于并发访问),可以在迭代基础列表时对其进行操作。但是,迭代器不会反映所做的更改。

这比其他解决方案好吗?可能不是,我不知道写时复制方法带来的开销。


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

恕我直言,更安全的方法是创建一个新集合,迭代给定的集合,在新集合中添加每个元素,并根据需要在新集合中添加额外的元素,最后返回新集合。


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

谢谢斯蒂芬。固定它。
Victor Ionescu

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

即使我们不能在迭代过程中将项目添加到同一列表中,也可以使用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]


-1

一般的,这不是安全的,但对于一些藏品可能。显而易见的替代方法是使用某种for循环。但是您没有说您正在使用什么集合,因此可能或不可能。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.