1. 前言
ArrayList 是最常用的 List 类型,它提供了非常便捷的方法对数据进行增删改查,例如:add、remove、get、contains 等等。但是,实际应用中,我们经常需要在 for 循环判断数据,然后删除数据,ArrayList 在这方面的操作上是非常不方便的。下面我们看几个例子:
案例一: 在循环 for (String s : list) 进行删除数据:
public class MyList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
for (String s : list) {
//当数据等于"c"时,删除当前数据
if ("c".equals(s)){
list.remove(s);
}
else {
System.out.println(s);
}
}
}
}
运行程序,抛出了java.util.ConcurrentModificationException
异常。因此,这种方式是不行的。
案例二: 在循环 for (i = 0; i < size; i++) 删除数据:
//同样,删除"c"数据
for (int i = 0; i < list.size(); i++){
String s = list.get(i);
if ("c".equals(s)){
list.remove(i);
}
else{
System.out.println(s);
}
}
执行结果如下:
a
b
e
只输出了abe,丢失了数据 d。这是因为ArrayList内部维护一个elementData数组,删除数据后,此时i的索引值刚好是 d。然后我们又执行了 i++,下次输出变成 e。为了保证数据不丢失,必须在删除之后执行一次 i–
for (int i = 0; i < list.size(); i++){
String s = list.get(i);
if ("c".equals(s)){
list.remove(i);
i--; //这句不能丢掉
}
else{
System.out.println(s);
}
}
案例三: 使用 iterator 删除数据(推荐):
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
String s = iterator.next();
if ("c".equals(s)){
iterator.remove();
}
else{
System.out.println(s);
}
}
输出结果如下:
a
b
d
e
从结果上看,这种方式没有任何问题。避免了开发人员对下标的操作,看上去代码也更直观、可读性更好。
2. iterator 对象是什么类型?
打开 ArrayList 源码,查看上面调用的 iterator() 方法。发现,其实就是 new Itr() 对象
public Iterator<E> iterator() {
return new Itr();
}
- Itr 是 ArrayList 的内部私有类,它实现了 Iterator 接口协议,因此也一样有 hasNext、next、remove 这几个常用方法。
- Itr 通过维护 cursor、lastRet 两个下标索引来操作数据
- Itr 只能调用next()方法逐个获取数据,从第1个遍历到最后一个
private class Itr implements Iterator<E> {
int cursor; // 下一个数据的下标值
int lastRet = -1; // 最后一次调用next()返回数据的下标值
int expectedModCount = modCount;
......
}
3. iterator 是怎么操作数据的?
通过上一步的源码得知,初始状态下,cursor = 0,lastRet = -1
iterator 每次获取下一个元素时,都要先调用 hasNext() 判断当前是否最后一个
public boolean hasNext() {
return cursor != size;
}
iterator 调用 next() 获取cursor当前指向的元素,并将 cursor 移动到下一个元素
public E next() {
checkForComodification();
int i = cursor;
//如果当前已经没数据了,会抛出NoSuchElementException异常,所以要先调用hasNext()判断
if (i >= size)
throw new NoSuchElementException();
//JAVA知识点:非static内部类怎么调用父类的属性?就是通过[父类.this.data]获取
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
//cursor移动到下一笔元素
cursor = i + 1;
//将lastRet指向当前元素,并返回值
return (E) elementData[lastRet = i];
}
iterator 调用 remove() 删除 lastRet 指向的数据,并将 cursor 指向 lastRet 的位置。
因为这个操作已经将cursor指向正确的位置,因此不会丢失数据
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet); //调用ArrayList的remove方法
cursor = lastRet; //cursor 指向 lastRet 的位置
lastRet = -1; //lastRet 重置为-1,因此不能连续调用2次remove
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
4. 总结
- ArrayList 可以通过任意下标值获取数据、插入/添加数据、删除数据,操作灵活性最高,当时循环中执行remove不方便
- Iterator 只能通过 next() 逐个获取下一个数据,灵活性差一点,适用于循环中remove数据
- Iterator 执行完 remove 之后,不能马上第二次执行 remove,必须先调用 next 才能再次 remove