主要方法
ArrayList是一个泛型容器,先介绍下它的主要方法:public boolean add(E e) //添加元素到末尾
public boolean isEmpty() //判断是否为空
public int size() //获取长度
public E get(int index) //访问指定位置的元素
public int indexOf(Object o) //查找元素, 如果找到,返回索引位置,否则返回-1
public int lastIndexOf(Object o) //从后往前找
public boolean contains(Object o) //是否包含指定元素,依据是equals方法的返回值
public E remove(int index) //删除指定位置的元素, 返回值为被删对象
//删除指定对象,只删除第一个相同的对象,返回值表示是否删除了元素
//如果o为null,则删除值为null的元素
public boolean remove(Object o)
public void clear() //删除所有元素
//在指定位置插入元素,index为0表示插入最前面,index为ArrayList的长度表示插到最后面
public void add(int index, E element)
public E set(int index, E element) //修改指定位置的元素内容
// Collection.java
default boolean removeIf(Predicate super E> filter) //删除符合filter条件的元素
边遍历边删除需求:在包含0,1,2,3,4,5的集合中,删除大于2的数字
错误例子1package cn.dhbin.arraylist;
import java.util.ArrayList;
import java.util.List;
/**
* @author donghaibin
* @date 2019/11/20
*/
public class Demo {
public static void main(String[] args) {
List list = new ArrayList<>();
list.add(0);
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
for (int i = 0; i < list.size(); i++) {
System.out.println("remove之前集合大小: " + list.size());
Integer item = list.get(i);
if (item > 2) {
list.remove(item);
System.out.println("remove之后集合大小:" + list.size());
}
}
System.out.println(list);
}
}
一个for循环遍历,如果item > 2就执行remove,看上去似乎没有问题的样子,也不会出现报错,但是不符合题目的要求
原因是remove方法更新了size的值,即集合的大小,集合的元素也向前移动了一位,变成[0,1,2,4,5],再执行下一个循环时,i++ = 4,删除的是5,4 < list.size跳出了循环,导致最后输出的结果是[0, 1, 2, 4]
错误例子2package cn.dhbin.arraylist;
import java.util.ArrayList;
import java.util.List;
/**
* @author donghaibin
* @date 2019/11/20
*/
public class Demo {
public static void main(String[] args) {
List list = new ArrayList<>();
list.add(0);
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
for (Integer integer : list) {
if (integer > 2) {
list.remove(integer);
}
}
}
}
这个例子执行直接报ConcurrentModificationException异常了。哈?一个单线程的程序怎么会发生了并发异常,为什么呢?
foreach底层实现其实就是迭代器,反编译上面的代码如下://
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package cn.dhbin.arraylist;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Demo {
public Demo() {
}
public static void main(String[] args) {
List list = new ArrayList();
list.add(0);
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
Iterator var2 = list.iterator();
while(var2.hasNext()) {
Integer integer = (Integer)var2.next();
if (integer > 2) {
list.remove(integer);
}
}
}
}
因为迭代器内部会维护一些索引位置相关的数据,要求在迭代过程中,容器不能发生结构性变化,否则这些索引位置就失效了。所谓结构性变化就是添加、插入和删除元素,只是修改元素内容不算结构性变化。如何避免这个异常呢?可以用迭代器的remove方法,如下所示:package cn.dhbin.arraylist;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* @author donghaibin
* @date 2019/11/20
*/
public class Demo {
public static void main(String[] args) {
List list = new ArrayList<>();
list.add(0);
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Integer next = iterator.next();
if (next > 2) {
iterator.remove();
}
}
}
}
在JDK8提供了更方便的方法,removeIf内部实现和上面的代码类似list.removeIf(next -> next > 2);
迭代器实现的原理
ArrayList中iterator方法的实现,代码为:public Iterator iterator() {
return new Itr();
}
Itr是ArrayList的内部类,实现了Iterator接口,声明为:private class Itr implements Iterator
它有三个成员变量,为int cursor; // 下一个要返回的元素位置
int lastRet = -1; // 最后一个返回的索引位置,如果没有,为-1
int expectedModCount = modCount;变量说明cursor下一个要返回的元素位置
lastRet最后一个返回的索引位置
modCountArrayList的变量,记录修改的次数
expectedModCount期望的修改次数,初始化为外部类当前的修改次数modCount
回顾一下,成员内部类可以直接访问外部类的实例变量。每次发生结构性变化的时候modCount都会增加,而每次迭代器操作的时候都会检查expectedModCount是否与modCount相同,这样就能检测出结构性变化。
看下iterator中的每个方法,hasNext方法比较简单cursor和size比较,不等于size说明还有下一位,返回truepublic boolean hasNext() {
return cursor != size;
}
next方法:public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
// 更新cursor,下一个要返回元素位置 + 1
cursor = i + 1;
// 更新lastRet
return (E) elementData[lastRet = i];
}
首先调用了checkForComodification,代码为:final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
检查modCount是否等于expectedModCount,如果不等于说明发生了结构性的变化,抛出异常。
remove方法:public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
// 检查是否发生结构性变化
checkForComodification();
try {
// 删除当前元素
ArrayList.this.remove(lastRet);
// 更新cursor=lastRet,因为容器向前移动了一位
cursor = lastRet;
// 复位
lastRet = -1;
// 把expectedModCount = modCount,同步修改次数。
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
它调用了ArrayList的remove方法,但同时更新了cursor、lastRet和expectedModCount的值,所以它可以正确删除。不过,需要注意的是,调用remove方法前必须先调用next。
总结
ArrayList的remove方法会修改容器的大小,直接for循环会出现与要求不符的结果。更推荐使用迭代器返回容器的元素。foreach语法更为简洁一些,更重要的是,迭代器语法更为通用,它适用于各种容器类。此外,迭代器表示的是一种关注点分离的思想,将数据的实际组织方式与数据的迭代遍历相分离,是一种常见的设计模式。需要访问容器元素的代码只需要一个Iterator接口的引用,不需要关注数据的实际组织方式,可以使用一致和统一的方式进行访问。