最近这一周总是迷迷糊糊的,各种踩坑,这不,踩了一个 ArrayList 的 remove 方法的坑,下面我就来介绍一下这个方法的坑。。。
/** 初始化一个集合 */
private static List<Integer> integers = Lists.newArrayList(1, 2, 2, 3, 4, 25);
/**
* 使用 main 方法调用
* 想调用哪个方法就将哪个方法解开注释吧
* @param args
*/
public static void main(String[] args) {
remove();
// foreachRemove();
// foreachRemove2();
// iteratorRemove();
// iteratorRemove2();
// removeEven();
// 遍历集合
integers.stream().forEach(System.out::println);
}
初始化一个集合列表,使用 main 方法调用,接下来就让我们看看有哪些坑吧。
/**
* remove(int index)、remove(Object o) 猜猜下面删除的到底是哪个元素
*/
static void remove() {
// 在这里,25 不会自动装箱,表示的还是索引值
integers.remove(25);
}
看到注释中的两个方法,一个是根据索引删除指定的值,一个是删除指定对象。当集合中存储的是 Integer 类型的时候,传入数字1,删除到底是根据索引还是根据对象删除呢?我们猜一下吧,根据对象删除!
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 25, Size: 6
咦,怎么报错了,根据报错的信息我们很清楚的看到,ArrayList 是根据索引来删除元素的,由于没有索引值为 25 的元素,所以就报越界异常了。其实我们了解重载方法我们就会知道 Java 会根据重载方法精确匹配,看下面的重载方法就明白了。
static void revoew(int i) {
System.out.println("我是普通变量");
}
static void revoew(Integer i) {
System.out.println("我是对象");
}
public static void main(String[] args) {
revoew(1);
}
// 输出
我是普通变量
跨过重载删除的坑,我们来看看迭代器中使用 remove 方法删除的坑吧。
/**
* 使用迭代器删除指定元素,两个元素都出现在元素中的前几个
*/
static void iteratorRemove() {
Iterator<Integer> iterator = integers.iterator();
while (iterator.hasNext()) {
Integer next = iterator.next();
if (Integer.valueOf(1).equals(next)) {
integers.remove(next);
}
if (Integer.valueOf(2).equals(next)) {
integers.remove(next);
}
}
}
来,猜猜上面方法会输出啥?我猜应该输出 3 4 5,运行 main 方法试试。。。
Exception in thread "main" java.util.ConcurrentModificationException
纳尼?怎么报出了个并发修改的异常啊!我明明跑的是单线程啊,吓得我赶紧看断点调一调到底是哪个地方抛出来的异常。
源码如下
public E next() {
// 判断当前链表结构是否被改变,如果改变了则 抛出 ConcurrentModificationException 异常
checkForComodification();
// 游标,迭代器每改变 list 中的结构游标都会改变,前提是通过迭代器来修改,否则游标不会改变
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
// 当数组中元素个数小于游标值时,就会报出下面这个错误,也就是我们要找的
// 使用迭代器的时候不要使用 list 的 remove 方法,而要使用 迭代器的remove 方法的原因
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
现在终于明白为啥会报这个错了,弄懂了这个坑,下一个坑又来了。代码如下:
/**
* 使用迭代器删除指定元素,删除指定元素,两个元素是集合中最后两个
*/
static void iteratorRemove2() {
Iterator<Integer> iterator = integers.iterator();
while (iterator.hasNext()) {
Integer next = iterator.next();
if (Integer.valueOf(4).equals(next)) {
integers.remove(next);
}
if (Integer.valueOf(25).equals(next)) {
integers.remove(next);
}
}
}
大家一看,肯定会说,这个方法和上面那个方法不是类似么,肯定一运行就报并发修改的那个错。还别说,真不是报那个错,而且还有结果输出呢!
1
2
2
3
25
咦,为啥没有报错?4 也被删除了,可是为啥 25 还在呢,可怕,有 bug。冷静分析一波,4 和 25 是集合中最后两个元素,删除 4 的时候,迭代器的游标值为 4,当把 4 删掉之后,游标还是 4 ,而size 也是4 ,那么继续循环应该会报错才对。但此时 iterator.hashNext() 返回的却是 false,直接终止循环,那么也就不会调用 iterator.next() 报错了。因为根本就不会执行了,那现在大家应该会问。25 去哪了?从结果看 25 并没有被删除。那么我们看看 remove 方法,由于 remove 最终还是调用 fastRemove(int index) ,直接看这个方法
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
// 删除一个元素,数组会将删除的元素后面的元素往前移
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // clear to let GC do its work
}
正是因为 4 后面的元素往前移动了一个位置,所以 25 就放到了原来 4 的位置,而 iterator 指针还是指向4的位置,那么调用 iterator.hashNext() 发现没有元素了,所以就退出循环,这也就是为什么不会报错,而且 25 还没有被删除的原因。
下面我们纪录一下迭代器的字节码,后面会用到
static void iteratorRemove();
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=2, args_size=0
0: getstatic #3 // Field integers:Ljava/util/List;
3: invokeinterface #10, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
8: astore_0
9: aload_0
10: invokeinterface #11, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
15: ifeq 73
18: aload_0
19: invokeinterface #12, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
24: checkcast #13 // class java/lang/Integer
27: astore_1
28: iconst_1
29: invokestatic #14 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
32: aload_1
33: invokevirtual #15 // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z
36: ifeq 49
39: getstatic #3 // Field integers:Ljava/util/List;
42: aload_1
43: invokeinterface #16, 2 // InterfaceMethod java/util/List.remove:(Ljava/lang/Object;)Z
48: pop
49: iconst_2
50: invokestatic #14 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
53: aload_1
54: invokevirtual #15 // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z
57: ifeq 70
60: getstatic #3 // Field integers:Ljava/util/List;
63: aload_1
64: invokeinterface #16, 2 // InterfaceMethod java/util/List.remove:(Ljava/lang/Object;)Z
69: pop
70: goto 9
73: return
LineNumberTable:
line 77: 0
line 78: 9
line 79: 18
line 80: 28
line 81: 39
line 83: 49
line 84: 60
line 86: 70
line 87: 73
LocalVariableTable:
Start Length Slot Name Signature
28 42 1 next Ljava/lang/Integer;
9 65 0 iterator Ljava/util/Iterator;
LocalVariableTypeTable:
Start Length Slot Name Signature
9 65 0 iterator Ljava/util/Iterator<Ljava/lang/Integer;>;
StackMapTable: number_of_entries = 4
frame_type = 252 /* append */
offset_delta = 9
locals = [ class java/util/Iterator ]
frame_type = 252 /* append */
offset_delta = 39
locals = [ class java/lang/Integer ]
frame_type = 250 /* chop */
offset_delta = 20
frame_type = 2 /* same */
好啦!迭代器中使用 list 的 remove 方法删除元素的坑踩完了,我们踩踩 foreach 循环里面删除元素的坑吧,代码如下:
/**
* 使用 foreach 循环,删除指定元素,两个元素都出现在元素中的前几个
*/
static void foreachRemove() {
for (Integer integer : integers) {
if (Integer.valueOf(1).equals(integer)) {
integers.remove(integer);
}
if (Integer.valueOf(2).equals(integer)) {
integers.remove(integer);
}
}
}
/**
* 使用 foreach 循环,删除指定元素,两个元素是集合中最后两个
*/
static void foreachRemove2() {
for (Integer integer : integers) {
if (Integer.valueOf(3).equals(integer)) {
integers.remove(integer);
}
if (Integer.valueOf(4).equals(integer)) {
integers.remove(integer);
}
}
}
这两个方法和上面迭代器的坑是一样的,我们看看 forEach循环的字节码吧
static void foreachRemove();
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=2, args_size=0
0: getstatic #3 // Field integers:Ljava/util/List;
3: invokeinterface #10, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
8: astore_0
9: aload_0
10: invokeinterface #11, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
15: ifeq 73
18: aload_0
19: invokeinterface #12, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
24: checkcast #13 // class java/lang/Integer
27: astore_1
28: iconst_1
29: invokestatic #14 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
32: aload_1
33: invokevirtual #15 // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z
36: ifeq 49
39: getstatic #3 // Field integers:Ljava/util/List;
42: aload_1
43: invokeinterface #16, 2 // InterfaceMethod java/util/List.remove:(Ljava/lang/Object;)Z
48: pop
49: iconst_2
50: invokestatic #14 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
53: aload_1
54: invokevirtual #15 // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z
57: ifeq 70
60: getstatic #3 // Field integers:Ljava/util/List;
63: aload_1
64: invokeinterface #16, 2 // InterfaceMethod java/util/List.remove:(Ljava/lang/Object;)Z
69: pop
70: goto 9
73: return
LineNumberTable:
line 49: 0
line 50: 28
line 51: 39
line 53: 49
line 54: 60
line 56: 70
line 57: 73
LocalVariableTable:
Start Length Slot Name Signature
28 42 1 integer Ljava/lang/Integer;
StackMapTable: number_of_entries = 4
frame_type = 252 /* append */
offset_delta = 9
locals = [ class java/util/Iterator ]
frame_type = 252 /* append */
offset_delta = 39
locals = [ class java/lang/Integer ]
frame_type = 250 /* chop */
offset_delta = 20
frame_type = 250 /* chop */
offset_delta = 2 /* same */
有没有发现 forEach 字节码和迭代器的字节码一样,没错,就是一样,连坑都一样,知道上面的坑后那么 forEach 里面的坑也就明白是怎么一回事了。
正确的删除方法, 应该使用迭代器里面的 remove 方法
static void correctRemove() {
Iterator<Integer> iterator = integers.iterator();
while (iterator.hasNext()){
Integer next = iterator.next();
if (Integer.valueOf(1).equals(next)) {
iterator.remove();
}
if (Integer.valueOf(2).equals(next)) {
iterator.remove();
}
}
}
总结:以上就是我使用 ArrayList 的 remove 方法遇到的一些坑,都是自己使用了不正确的方法来删除集合中的元素,希望大家以后都是用正确的删除方式删除集合中的元素。Iterator 里面有 remove 方法,存在即是道理。走上正确的道路才能成功!!!