Xóa các phần tử khỏi bộ sưu tập trong khi lặp


215

AFAIK, có hai cách tiếp cận:

  1. Lặp lại một bản sao của bộ sưu tập
  2. Sử dụng trình vòng lặp của bộ sưu tập thực tế

Ví dụ,

List<Foo> fooListCopy = new ArrayList<Foo>(fooList);
for(Foo foo : fooListCopy){
    // modify actual fooList
}

Iterator<Foo> itr = fooList.iterator();
while(itr.hasNext()){
    // modify actual fooList using itr.remove()
}

Có bất kỳ lý do nào để thích cách tiếp cận này hơn cách tiếp cận khác (ví dụ: thích cách tiếp cận đầu tiên vì lý do đơn giản là dễ đọc)?


1
Chỉ tò mò, tại sao bạn tạo một bản sao của kẻ ngốc thay vì chỉ lặp qua kẻ ngốc trong ví dụ đầu tiên?
Haz

@Haz, nên tôi chỉ phải lặp một lần.
user1329572

15
Lưu ý: thích 'for' over 'while' cũng với iterators để giới hạn phạm vi của biến: for (Iterator <Foo> itr = fooList.iterator (); itr.hasNext ();) {}
Puce

Tôi không biết whilecó các quy tắc phạm vi khác vớifor
Alexander Mills

Trong một tình huống phức tạp hơn, bạn có thể gặp trường hợp fooListlà một biến thể hiện và bạn gọi một phương thức trong vòng lặp kết thúc bằng cách gọi một phương thức khác trong cùng một lớp fooList.remove(obj). Đã thấy điều này xảy ra. Trong trường hợp sao chép danh sách là an toàn nhất.
Dave Griffiths

Câu trả lời:


416

Hãy để tôi đưa ra một vài ví dụ với một số lựa chọn thay thế để tránh a ConcurrentModificationException.

Giả sử chúng ta có bộ sưu tập sách sau đây

List<Book> books = new ArrayList<Book>();
books.add(new Book(new ISBN("0-201-63361-2")));
books.add(new Book(new ISBN("0-201-63361-3")));
books.add(new Book(new ISBN("0-201-63361-4")));

Thu thập và loại bỏ

Kỹ thuật đầu tiên bao gồm thu thập tất cả các đối tượng mà chúng ta muốn xóa (ví dụ: sử dụng vòng lặp nâng cao) và sau khi hoàn thành việc lặp lại, chúng ta xóa tất cả các đối tượng tìm thấy.

ISBN isbn = new ISBN("0-201-63361-2");
List<Book> found = new ArrayList<Book>();
for(Book book : books){
    if(book.getIsbn().equals(isbn)){
        found.add(book);
    }
}
books.removeAll(found);

Điều này giả sử rằng thao tác bạn muốn làm là "xóa".

Nếu bạn muốn "thêm" phương pháp này cũng sẽ hoạt động, nhưng tôi cho rằng bạn sẽ lặp qua một bộ sưu tập khác để xác định những yếu tố nào bạn muốn thêm vào bộ sưu tập thứ hai và sau đó đưa ra một addAllphương thức ở cuối.

Sử dụng ListIterator

Nếu bạn đang làm việc với các danh sách, một kỹ thuật khác bao gồm việc sử dụng một ListIteratorhỗ trợ để loại bỏ và thêm các mục trong quá trình lặp.

ListIterator<Book> iter = books.listIterator();
while(iter.hasNext()){
    if(iter.next().getIsbn().equals(isbn)){
        iter.remove();
    }
}

Một lần nữa, tôi đã sử dụng phương thức "loại bỏ" trong ví dụ ở trên, đây là điều mà câu hỏi của bạn dường như ngụ ý, nhưng bạn cũng có thể sử dụng addphương thức của nó để thêm các phần tử mới trong quá trình lặp.

Sử dụng JDK> = 8

Đối với những người làm việc với Java 8 hoặc các phiên bản cao cấp, có một vài kỹ thuật khác mà bạn có thể sử dụng để tận dụng lợi thế của nó.

Bạn có thể sử dụng removeIfphương thức mới trong Collectionlớp cơ sở:

ISBN other = new ISBN("0-201-63361-2");
books.removeIf(b -> b.getIsbn().equals(other));

Hoặc sử dụng API luồng mới:

ISBN other = new ISBN("0-201-63361-2");
List<Book> filtered = books.stream()
                           .filter(b -> b.getIsbn().equals(other))
                           .collect(Collectors.toList());

Trong trường hợp cuối cùng này, để lọc các phần tử ra khỏi bộ sưu tập, bạn chỉ định lại tham chiếu ban đầu cho bộ sưu tập được lọc (nghĩa là books = filtered) hoặc sử dụng bộ sưu tập được lọc cho removeAllcác phần tử tìm thấy từ bộ sưu tập gốc (nghĩa là books.removeAll(filtered)).

Sử dụng danh sách con hoặc tập hợp con

Có những lựa chọn thay thế khác là tốt. Nếu danh sách được sắp xếp và bạn muốn xóa các phần tử liên tiếp, bạn có thể tạo một danh sách phụ và sau đó xóa nó:

books.subList(0,5).clear();

Vì danh sách con được hỗ trợ bởi danh sách ban đầu, đây sẽ là một cách hiệu quả để loại bỏ tập hợp con các phần tử này.

Một cái gì đó tương tự có thể đạt được với các bộ được sắp xếp bằng NavigableSet.subSetphương pháp hoặc bất kỳ phương pháp cắt nào được cung cấp ở đó.

Cân nhắc:

Phương pháp bạn sử dụng có thể phụ thuộc vào những gì bạn định làm

  • Bộ sưu tập và removeAlkỹ thuật hoạt động với mọi Bộ sưu tập (Bộ sưu tập, Danh sách, Bộ, v.v.).
  • Các ListIteratorkỹ thuật rõ ràng là chỉ hoạt động với danh sách, với điều kiện cho họ ListIteratorthực hiện cung cấp hỗ trợ cho tiện ích và loại bỏ các hoạt động.
  • Cách Iteratortiếp cận sẽ hoạt động với bất kỳ loại bộ sưu tập nào, nhưng nó chỉ hỗ trợ loại bỏ các hoạt động.
  • Với ListIterator/ Iteratorphương pháp tiếp cận, lợi thế rõ ràng là không phải sao chép bất cứ điều gì kể từ khi chúng tôi xóa khi chúng tôi lặp lại. Vì vậy, điều này rất hiệu quả.
  • Ví dụ về luồng JDK 8 không thực sự loại bỏ bất cứ thứ gì, nhưng tìm kiếm các phần tử mong muốn và sau đó chúng tôi đã thay thế tham chiếu bộ sưu tập ban đầu bằng cái mới và để cái cũ được thu gom rác. Vì vậy, chúng tôi chỉ lặp lại một lần trong bộ sưu tập và điều đó sẽ hiệu quả.
  • Trong việc thu thập và removeAlltiếp cận nhược điểm là chúng ta phải lặp lại hai lần. Đầu tiên chúng tôi lặp lại trong vòng lặp tìm kiếm một đối tượng phù hợp với tiêu chí loại bỏ của chúng tôi và khi chúng tôi đã tìm thấy nó, chúng tôi yêu cầu xóa nó khỏi bộ sưu tập ban đầu, điều này có nghĩa là một công việc lặp lại thứ hai để tìm kiếm mục này để xóa nó.
  • Tôi nghĩ điều đáng nói là phương thức gỡ bỏ của Iteratorgiao diện được đánh dấu là "tùy chọn" trong Javadocs, điều đó có nghĩa là có thể có các Iteratortriển khai ném UnsupportedOperationExceptionnếu chúng ta gọi phương thức gỡ bỏ. Như vậy, tôi muốn nói rằng phương pháp này kém an toàn hơn các phương pháp khác nếu chúng tôi không thể đảm bảo hỗ trợ lặp để loại bỏ các phần tử.

Bravo! đây là hướng dẫn dứt khoát
Magno C

Đây là một câu trả lời hoàn hảo! Cảm ơn bạn.
Wilhelm

6
Trong đoạn văn của bạn về Luồng JDK8 mà bạn đề cập đến removeAll(filtered). Một lối tắt cho điều đó sẽ làremoveIf(b -> b.getIsbn().equals(other))
ifloop 27/03/18

Sự khác biệt giữa Iterator và ListIterator là gì?
Alexander Mills

Không được xem xét loại bỏ, nhưng đó là câu trả lời cho những lời cầu nguyện của tôi. Cảm ơn!
Akabelle


13

Có bất kỳ lý do để thích một cách tiếp cận hơn các cách tiếp cận khác

Cách tiếp cận đầu tiên sẽ hoạt động, nhưng có chi phí rõ ràng là sao chép danh sách.

Cách tiếp cận thứ hai sẽ không hoạt động vì nhiều container không cho phép sửa đổi trong quá trình lặp. Điều này bao gồmArrayList .

Nếu sửa đổi duy nhất là loại bỏ phần tử hiện tại, bạn có thể làm cho cách tiếp cận thứ hai hoạt động bằng cách sử dụng itr.remove()(nghĩa là sử dụng phương thức của trình vòng lặpremove() , chứ không phải của bộ chứa ). Đây sẽ là phương pháp ưa thích của tôi cho các trình vòng lặp hỗ trợ remove().


Rất tiếc, xin lỗi ... có nghĩa là tôi sẽ sử dụng phương thức loại bỏ của trình vòng lặp chứ không phải công cụ chứa. Và sao chép danh sách tạo ra bao nhiêu chi phí? Nó không thể nhiều và vì nó nằm trong một phương thức, nên nó sẽ được thu gom rác khá nhanh. Xem chỉnh sửa ..
user1329572

1
@aix Tôi nghĩ rằng đáng để đề cập đến phương thức gỡ bỏ của Iteratorgiao diện được đánh dấu là tùy chọn trong Javadocs, điều đó có nghĩa là có thể có các triển khai Iterator có thể ném UnsupportedOperationException. Như vậy, tôi muốn nói rằng phương pháp này kém an toàn hơn phương pháp đầu tiên. Tùy thuộc vào việc triển khai dự định sử dụng, cách tiếp cận đầu tiên có thể phù hợp hơn.
Edwin Dalorzo

@EdwinDalorzo remove()trên chính bộ sưu tập ban đầu cũng có thể ném UnsupportedOperationException: docs.oracle.com/javase/7/docs/api/java/util/ . Đáng buồn thay, các giao diện bộ chứa Java, đáng buồn, được định nghĩa là cực kỳ không đáng tin cậy (đánh bại điểm của giao diện, một cách trung thực). Nếu bạn không biết cách triển khai chính xác sẽ được sử dụng trong thời gian chạy, tốt hơn là thực hiện mọi thứ theo cách không thay đổi - ví dụ: sử dụng API Java 8+ Streams để lọc các phần tử và thu thập chúng vào một thùng chứa mới, sau đó thay thế hoàn toàn cái cũ bằng nó
Matthew đọc

5

Chỉ có cách tiếp cận thứ hai sẽ làm việc. Bạn chỉ có thể sửa đổi bộ sưu tập trong quá trình lặp iterator.remove(). Tất cả những nỗ lực khác sẽ gây ra ConcurrentModificationException.


2
Lần thử đầu tiên lặp lại trên một bản sao, có nghĩa là anh ta có thể sửa đổi bản gốc.
Colin D

2

Old Timer yêu thích (nó vẫn hoạt động):

List<String> list;

for(int i = list.size() - 1; i >= 0; --i) 
{
        if(list.get(i).contains("bad"))
        {
                list.remove(i);
        }
}

1

Bạn không thể thực hiện lần thứ hai, bởi vì ngay cả khi bạn sử dụng remove()phương thức trên Iterator , bạn sẽ bị ném Ngoại lệ .

Cá nhân, tôi thích cái đầu tiên cho tất cả các Collectiontrường hợp, mặc dù đã nghe lỏm thêm việc tạo cái mới Collection, tôi thấy nó ít bị lỗi hơn trong quá trình chỉnh sửa của các nhà phát triển khác. Trên một số triển khai Bộ sưu tập, Iterator remove()được hỗ trợ, mặt khác thì không. Bạn có thể đọc thêm trong các tài liệu cho Iterator .

Các lựa chọn thứ ba, là tạo ra một mới Collection, lặp so với ban đầu, và thêm tất cả các thành viên của những người đầu tiên Collectionđến lần thứ hai Collectionkhông lên để xóa. Tùy thuộc vào kích thước Collectionvà số lần xóa, điều này có thể tiết kiệm đáng kể bộ nhớ, khi so sánh với phương pháp đầu tiên.


0

Tôi sẽ chọn cái thứ hai vì bạn không phải tạo một bản sao của bộ nhớ và Iterator hoạt động nhanh hơn. Vì vậy, bạn tiết kiệm bộ nhớ và thời gian.


" Iterator hoạt động nhanh hơn ". Bất cứ điều gì để hỗ trợ yêu cầu này? Ngoài ra, dấu chân bộ nhớ của việc tạo một bản sao của một danh sách là rất nhỏ, đặc biệt là vì nó sẽ nằm trong một phương thức và sẽ được thu gom rác gần như ngay lập tức.
user1329572

1
Trong cách tiếp cận đầu tiên, nhược điểm là chúng ta phải lặp lại hai lần. Chúng tôi lặp đi lặp lại trong vòng lặp tìm kiếm một phần tử và khi chúng tôi tìm thấy nó, chúng tôi yêu cầu xóa nó khỏi danh sách ban đầu, điều này có nghĩa là một công việc lặp thứ hai để tìm kiếm mục này. Điều này sẽ hỗ trợ cho tuyên bố rằng, ít nhất là trong trường hợp này, phương pháp lặp sẽ nhanh hơn. Chúng ta phải xem xét rằng chỉ có không gian cấu trúc của bộ sưu tập là thứ được tạo ra, các đối tượng bên trong các bộ sưu tập không được sao chép. Cả hai bộ sưu tập sẽ giữ các tham chiếu đến cùng một đối tượng. Khi GC xảy ra chúng ta không thể nói !!!
Edwin Dalorzo

-2

tại sao không phải cái này

for( int i = 0; i < Foo.size(); i++ )
{
   if( Foo.get(i).equals( some test ) )
   {
      Foo.remove(i);
   }
}

Và nếu đó là bản đồ, không phải danh sách, bạn có thể sử dụng keyset ()


4
Cách tiếp cận này có nhiều nhược điểm lớn. Đầu tiên, mỗi khi bạn loại bỏ một phần tử, các chỉ mục được sắp xếp lại. Do đó, nếu bạn loại bỏ phần tử 0, thì phần tử 1 trở thành phần tử mới 0. Nếu bạn định làm điều này, ít nhất hãy làm ngược lại để tránh vấn đề này. Thứ hai, không phải tất cả các triển khai Danh sách đều cung cấp quyền truy cập trực tiếp vào các phần tử (như ArrayList thực hiện). Trong LinkedList, điều này sẽ rất kém hiệu quả bởi vì mỗi khi bạn phát hành, get(i)bạn phải truy cập tất cả các nút cho đến khi bạn đạt được i.
Edwin Dalorzo

Không bao giờ xem xét điều này vì tôi thường chỉ sử dụng nó để loại bỏ một mục mà tôi đang tìm kiếm. Tốt để biết.
Drake Clarris

4
Tôi đến bữa tiệc muộn, nhưng chắc chắn trong mã khối if sau Foo.remove(i);bạn nên làm gì i--;?
Bertie Wheen

vì nó đã bị lỗi
Jack
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.