马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
前言:
今天面试时,面试官问了一个问题:在增强 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);
- }
复制代码 效果:
面试官又问了,在循环里修改元素,会发生什么?
字符串类型:
- 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);
复制代码 数字类型:
- 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);
复制代码 对象类型:
界说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 这个方法啊!
至此结束!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |