Thêm các phần tử vào một tập hợp trong quá trình lặp lại


81

Có thể thêm các phần tử vào một bộ sưu tập trong khi lặp lại nó không?

Cụ thể hơn, tôi muốn lặp lại một tập hợp và nếu một phần tử thỏa mãn một điều kiện nhất định, tôi muốn thêm một số phần tử khác vào tập hợp và đảm bảo rằng những phần tử đã thêm này cũng được lặp lại. (Tôi nhận ra rằng điều này có thể dẫn đến một vòng lặp không kết thúc, nhưng tôi khá chắc chắn rằng nó sẽ không xảy ra trong trường hợp của tôi.)

Các Java Tutorial của Sun đề nghị này là không thể: "Lưu ý rằng Iterator.removechỉ . Cách an toàn để sửa đổi một bộ sưu tập trong suốt lặp; hành vi này là không xác định nếu bộ sưu tập cơ bản được sửa đổi trong bất kỳ cách nào khác trong khi lặp đi lặp lại là cơ bản dở dang"

Vì vậy, nếu tôi không thể làm những gì tôi muốn làm bằng cách sử dụng trình vòng lặp, bạn khuyên tôi nên làm gì?

Câu trả lời:


62

Làm thế nào về việc xây dựng một Hàng đợi với các phần tử bạn muốn lặp lại; khi bạn muốn thêm các phần tử, hãy xếp chúng ở cuối hàng đợi và tiếp tục xóa các phần tử cho đến khi hàng đợi trống. Đây là cách tìm kiếm theo chiều rộng thường hoạt động.


2
Đây là một cách tốt để thực hiện mọi việc nếu nó phù hợp với mô hình mà OP đang mã hóa. Bằng cách này, bạn không sử dụng trình lặp - chỉ là vòng lặp while. trong khi có các phần tử trong hàng đợi, hãy xử lý phần tử đầu tiên. Tuy nhiên, bạn có thể làm điều này với một Danh sách.
Eddie

31
ListIterator iter = list.listIterator() có cả add()remove()phương pháp, vì vậy bạn có thể thêm và loại bỏ các yếu tố trong quá trình lặp đi lặp lại
soulmachine

4
@soulmachine Bạn có chắc chắn về điều này không? Nếu tôi cố gắng làm như vậy, tôi nhận được ConcurrentModificationException.
Nieke Aerts

Tôi nghĩ rằng bạn đã đúng, nhưng có một lựa chọn, sử dụng thread-safe bộ sưu tập nhưLinkedBlockingQueue
soulmachine

Tôi tin rằng vẫn còn một khoảng trống ở đây. Ví dụ: nếu tôi có một thuật toán quay lui, thì tôi sẽ không thể (hoặc làm thế nào?) Xử lý nó bằng Bộ trừ khi tôi cần sử dụng anh chị em của giao diện Danh sách như @soulmachine đã đề xuất.
stdout

46

Có hai vấn đề ở đây:

Vấn đề đầu tiên là, thêm vào một Collectionsau khi một Iteratorđược trả về. Như đã đề cập, không có hành vi xác định khi cơ bản Collectionđược sửa đổi, như đã lưu ý trong tài liệu cho Iterator.remove:

... Hành vi của một trình lặp là không xác định nếu tập hợp cơ bản được sửa đổi trong khi quá trình lặp đang diễn ra theo bất kỳ cách nào khác ngoài việc gọi phương thức này.

Vấn đề thứ hai là, ngay cả khi một phần tử Iteratorcó thể được lấy, và sau đó quay trở lại cùng một phần tử Iteratorlúc đó, không có gì đảm bảo về thứ tự của lần lặp, như đã lưu ý trong Collection.iteratortài liệu phương pháp:

... Không có đảm bảo nào liên quan đến thứ tự mà các phần tử được trả về (trừ khi tập hợp này là một thể hiện của một số lớp cung cấp một bảo đảm).

Ví dụ, giả sử chúng ta có danh sách [1, 2, 3, 4].

Giả sử nó 5đã được thêm vào Iteratorlúc nào 3, và bằng cách nào đó, chúng ta nhận được một Iteratorcái có thể tiếp tục lặp lại từ đó 4. Tuy nhiên, không có người giám hộ nào 5sẽ đến sau 4. Thứ tự lặp có thể là [5, 1, 2, 3, 4]- sau đó trình lặp sẽ vẫn bỏ sót phần tử 5.

Vì không có gì đảm bảo cho hành vi, người ta không thể cho rằng mọi thứ sẽ xảy ra theo một cách nhất định.

Một cách thay thế có thể là có một phần riêng biệt Collectionmà các phần tử mới tạo có thể được thêm vào và sau đó lặp lại các phần tử đó:

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

Biên tập

Xây dựng trên câu trả lời của Avi , có thể xếp các phần tử mà chúng ta muốn lặp lại vào một hàng đợi và loại bỏ các phần tử trong khi hàng đợi có các phần tử. Điều này sẽ cho phép "lặp lại" các phần tử mới ngoài các phần tử ban đầu.

Hãy xem nó sẽ hoạt động như thế nào.

Về mặt khái niệm, nếu chúng ta có các phần tử sau trong hàng đợi:

[1, 2, 3, 4]

Và, khi chúng tôi xóa 1, chúng tôi quyết định thêm 42, hàng đợi sẽ như sau:

[2, 3, 4, 42]

Vì hàng đợi là cấu trúc dữ liệu FIFO (nhập trước, xuất trước), thứ tự này là điển hình. (Như đã lưu ý trong tài liệu về Queuegiao diện, đây không phải là điều cần thiết của a Queue. Lấy trường hợp PriorityQueuesắp xếp thứ tự các phần tử theo thứ tự tự nhiên của chúng, vì vậy đó không phải là FIFO.)

Sau đây là một ví dụ sử dụng LinkedList(là a Queue) để đi qua tất cả các phần tử cùng với các phần tử bổ sung được thêm vào trong quá trình dequeing. Tương tự như ví dụ trên, phần tử 42được thêm vào khi phần tử 2bị xóa:

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

Kết quả là như sau:

1
2
3
4
42

Như hy vọng, phần tử 42được thêm vào khi chúng tôi nhấn đã 2xuất hiện.


Tôi nghĩ rằng quan điểm của Avi là nếu bạn có một hàng đợi, bạn không cần phải lặp lại nó. Bạn chỉ xếp hàng các phần tử từ phía trước trong khi nó không trống và xếp hàng các phần tử mới ở phía sau.
Nat

@Nat: Bạn nói đúng, cảm ơn bạn đã chỉ ra điều đó. Tôi đã chỉnh sửa câu trả lời của mình để phản ánh điều đó.
coobird

1
@coobird Vì lý do nào đó mà câu trả lời của bạn bị cắt ngắn. [...] để xem qua tất cả các yếu tố cùng với thêm el— và đó là tất cả những gì tôi có thể thấy, tuy nhiên nếu tôi thử và chỉnh sửa câu trả lời thì mọi thứ vẫn ở đó. Bất kỳ ý tưởng về những gì đang xảy ra?
Kohányi Róbert


4

Trên thực tế nó là khá dễ dàng. Chỉ cần suy nghĩ cho cách tối ưu. Tôi tin rằng cách tối ưu là:

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

Ví dụ sau hoạt động hoàn hảo trong trường hợp hợp lý nhất - khi bạn không cần lặp lại các phần tử mới đã thêm trước phần tử lặp. Về các phần tử được thêm vào sau phần tử lặp - ở đó bạn có thể không muốn lặp lại chúng. Trong trường hợp này, bạn chỉ cần thêm / hoặc mở rộng đối tượng năm với một cờ sẽ đánh dấu chúng không lặp lại chúng.


IndexOf không cần thiết để thêm và có thể gây nhầm lẫn nếu bạn có các bản sao.
Peter Lawrey

Vâng, thực sự, các bản sao là một vấn đề. Thanx vì đã thêm điều đó.
PatlaDJ

Cần phải nói thêm rằng, tùy thuộc vào việc triển khai danh sách thực tế, list.get (i) có thể đắt hơn nhiều so với việc sử dụng trình lặp. Có thể có một hình phạt đáng kể hiệu suất ít nhất là cho danh sách liên kết lớn hơn, ví dụ)
Stefan Winkler

4

Sử dụng ListIteratornhư sau:

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

Điều quan trọng là lặp lại theo thứ tự ngược lại - sau đó các phần tử được thêm vào sẽ xuất hiện trong lần lặp tiếp theo.


2

Tôi biết nó đã khá cũ. Nhưng nghĩ rằng nó có ích gì cho bất kỳ ai khác. Gần đây tôi đã gặp phải vấn đề tương tự này, nơi tôi cần một hàng đợi có thể sửa đổi trong quá trình lặp lại. Tôi đã sử dụng listIterator để triển khai nhiều dòng giống như những gì Avi đề xuất -> Câu trả lời của Avi . Xem nếu điều này sẽ phù hợp với nhu cầu của bạn.

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

Sử dụng trình lặp ... không, tôi không nghĩ vậy. Bạn sẽ phải hack với nhau một cái gì đó như thế này:

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

Loại men nào:
[foo, bar, baz, added-item-1, added-item-2]


1

Bên cạnh giải pháp sử dụng danh sách bổ sung và gọi addAll để chèn các mục mới sau lần lặp (ví dụ: giải pháp của người dùng Nat), bạn cũng có thể sử dụng các bộ sưu tập đồng thời như CopyOnWriteArrayList .

Phương thức trình lặp kiểu "ảnh chụp nhanh" sử dụng một tham chiếu đến trạng thái của mảng tại điểm mà trình lặp được tạo. Mảng này không bao giờ thay đổi trong suốt thời gian tồn tại của trình lặp, do đó không thể can thiệp và trình lặp được đảm bảo không ném ConcurrentModificationException.

Với bộ sưu tập đặc biệt này (thường được sử dụng để truy cập đồng thời), bạn có thể thao tác với danh sách bên dưới trong khi lặp qua nó. Tuy nhiên, trình lặp sẽ không phản ánh các thay đổi.

Giải pháp này có tốt hơn giải pháp kia không? Có lẽ là không, tôi không biết chi phí được giới thiệu bởi phương pháp 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

Đối với kỳ thi, chúng tôi có hai danh sách:

  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

Tôi thích xử lý các bộ sưu tập theo chức năng hơn là thay đổi chúng tại chỗ. Điều đó hoàn toàn tránh được loại vấn đề này, cũng như các vấn đề về răng cưa và các nguồn lỗi phức tạp khác.

Vì vậy, tôi sẽ triển khai nó như sau:

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, cách an toàn hơn sẽ là tạo một bộ sưu tập mới, lặp lại bộ sưu tập đã cho của bạn, thêm từng phần tử trong bộ sưu tập mới và thêm các phần tử bổ sung nếu cần trong bộ sưu tập mới, cuối cùng trả lại bộ sưu tập mới.


0

Đưa ra một danh sách List<Object>mà bạn muốn lặp lại, cách dễ dàng là:

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

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

   list.remove(0);
}

Vì vậy, bạn lặp qua một danh sách, luôn lấy phần tử đầu tiên và sau đó loại bỏ nó. Bằng cách này, bạn có thể thêm các phần tử mới vào danh sách trong khi lặp lại.


0

Quên về các trình vòng lặp, chúng không hoạt động để thêm, chỉ để xóa. Câu trả lời của tôi chỉ áp dụng cho các danh sách, vì vậy đừng trừng phạt tôi vì không giải quyết được vấn đề cho các bộ sưu tập. Bám sát những điều cơ bản:

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

Cảm ơn Stefan. Đã sửa nó.
Victor Ionescu

0

Tôi mệt mỏi với ListIterator nhưng nó không giúp được gì cho trường hợp của tôi, nơi bạn phải sử dụng danh sách trong khi thêm vào nó. Đây là những gì phù hợp với tôi:

Sử dụng 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);
}

Điều này có thể đưa ra một ngoại lệ hoặc chạy vào vòng lặp vô hạn. Tuy nhiên, như bạn đã đề cập

Tôi khá chắc rằng nó sẽ không xảy ra trong trường hợp của tôi

kiểm tra các trường hợp góc trong mã như vậy là trách nhiệm của bạn.


0

Đây là điều tôi thường làm, với các bộ sưu tập như bộ:

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

Điều này tạo ra một số bộ nhớ phụ (con trỏ cho các tập hợp trung gian, nhưng không có phần tử trùng lặp nào xảy ra) và các bước bổ sung (lặp lại các thay đổi), tuy nhiên, điều đó thường không phải là vấn đề lớn và nó có thể tốt hơn là làm việc với một bản sao tập hợp ban đầu.


0

Mặc dù chúng ta không thể thêm các mục vào cùng một danh sách trong khi lặp lại, chúng ta có thể sử dụng flatMap của Java 8, để thêm các phần tử mới vào một luồng. Điều này có thể được thực hiện với một điều kiện. Sau đó, mục được thêm vào có thể được xử lý.

Đây là một ví dụ Java cho thấy cách thêm vào luồng đang diễn ra một đối tượng tùy thuộc vào điều kiện, sau đó được xử lý với điều kiệ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);

Đầu ra của ví dụ đồ chơi là:

[2, 3, 21, 4]


-1

Nói chung , nó không an toàn, mặc dù đối với một số bộ sưu tập thì có thể như vậy. Cách thay thế rõ ràng là sử dụng một số loại vòng lặp for. Nhưng bạn không nói bạn đang sử dụng bộ sưu tập nào, vì vậy điều đó có thể có hoặc có thể không.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.