前言:

今天面试时,面试官问了一个问题:在增强 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: * * 
    *
  • Iterators allow the caller to remove elements from the * underlying collection during the iteration with well-defined * semantics. *
  • Method names have been improved. *
* *

This interface is a member of the ** Java Collections Framework. * * @param the type of elements returned by this iterator * * @authorJosh Bloch * @see Collection * @see ListIterator * @see Iterable * @since 1.2 */public interface Iterator {/** * 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 *

The default implementation behaves as if: *

{@code * while (hasNext()) * action.accept(next()); * }

* * @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 action) {Objects.requireNonNull(action);while (hasNext())action.accept(next());}}

英语好的同学已经看懂了,而我只能百度翻译了。总结一下这些方法:

  • next() 每次调用都给出集合的下一项。
  • hasNext() 用来告诉是否存在下一项。
  • remove() 删除有next()最新返回的项。
  • forEachRemaining对集合中剩余的元素进行操作,直到元素完毕或者抛出异常

这里这个forEachRemaining的是JDK1.8新增的方法,很有意思,它的作用是,当前 iterator遍历了集合后,iterator中就没有剩余元素了,所以就不执行了。

List list = new ArrayList();list.add("a");list.add("b");list.add("c");list.add("d");Iterator 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 iterator2 = list.iterator();while(iterator2.hasNext()){String item = iterator2.next();System.out.println(item);}

结果:

abcd再次执行...创建一个新的iterator,在执行abcd

好了,Iterable 接口研究完毕,回归正题。在增强 for 循环中为什么删除元素为什么会报错?

分析:

定义一个ArrayList,在增强for循环中删除元素。

List list = new ArrayList();list.add("a");list.add("b");list.add("c");list.add("d");for(String x : list){list.remove(x);}

强调!!!

如果数组只有2个元素,则可以成功删除一个!

List 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.ConcurrentModificationExceptionat 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 {int cursor; // index of next element to returnint lastRet = -1; // index of last element returned; -1 if no suchint 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 list = new ArrayList();list.add("a");list.add("b");list.add("c");list.add("d");Iterator iterator = list.iterator();while (iterator.hasNext()) {if(iterator.next().equals("a")){iterator.remove();}}for(String x : list){System.out.println(x);}

结果:

bcd

面试官又问了,在循环里修改元素,会发生什么?

字符串类型:

List 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 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 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 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))); //trueif(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这个方法啊!

至此结束!