前言:
今天面试时,面试官问了一个问题:在增强 for 循环中为什么删除元素为什么会报错?如果是修改元素,会发生什么?
我回答的是因为 ArrayList是线程不安全的,所以会报错。额....(⊙﹏⊙) !肯定不对啊。
所以面试完赶紧查询,码住!!!
什么是增强for循环?
增强for循环(也称for each循环)是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。他的内部原理其实是一个Iterator迭代器。并且只有实现Iterable接口的那些类可以拥有增强for循环。
可以看一下这里 Iterator 的源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95package java.util; import java.util.function.Consumer; /** * An iterator over a collection. {@code Iterator} takes the place of * {@link Enumeration} in the Java Collections Framework. Iterators * differ from enumerations in two ways: * * <ul> * <li> Iterators allow the caller to remove elements from the * underlying collection during the iteration with well-defined * semantics. * <li> Method names have been improved. * </ul> * * <p>This interface is a member of the * <a href="{@docRoot}/../technotes/guides/collections/index.html"> * Java Collections Framework</a>. * * @param <E> the type of elements returned by this iterator * * @author Josh Bloch * @see Collection * @see ListIterator * @see Iterable * @since 1.2 */ public interface Iterator<E> { /** * Returns {@code true} if the iteration has more elements. * (In other words, returns {@code true} if {@link #next} would * return an element rather than throwing an exception.) * * @return {@code true} if the iteration has more elements */ boolean hasNext(); /** * Returns the next element in the iteration. * * @return the next element in the iteration * @throws NoSuchElementException if the iteration has no more elements */ E next(); /** * Removes from the underlying collection the last element returned * by this iterator (optional operation). This method can be called * only once per call to {@link #next}. The behavior of an iterator * is unspecified if the underlying collection is modified while the * iteration is in progress in any way other than by calling this * method. * * @implSpec * The default implementation throws an instance of * {@link UnsupportedOperationException} and performs no other action. * * @throws UnsupportedOperationException if the {@code remove} * operation is not supported by this iterator * * @throws IllegalStateException if the {@code next} method has not * yet been called, or the {@code remove} method has already * been called after the last call to the {@code next} * method */ default void remove() { throw new UnsupportedOperationException("remove"); } /** * Performs the given action for each remaining element until all elements * have been processed or the action throws an exception. Actions are * performed in the order of iteration, if that order is specified. * Exceptions thrown by the action are relayed to the caller. * * @implSpec * <p>The default implementation behaves as if: * <pre>{@code * while (hasNext()) * action.accept(next()); * }</pre> * * @param action The action to be performed for each element * @throws NullPointerException if the specified action is null * @since 1.8 */ default void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); while (hasNext()) action.accept(next()); } }
英语好的同学已经看懂了,而我只能百度翻译了。总结一下这些方法:
- next() 每次调用都给出集合的下一项。
- hasNext() 用来告诉是否存在下一项。
- remove() 删除有next()最新返回的项。
- forEachRemaining 对集合中剩余的元素进行操作,直到元素完毕或者抛出异常
这里这个 forEachRemaining 的是JDK1.8新增的方法,很有意思,它的作用是,当前 iterator遍历了集合后,iterator中就没有剩余元素了,所以就不执行了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21List<String> list = new ArrayList<>(); list.add("a"); list.add("b"); list.add("c"); list.add("d"); Iterator<String> iterator = list.iterator(); while(iterator.hasNext()){ String item = iterator.next(); System.out.println(item); } System.out.println("再次执行..."); while(iterator.hasNext()){ String item = iterator.next(); System.out.println(item); } System.out.println("创建一个新的iterator,在执行"); Iterator<String> iterator2 = list.iterator(); while(iterator2.hasNext()){ String item = iterator2.next(); System.out.println(item); }
结果:
1
2
3
4
5
6
7
8
9
10a b c d 再次执行... 创建一个新的iterator,在执行 a b c d
好了,Iterable 接口研究完毕,回归正题。在增强 for 循环中为什么删除元素为什么会报错?
分析:
定义一个ArrayList,在增强for循环中删除元素。
1
2
3
4
5
6
7
8
9List<String> list = new ArrayList<>(); list.add("a"); list.add("b"); list.add("c"); list.add("d"); for(String x : list){ list.remove(x); }
强调!!!
如果数组只有2个元素,则可以成功删除一个!
1
2
3
4
5
6
7List<String> list = new ArrayList<>(); list.add("a"); list.add("b"); for(String x : list){ list.remove(x); } System.out.println(list.get(0));
输出:b
为什么会这样呐?
排查上个的报错信息:
1
2
3
4Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901) at java.util.ArrayList$Itr.next(ArrayList.java:851) at com.fan.Main.main(Main.java:17)
好家伙,报错信息整整齐齐,那我们就来看看,这个 ConcurrentModificationException 异常是怎么触发的。
按照报错顺序先进入报错代码段 Itr.next方法
1
2
3
4
5
6
7
8
9
10
11
12public 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 = i + 1; return (E) elementData[lastRet = i]; }
可以看到,首先引入眼帘的是 checkForComodification() 方法,也就是报错信息第二行的异常。
1
2
3
4final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
这里只有两个变量,也不知道是什么,查看其他方法,例如 add()
1
2
3
4
5public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
可以看到,elementData 是ArrayList存放元素的数组,modCount 这个变量并没有明确定义,它只是通过方法传递进来的,根据字面意思,它是一个增量,也就是对 elementData 中的结构添加、删除等操作的标记。
再来看 expectedModCount
1
2
3
4
5
6
7
8private class Itr implements Iterator<E> { int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; public boolean hasNext() { return cursor != size; }
在创建 itr 对象的时候,就会将 modCount 的值赋给 expectedModCount 的,所以expectedModCount 是记录实例化迭代器Itr时,elementData容量的修改次数,。这里有俩个变量。
这里先记住,再来看 ArrayList.remove 方法,注意,这里的是 ArrayList.remove 方法,不是 Itr.remove 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; }
进入 fastRemove ,这个是实际操作删除方法
1
2
3
4
5
6
7
8private 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 }
可以看到,首先将 modCount 添加一次,表示 执行了一个修改操作。然后for循环执行下一次的next方法,执行 checkForComodification 方法中,判断,modCount != expectedModCount ,因为这里我们只修改了 modCount 的值,没有修改 expectedModCount 的值,而expectedModCount 的值集合的值,在 for each循环中,先要遍历一次括号里面的集合( (String x : list) )给expectedModCount 赋值,所以当前 expectedModCount 为 4,而 modCount 进行了一次删除操作,所以为 5 ,所以 modCount != expectedModCount 为 true,执行
throw new ConcurrentModificationException();
同理:
循环开始前, Itr.next方法中,cursor=0,不不大于size,所以next可以正常返回。但是此时cursor被置为了1,再调用remove方法,此时list中就只有一个元素了,size为1。
再次循环,由于此时cursor为1,我们remove了一个元素后,list的size也变为了1,所以hasNext()判断为false了,就跳出循环了,然后程序结束。也就是说,虽然我们list中有两个元素,但是实际上for循环只进行了一次,所以2个元素不报错!
如果我们使用的是 iterator.hasNext(); 循环,则可以在修改元素后,预先执行一下 iterator.next(); 方法,同步 modCount 和expectedModCount 则可以进行修改。如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14List<String> list = new ArrayList<>(); list.add("a"); list.add("b"); list.add("c"); list.add("d"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { if(iterator.next().equals("a")){ iterator.remove(); } } for(String x : list){ System.out.println(x); }
结果:
1
2
3
4b c d
面试官又问了,在循环里修改元素,会发生什么?
字符串类型:
1
2
3
4
5
6
7
8
9List<String> list = new ArrayList<>(); list.add("a"); list.add("b"); for(String x : list){ if(x.equals("a")){ x = "c"; } } System.out.println(list);
1[a, b]
数字类型:
1
2
3
4
5
6
7
8
9List<Integer> list = new ArrayList<>(); list.add(1); list.add(2); for(Integer x : list){ if(x.equals(1)){ x = 6; } } System.out.println(list);
1[1, 2]
对象类型:
定义Book类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47package com.fan.esjavaapi.bean; import java.util.Date; public class Book { private String name; private String author; private String publisher; private String isbn; public Date getData() { return data; } public void setData(Date data) { this.data = data; } private Date data; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public String getPublisher() { return publisher; } public void setPublisher(String publisher) { this.publisher = publisher; } public String getIsbn() { return isbn; } public void setIsbn(String isbn) { this.isbn = isbn; } }
修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19List<Book> bookList = new ArrayList<>(); Book book = new Book(); book.setName("十万个为什么"); book.setAuthor("埃斯托洛凡"); book.setIsbn("10110"); book.setPublisher("人民出版社"); Book book2 = new Book(); book2.setName("海底世界"); book2.setAuthor("奥夫佗罗夫斯基"); book2.setIsbn("1011s0"); book2.setPublisher("人民出版社"); bookList.add(book); bookList.add(book2); for(Book bk : bookList){ if(bk.getName().equals("十万个为什么")){ bk.setPublisher("明湖"); } } bookList.forEach(s -> System.out.println(s.getName()+"--"+s.getPublisher()));
结果
1
2十万个为什么--明湖 海底世界--人民出版社
可以看到,java对象类型的数组成功了!对于基本类型和对象类型是不同的!
先理解值传递和引用传递:
-
值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
-
引用传递是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
而增强for循环中的单个类型变量(以x为例) 是值传递!相当于:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16List<Integer> list = new ArrayList<>(); list.add(1); list.add(2); for(Integer x : list){ if(x.equals(1)){ x = 6; } } System.out.println(list); // 相当于: for(int i=0;i<list.size();i++){ int x = list.get(i); if(x==4){ x=233; } }
所以改变的只是副本x,而不是list的元素!而对象循环修改的是对象的属性,而不是对象本身。即:
1
2
3
4
5
6
7for(int i =0;i<bookList.size();i++){ Book b = bookList.get(i); System.out.println(Objects.equals(b,bookList.get(i))); //true if(b.getName().equals("十万个为什么")){ b.setPublisher("明湖"); } }
bookList.get(i)给 Book b赋值,其实它们的引用是一个地址。所以修改 b就是修改 bookList.get(i)
至此 疑问解除。
补充:
既然都看了ArrayList的删除源码了,不妨在看看删除时的这个方法:arraycopy
1
2
3
4
5
6
7
8private 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 }
elementData: ArrayList集合
index+1:从ArrayList集合的起始位置开始
elementData:要复制的目标数组(也是elementData,自己复制自己)
index:目标数组的开始起始位置
numMoved:要复制的数组的长度
所以,一目了然,使用 arraycopy 将原ArrayList从第二个元素开始复制,再将自己的前三个元素替换为复制的元素,最后通过 elementData[--size] = null 将最后的一个元素 赋空值。
各位同学也要记住 arraycopy 这个方法啊!
至此结束!
最后
以上就是糟糕服饰最近收集整理的关于面试题:在增强 for 循环中为什么删除元素为什么会报错?如果是修改元素,会发生什么?的全部内容,更多相关面试题:在增强内容请搜索靠谱客的其他文章。
发表评论 取消回复