ลบองค์ประกอบออกจากคอลเลกชันขณะทำซ้ำ


215

AFAIK มีสองวิธี:

  1. ทำซ้ำสำเนาของคอลเล็กชัน
  2. ใช้ตัววนซ้ำของการรวบรวมจริง

ตัวอย่างเช่น

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

มีเหตุผลใดที่จะชอบแนวทางหนึ่งมากกว่าอีกวิธีหนึ่ง (เช่นเลือกแนวทางแรกด้วยเหตุผลง่าย ๆ ในการอ่าน)?


1
แค่อยากรู้อยากเห็นทำไมคุณสร้างสำเนาของคนโง่มากกว่าแค่วนคนโง่ในตัวอย่างแรก?
Haz

@Haz ดังนั้นฉันจะต้องวนรอบเพียงครั้งเดียว
user1329572

15
หมายเหตุ: ต้องการ 'สำหรับ' มากกว่า 'ในขณะที่' พร้อมกับตัววนซ้ำเพื่อ จำกัด ขอบเขตของตัวแปร: สำหรับ (Iterator <Foo> itr = fooList.iterator (); itr.hasNext ();) {}
Puce

ฉันไม่ทราบว่าwhileมีกฎการกำหนดขอบเขตที่แตกต่างจากfor
อเล็กซานเดอร์มิลส์

ในสถานการณ์ที่ซับซ้อนมากขึ้นคุณอาจมีกรณีที่เป็นตัวแปรเช่นและคุณเรียกวิธีการในช่วงห่วงที่ปลายขึ้นเรียกวิธีการอื่นในระดับเดียวกันที่ไม่fooList fooList.remove(obj)ได้เห็นสิ่งนี้เกิดขึ้น ในกรณีใดการคัดลอกรายการจะปลอดภัยที่สุด
Dave Griffiths

คำตอบ:


419

ConcurrentModificationExceptionผมขอให้ตัวอย่างบางส่วนที่มีทางเลือกบางอย่างที่จะหลีกเลี่ยง

สมมติว่าเรามีคอลเล็กชันหนังสือดังต่อไปนี้

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

รวบรวมและลบ

เทคนิคแรกประกอบด้วยการรวบรวมวัตถุทั้งหมดที่เราต้องการลบ (เช่นการใช้ขั้นสูงสำหรับการวนซ้ำ) และหลังจากที่เราทำการวนซ้ำเสร็จสิ้นเราจะลบวัตถุที่พบทั้งหมด

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

นี่เป็นการสมมติว่าการดำเนินการที่คุณต้องการทำคือ "ลบ"

หากคุณต้องการที่จะ "เพิ่ม" วิธีการนี้จะใช้งานได้ แต่ฉันคิดว่าคุณจะวนซ้ำคอลเล็กชันอื่นเพื่อพิจารณาว่าคุณต้องการเพิ่มองค์ประกอบใดลงในคอลเล็กชันที่สองจากนั้นจึงออกaddAllวิธีในตอนท้าย

ใช้ ListIterator

หากคุณกำลังทำงานกับรายการเทคนิคอื่นประกอบด้วยการใช้ListIteratorที่มีการสนับสนุนสำหรับการลบและการเพิ่มรายการในระหว่างการทำซ้ำตัวเอง

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

อีกครั้งฉันใช้วิธี "ลบ" ในตัวอย่างด้านบนซึ่งเป็นคำถามของคุณที่ดูเหมือนจะบอกเป็นนัย แต่คุณอาจใช้addวิธีนี้เพื่อเพิ่มองค์ประกอบใหม่ในระหว่างการทำซ้ำ

ใช้ JDK> = 8

สำหรับผู้ที่ทำงานกับ Java 8 หรือรุ่นที่เหนือกว่ามีเทคนิคอื่น ๆ ที่คุณสามารถใช้เพื่อใช้ประโยชน์จากมัน

คุณสามารถใช้removeIfวิธีการใหม่ในCollectionชั้นฐาน:

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

หรือใช้ API สตรีมใหม่:

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

ในกรณีสุดท้ายนี้ในการกรององค์ประกอบออกจากคอลเลกชันคุณมอบหมายการอ้างอิงดั้งเดิมไปยังคอลเลกชันที่กรอง (เช่นbooks = filtered) หรือใช้คอลเลกชันที่กรองไปremoveAllยังองค์ประกอบที่พบจากคอลเลกชันดั้งเดิม (เช่นbooks.removeAll(filtered))

ใช้รายการย่อยหรือกลุ่มย่อย

มีทางเลือกอื่น ๆ เช่นกัน หากรายการถูกเรียงลำดับและคุณต้องการลบองค์ประกอบที่ต่อเนื่องกันคุณสามารถสร้างรายการย่อยแล้วล้างมัน:

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

เนื่องจากรายการย่อยได้รับการสนับสนุนโดยรายการดั้งเดิมจึงเป็นวิธีที่มีประสิทธิภาพในการลบองค์ประกอบย่อยขององค์ประกอบนี้

สิ่งที่คล้ายกันสามารถทำได้ด้วยชุดเรียงที่ใช้NavigableSet.subSetวิธีการหรือวิธีการใด ๆ ที่มีการเสนอ

การพิจารณา:

วิธีการที่คุณใช้อาจขึ้นอยู่กับสิ่งที่คุณตั้งใจจะทำ

  • การรวบรวมและremoveAlเทคนิคทำงานกับการรวบรวม (การรวบรวมรายการการตั้งค่า ฯลฯ )
  • ListIteratorเทคนิคการทำงานอย่างเห็นได้ชัดเท่านั้นที่มีรายชื่อให้ว่าพวกเขาได้รับListIteratorข้อเสนอการดำเนินการสนับสนุนสำหรับการเพิ่มและลบการดำเนินงาน
  • Iteratorวิธีการจะทำงานร่วมกับชนิดของคอลเลกชันใด ๆ แต่จะสนับสนุนเฉพาะการดำเนินงานลบ
  • ด้วยวิธีListIterator/ Iteratorข้อดีที่ชัดเจนคือไม่ต้องคัดลอกอะไรเลยตั้งแต่เราลบเมื่อเราย้ำ ดังนั้นนี้มีประสิทธิภาพมาก
  • ตัวอย่างสตรีม JDK 8 ไม่ได้ลบอะไรจริงๆ แต่มองหาองค์ประกอบที่ต้องการจากนั้นเราแทนที่การอ้างอิงคอลเลกชันดั้งเดิมด้วยอันใหม่และให้อันเก่าเก็บขยะ ดังนั้นเราจึงย้ำเพียงครั้งเดียวในการรวบรวมและจะมีประสิทธิภาพ
  • ในการรวบรวมและremoveAllเข้าใกล้ข้อเสียคือเราต้องย้ำสองครั้ง อันดับแรกเราวนซ้ำในวง foor-loop มองหาวัตถุที่ตรงกับเกณฑ์การเอาออกของเราและเมื่อเราพบแล้วเราขอให้ลบออกจากคอลเลกชันดั้งเดิม ย้ายมัน.
  • ฉันคิดว่าเป็นมูลค่าการกล่าวขวัญว่าวิธีการลบของIteratorอินเตอร์เฟซที่ถูกทำเครื่องหมายเป็น "ตัวเลือก" ใน Javadocs ซึ่งหมายความว่าอาจมีIteratorการใช้งานที่โยนUnsupportedOperationExceptionถ้าเราเรียกใช้วิธีการลบ ดังนั้นฉันจึงบอกว่าวิธีนี้ปลอดภัยน้อยกว่าวิธีอื่นถ้าเราไม่สามารถรับประกันการสนับสนุนตัววนซ้ำสำหรับการลบองค์ประกอบ

ไชโย! นี่คือคู่มือที่ชัดเจน
Magno C

นี่คือคำตอบที่สมบูรณ์แบบ! ขอบคุณ.
วิลเฮล์ม

7
ในวรรคของคุณเกี่ยวกับ JDK8 Streams removeAll(filtered)ที่คุณพูดถึง ทางลัดสำหรับสิ่งนั้นremoveIf(b -> b.getIsbn().equals(other))
ifloop

อะไรคือความแตกต่างระหว่าง Iterator และ ListIterator
Alexander Mills

ไม่ได้พิจารณาว่าจะเป็นการลบหาก แต่เป็นคำตอบสำหรับคำอธิษฐานของฉัน ขอบคุณ!
Akabelle


13

มีเหตุผลใดที่จะชอบแนวทางหนึ่งมากกว่าอีกวิธีหนึ่ง

วิธีแรกจะใช้งานได้ แต่มีค่าใช้จ่ายที่ชัดเจนในการคัดลอกรายการ

วิธีที่สองจะไม่ทำงานเนื่องจากคอนเทนเนอร์จำนวนมากไม่อนุญาตให้มีการปรับเปลี่ยนในระหว่างการทำซ้ำ ซึ่งรวมถึงArrayList

หากการปรับเปลี่ยนเพียงคือการลบองค์ประกอบปัจจุบันคุณสามารถทำให้การทำงานแนวทางที่สองโดยใช้itr.remove()(นั่นคือใช้iterator 's remove()วิธีการไม่ได้เป็นภาชนะ ' s) นี้จะเป็นวิธีการที่ต้องการของฉันสำหรับ iterators remove()ว่าการสนับสนุน


โอ๊ะโอขออภัย ... มันบอกเป็นนัย ๆ ว่าฉันจะใช้วิธีลบของตัววนซ้ำไม่ใช่ของคอนเทนเนอร์ และการคัดลอกรายการจะมีค่าใช้จ่ายเท่าใด มันไม่มากนักและเนื่องจากมันถูกกำหนดขอบเขตให้กับวิธีการมันควรจะเก็บขยะได้ค่อนข้างเร็ว ดูการแก้ไข ..
user1329572

1
@aix ฉันคิดว่ามันเป็นมูลค่าการกล่าวขวัญวิธีการลบของIteratorอินเตอร์เฟซที่ถูกทำเครื่องหมายเป็นตัวเลือกใน Javadocs ซึ่งหมายความว่าอาจจะมีการใช้งาน Iterator UnsupportedOperationExceptionที่อาจโยน เช่นนี้ฉันจะบอกว่าวิธีนี้ปลอดภัยน้อยกว่าวิธีแรก ขึ้นอยู่กับการใช้งานที่ต้องการใช้วิธีแรกอาจเหมาะสมกว่า
Edwin Dalorzo

@EdwinDalorzo remove()ในคอลเลกชันเดิมของตัวเองนอกจากนี้ยังอาจจะโยนUnsupportedOperationException: docs.oracle.com/javase/7/docs/api/java/util/... อินเทอร์เฟซคอนเทนเนอร์ของจาวานั้นน่าเศร้าที่ถูกกำหนดให้ไม่น่าเชื่อถืออย่างยิ่ง หากคุณไม่ทราบว่าการใช้งานที่แน่นอนที่จะใช้ในรันไทม์จะเป็นการดีกว่าที่จะทำสิ่งต่าง ๆ ในแบบที่ไม่เปลี่ยนรูป - เช่นใช้ Java 8+ Streams API เพื่อกรององค์ประกอบลงและรวบรวมลงในคอนเทนเนอร์ใหม่แล้ว แทนที่เก่าด้วยมันทั้งหมด
Matthew อ่าน

5

วิธีที่สองเท่านั้นที่ใช้งานได้ คุณสามารถแก้ไขคอลเลกชันในระหว่างการทำซ้ำโดยใช้iterator.remove()เพียง ConcurrentModificationExceptionทุกความพยายามที่จะทำให้คนอื่น ๆ


2
ความพยายามครั้งแรกซ้ำในสำเนาหมายความว่าเขาสามารถปรับเปลี่ยนต้นฉบับ
โคลิน D

3

Old Timer Favorite (ยังใช้งานได้):

List<String> list;

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

1

คุณไม่สามารถทำสองเพราะแม้ว่าคุณจะใช้remove()วิธีการในIterator , คุณจะได้รับข้อยกเว้นโยน

โดยส่วนตัวแล้วฉันต้องการที่จะเป็นคนแรกสำหรับทุก ๆCollectionกรณีแม้ว่าจะมีการได้ยินเพิ่มเติมจากการสร้างใหม่Collectionฉันพบว่ามันมีข้อผิดพลาดน้อยกว่าระหว่างการแก้ไขโดยนักพัฒนาอื่น ๆ ในการใช้งานคอลเล็กชันบางตัว Iterator remove()ได้รับการสนับสนุน คุณสามารถอ่านเพิ่มเติมในเอกสารสำหรับIterator

ทางเลือกที่สามคือการสร้างใหม่วนCollectionซ้ำจากเดิมและเพิ่มสมาชิกทั้งหมดของสมาชิกลำดับแรกCollectionเป็นอันดับCollectionที่สองซึ่งไม่พร้อมสำหรับการลบ ขึ้นอยู่กับขนาดของCollectionและจำนวนการลบสิ่งนี้สามารถประหยัดหน่วยความจำได้อย่างมีนัยสำคัญเมื่อเปรียบเทียบกับวิธีแรก


0

ฉันจะเลือกวินาทีเพราะคุณไม่ต้องทำสำเนาหน่วยความจำและ Iterator ทำงานได้เร็วขึ้น ดังนั้นคุณจึงประหยัดหน่วยความจำและเวลา


" Iterator ทำงานได้เร็วขึ้น " มีอะไรให้สนับสนุนข้อเรียกร้องนี้บ้าง? นอกจากนี้รอยเท้าหน่วยความจำในการทำสำเนาของรายการมีความสำคัญมากโดยเฉพาะอย่างยิ่งเนื่องจากจะมีการกำหนดขอบเขตภายในวิธีการและจะมีการรวบรวมขยะในทันที
user1329572

1
ในวิธีแรกข้อเสียคือเราต้องย้ำสองครั้ง เราวนซ้ำในวงวนที่มองหาองค์ประกอบและเมื่อเราพบมันเราจะขอให้ลบมันออกจากรายการเดิมซึ่งจะบอกเป็นนัยถึงการวนซ้ำครั้งที่สองเพื่อค้นหารายการนี้ สิ่งนี้จะสนับสนุนการอ้างสิทธิ์ว่าอย่างน้อยในกรณีนี้วิธีการวนซ้ำควรเร็วขึ้น เราต้องพิจารณาว่าเฉพาะพื้นที่โครงสร้างของคอลเล็กชันเท่านั้นที่ถูกสร้างขึ้นวัตถุภายในคอลเลกชันจะไม่ถูกคัดลอก คอลเลกชันทั้งสองจะเก็บการอ้างอิงไปยังวัตถุเดียวกัน เมื่อ GC เกิดขึ้นเราไม่สามารถบอกได้ !!!
Edwin Dalorzo

-2

ทำไมไม่ทำอย่างนี้ล่ะ?

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

และถ้าเป็นแผนที่ไม่ใช่รายการคุณสามารถใช้ชุดคีย์ ()


4
วิธีนี้มีข้อเสียที่สำคัญมากมาย ครั้งแรกทุกครั้งที่คุณลบองค์ประกอบดัชนีจะถูกจัดระเบียบใหม่ ดังนั้นหากคุณลบองค์ประกอบ 0 ดังนั้นองค์ประกอบที่ 1 จะกลายเป็นองค์ประกอบใหม่ 0 หากคุณกำลังจะไปที่สิ่งนี้อย่างน้อยก็ย้อนกลับเพื่อหลีกเลี่ยงปัญหานี้ ประการที่สองการใช้งาน List ทั้งหมดไม่ให้การเข้าถึงองค์ประกอบโดยตรง (เช่นเดียวกับ ArrayList) ใน LinkedList นี้จะไม่มีประสิทธิภาพอย่างมากเพราะเวลาที่คุณออกทุกคุณได้ไปเยี่ยมชมโหนดทั้งหมดจนกว่าจะถึงget(i) i
Edwin Dalorzo

ไม่เคยคิดว่าเป็นเพราะฉันมักจะใช้มันเพื่อลบรายการเดียวที่ฉันกำลังมองหา ดีแล้วที่รู้.
Drake Clarris

4
ฉันไปงานปาร์ตี้ช้า แต่แน่นอนว่าถ้ารหัสบล็อกหลังจากที่Foo.remove(i);คุณควรทำi--;อย่างไร
Bertie Wheen

เพราะมันบั๊ก
แจ็ค
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.