在工作和学习中,经常碰到删除ArrayList里面的某个元素,看似一个很简单的问题,却很容易出bug。不妨把这个问题当做一道面试题目,我想一定能难道不少的人。今天就给大家说一下在ArrayList循环遍历并删除元素的问题。首先请看下面的例子:
|
import java.util.ArrayList;
publicclassArrayListRemove
{
publicstaticvoidmain(String[]args)
{
ArrayList<String>list=newArrayList<String>();
list.add("a");
list.add("b");
list.add("b");
list.add("c");
list.add("c");
list.add("c");
remove(list);
for(Strings:list)
{
System.out.println("element : "+s);
}
}
publicstaticvoidremove(ArrayList<String>list)
{
// TODO:
}
}
|
如果要想删除list的b字符,有下面两种常见的错误例子:
错误写法实例一:
|
publicstaticvoidremove(ArrayList<String>list)
{
for(inti=0;i<list.size();i++)
{
Strings=list.get(i);
if(s.equals("b"))
{
list.remove(s);
}
}
}
|
错误的原因:这种最普通的循环写法执行后会发现第二个“b”的字符串没有删掉。
错误写法实例二:
|
publicstaticvoidremove(ArrayList<String>list)
{
for(Strings:list)
{
if(s.equals("b"))
{
list.remove(s);
}
}
}
|
错误的原因:这种for-each写法会报出著名的并发修改异常:java.util.ConcurrentModificationException。
先解释一下实例一的错误原因。翻开JDK的ArrayList源码,先看下ArrayList中的remove方法(注意ArrayList中的remove有两个同名方法,只是入参不同,这里看的是入参为Object的remove方法)是怎么实现的:
|
publicbooleanremove(Objecto){
if(o==null){
for(intindex=0;index<size;index++)
if(elementData[index]==null){
fastRemove(index);
returntrue;
}
}else{
for(intindex=0;index<size;index++)
if(o.equals(elementData[index])){
fastRemove(index);
returntrue;
}
}
returnfalse;
}
|
一般情况下程序的执行路径会走到else路径下最终调用faseRemove方法:
|
privatevoidfastRemove(intindex){
modCount++;
intnumMoved=size-index-1;
if(numMoved>0)
System.arraycopy(elementData,index+1,elementData,index,numMoved);
elementData[--size]=null;// Let gc do its work
}
|
可以看到会执行System.arraycopy方法,导致删除元素时涉及到数组元素的移动。针对错误写法一,在遍历第一个字符串b时因为符合删除条件,所以将该元素从数组中删除,并且将后一个元素移动(也就是第二个字符串b)至当前位置,导致下一次循环遍历时后一个字符串b并没有遍历到,所以无法删除。针对这种情况可以倒序删除的方式来避免:
|
publicstaticvoidremove(ArrayList<String>list)
{
for(inti=list.size()-1;i>=0;i--)
{
Strings=list.get(i);
if(s.equals("b"))
{
list.remove(s);
}
}
}
|
因为数组倒序遍历时即使发生元素删除也不影响后序元素遍历。
接着解释一下实例二的错误原因。错误二产生的原因却是foreach写法是对实际的Iterable、hasNext、next方法的简写,问题同样处在上文的fastRemove方法中,可以看到第一行把modCount变量的值加一,但在ArrayList返回的迭代器(该代码在其父类AbstractList中):
|
publicIterator<E>iterator(){
returnnewItr();
}
|
这里返回的是AbstractList类内部的迭代器实现private class Itr implements Iterator,看这个类的next方法:
|
publicEnext(){
checkForComodification();
try{
Enext=get(cursor);
lastRet=cursor++;
returnnext;
}catch(IndexOutOfBoundsExceptione){
checkForComodification();
thrownewNoSuchElementException();
}
}
|
第一行checkForComodification方法:
|
finalvoidcheckForComodification(){
if(modCount!=expectedModCount)
thrownewConcurrentModificationException();
}
|
这里会做迭代器内部修改次数检查,因为上面的remove(Object)方法修改了modCount的值,所以才会报出并发修改异常。要避免这种情况的出现则在使用迭代器迭代时(显示或for-each的隐式)不要使用ArrayList的remove,改为用Iterator的remove即可。
|
publicstaticvoidremove(ArrayList<String>list)
{
Iterator<String>it=list.iterator();
while(it.hasNext())
{
Strings=it.next();
if(s.equals("b"))
{
it.remove();
}
}
}
|
分享到:
相关推荐
对于集合框架,如ArrayList、LinkedList等,增强型`for`循环是首选,它能简洁地遍历集合中的所有元素。如果需要对集合进行更复杂的操作,比如删除元素,则可能需要使用基本的`for`循环配合迭代器。 5. **其他用法*...
例如,在上面的代码中,开发者尝试使用 foreach 循环来遍历 ArrayList,並在遍历过程中修改元素的值。但是,实际输出的结果却不是预期的结果。这是因为,foreach 循环中的迭代变量 temp 只是一个局部变量,是集合...
在for循环中直接删除元素可能导致遍历过程中的索引错乱。例如,当删除一个元素后,未更新的索引将指向下一个元素,但实际上,由于删除,这个位置已被前一个元素占用。例如: ```java List<Long> list = new ...
例如,在上述代码中,我们创建了一个`ArrayList`并获取了一个迭代器,然后在`while`循环中尝试使用`list.remove()`删除元素,这违反了迭代器的规则,因此抛出了异常。正确做法是使用迭代器的`remove()`方法删除元素...
例如,了解HashMap的扩容机制、LinkedList的节点操作、ArrayList的动态数组管理等,能够帮助开发者在设计和实现自己的数据结构时避免常见陷阱。 6. **工具类**:Collections和Arrays类提供了很多实用的静态方法,如...
迭代时删除指定元素 3.5 小结 第4课 Java的内存回收 4.1 Java引用的种类 4.1.1 对象在内存中状态 4.1.2 强引用 4.1.3 软引用 4.1.4 弱引用 4.1.5 虚引用 4.2 Java的内存泄漏 4.3 垃圾回收机制 4.3.1 垃圾...
5. **遍历安全**:为了避免并发修改异常,可以考虑使用`Iterator`的增强型for循环(foreach)来遍历集合,这样在遍历时删除元素会更安全,因为编译器会自动处理迭代器的同步问题。 ```java for (String element : ...
当你需要在遍历List的同时删除元素时,直接使用for-each循环或者索引循环是错误的。这两种方式都会导致`ConcurrentModificationException`。正确的做法是使用迭代器(Iterator)进行删除,或者使用Java 8的Lambda...
ArrayList在随机访问时表现更佳,而LinkedList在插入和删除操作时更优。应根据实际情况选择合适的集合类型。 #### 4. I/O操作优化 在处理I/O操作时,应使用BufferedReader和BufferedWriter来减少对磁盘的读写次数,...
- **ArrayList vs LinkedList**:对于频繁插入删除的场景,应该选择`LinkedList`;而如果主要是元素的读取操作,则`ArrayList`更合适。 - **空指针异常**:在向集合中添加元素之前,务必检查该元素是否为null,以...
在Java开发过程中,开发者经常会...这些错误揭示了Java编程中的一些常见陷阱,理解并避免它们能提升代码质量、性能和稳定性。作为Java开发者,应该时刻关注这些最佳实践,不断学习和改进,以编写更高效、更可靠的代码。
源码可能涉及各种集合操作,如添加、删除、遍历和查找元素,以及集合间的转换。 5. **泛型**:泛型提供了一种在编译时确保类型安全的方式,防止在容器中存储不兼容的数据类型。源码将展示如何定义和使用泛型类、...
熟练掌握它们的操作,如添加、删除、遍历以及查找元素,可以大大提高代码效率。 5. **多线程**:Java提供了内置的多线程支持,通过Thread类和Runnable接口可以创建并管理线程。理解线程同步(如synchronized关键字...
1. **集合框架基础**:理解Collection接口是所有单值容器的根接口,而Iterable接口使得可以使用for-each循环遍历集合。了解ArrayList和LinkedList的不同,比如ArrayList更适合随机访问,而LinkedList适合频繁插入和...
- 使用for循环遍历数组是最常见的方法,例如`for(int i=0; i; i++)`。 - Java 5及以上版本引入增强型for循环(foreach),简化了遍历过程,如`for(int num : numbers)`。 4. **多维数组**: - Java支持二维及...
同时,理解迭代器的工作原理,避免在遍历集合时修改集合,这些都是避免运行时错误的关键。 并发编程是Java的强项,但也是挑战。线程安全的编程需要理解synchronized、volatile关键字的作用,以及死锁、活锁、饥饿等...
总的来说,理解Java内存管理和垃圾收集机制,以及识别和避免内存泄漏的常见陷阱,是提高程序性能和稳定性的重要步骤。通过定期的内存分析和优化实践,开发者可以有效地防止和解决Java中的内存泄漏问题,提升应用的...
5. **迭代器和泛型遍历**:学习如何使用迭代器遍历集合,以及Java 5引入的foreach循环(增强型for循环)对集合的便捷遍历。 6. **集合操作**:课程会涵盖集合的基本操作,如添加、删除、查找元素,以及集合之间的...
例如,用for循环遍历数组: ```java for (int i = 0; i ; i++) { System.out.println(numbers[i]); } ``` 数组的操作还包括查找和排序。线性查找是最基础的方法,但效率较低。二分查找适用于已排序的数组,效率更...