概述
前言:
今天面试时,面试官问了一个问题:在增强 for 循环中为什么删除元素为什么会报错?如果是修改元素,会发生什么?
我回答的是因为 ArrayList是线程不安全的,所以会报错。额....(⊙﹏⊙) !肯定不对啊。
所以面试完赶紧查询,码住!!!
什么是增强for循环?
增强for循环(也称for each循环)是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。他的内部原理其实是一个Iterator迭代器。并且只有实现Iterable接口的那些类可以拥有增强for循环。
可以看一下这里 Iterator 的源码
package 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中就没有剩余元素了,所以就不执行了。
List<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);
}
结果:
a
b
c
d
再次执行...
创建一个新的iterator,在执行
a
b
c
d
好了,Iterable 接口研究完毕,回归正题。在增强 for 循环中为什么删除元素为什么会报错?
分析:
定义一个ArrayList,在增强for循环中删除元素。
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
for(String x : list){
list.remove(x);
}
强调!!!
如果数组只有2个元素,则可以成功删除一个!
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
for(String x : list){
list.remove(x);
}
System.out.println(list.get(0));
输出:b
为什么会这样呐?
排查上个的报错信息:
Exception 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方法
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 = i + 1;
return (E) elementData[lastRet = i];
}
可以看到,首先引入眼帘的是 checkForComodification() 方法,也就是报错信息第二行的异常。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
这里只有两个变量,也不知道是什么,查看其他方法,例如 add()
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
可以看到,elementData 是ArrayList存放元素的数组,modCount 这个变量并没有明确定义,它只是通过方法传递进来的,根据字面意思,它是一个增量,也就是对 elementData 中的结构添加、删除等操作的标记。
再来看 expectedModCount
private 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 方法
public 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 ,这个是实际操作删除方法
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
}
可以看到,首先将 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 则可以进行修改。如下:
List<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);
}
结果:
b
c
d
面试官又问了,在循环里修改元素,会发生什么?
字符串类型:
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
for(String x : list){
if(x.equals("a")){
x = "c";
}
}
System.out.println(list);
[a, b]
数字类型:
List<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, 2]
对象类型:
定义Book类
package 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;
}
}
修改:
List<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()));
结果
十万个为什么--明湖
海底世界--人民出版社
可以看到,java对象类型的数组成功了!对于基本类型和对象类型是不同的!
先理解值传递和引用传递:
-
值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
-
引用传递是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
而增强for循环中的单个类型变量(以x为例) 是值传递!相当于:
List<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的元素!而对象循环修改的是对象的属性,而不是对象本身。即:
for(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
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
}
elementData: ArrayList集合
index+1:从ArrayList集合的起始位置开始
elementData:要复制的目标数组(也是elementData,自己复制自己)
index:目标数组的开始起始位置
numMoved:要复制的数组的长度
所以,一目了然,使用 arraycopy 将原ArrayList从第二个元素开始复制,再将自己的前三个元素替换为复制的元素,最后通过 elementData[--size] = null 将最后的一个元素 赋空值。
各位同学也要记住 arraycopy 这个方法啊!
至此结束!
最后
以上就是糟糕服饰为你收集整理的面试题:在增强 for 循环中为什么删除元素为什么会报错?如果是修改元素,会发生什么?的全部内容,希望文章能够帮你解决面试题:在增强 for 循环中为什么删除元素为什么会报错?如果是修改元素,会发生什么?所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复