面试题:在增强 for 循环中为什么删除元素为什么会报错?如果是修改元素, ...

打印 上一主题 下一主题

主题 1014|帖子 1014|积分 3042

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
前言:

今天面试时,面试官问了一个问题:在增强 for 循环中为什么删除元素为什么会报错?如果是修改元素,会发生什么?
我回答的是因为 ArrayList是线程不安全的,所以会报错。额....(⊙﹏⊙) !肯定不对啊。
所以面试完赶紧查询,码住!!!
什么是增强for循环?

增强for循环(也称for each循环)是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和聚集的。他的内部原理其实是一个Iterator迭代器。而且只有实现Iterable接口的那些类可以拥有增强for循环。
可以看一下这里  Iterator 的源码
  1. package java.util;
  2. import java.util.function.Consumer;
  3. /**
  4. * An iterator over a collection.  {@code Iterator} takes the place of
  5. * {@link Enumeration} in the Java Collections Framework.  Iterators
  6. * differ from enumerations in two ways:
  7. *
  8. * <ul>
  9. *      <li> Iterators allow the caller to remove elements from the
  10. *           underlying collection during the iteration with well-defined
  11. *           semantics.
  12. *      <li> Method names have been improved.
  13. * </ul>
  14. *
  15. * <p>This interface is a member of the
  16. * <a href="{@docRoot}/../technotes/guides/collections/index.html">
  17. * Java Collections Framework</a>.
  18. *
  19. * @param <E> the type of elements returned by this iterator
  20. *
  21. * @author  Josh Bloch
  22. * @see Collection
  23. * @see ListIterator
  24. * @see Iterable
  25. * @since 1.2
  26. */
  27. public interface Iterator<E> {
  28.     /**
  29.      * Returns {@code true} if the iteration has more elements.
  30.      * (In other words, returns {@code true} if {@link #next} would
  31.      * return an element rather than throwing an exception.)
  32.      *
  33.      * @return {@code true} if the iteration has more elements
  34.      */
  35.     boolean hasNext();
  36.     /**
  37.      * Returns the next element in the iteration.
  38.      *
  39.      * @return the next element in the iteration
  40.      * @throws NoSuchElementException if the iteration has no more elements
  41.      */
  42.     E next();
  43.     /**
  44.      * Removes from the underlying collection the last element returned
  45.      * by this iterator (optional operation).  This method can be called
  46.      * only once per call to {@link #next}.  The behavior of an iterator
  47.      * is unspecified if the underlying collection is modified while the
  48.      * iteration is in progress in any way other than by calling this
  49.      * method.
  50.      *
  51.      * @implSpec
  52.      * The default implementation throws an instance of
  53.      * {@link UnsupportedOperationException} and performs no other action.
  54.      *
  55.      * @throws UnsupportedOperationException if the {@code remove}
  56.      *         operation is not supported by this iterator
  57.      *
  58.      * @throws IllegalStateException if the {@code next} method has not
  59.      *         yet been called, or the {@code remove} method has already
  60.      *         been called after the last call to the {@code next}
  61.      *         method
  62.      */
  63.     default void remove() {
  64.         throw new UnsupportedOperationException("remove");
  65.     }
  66.     /**
  67.      * Performs the given action for each remaining element until all elements
  68.      * have been processed or the action throws an exception.  Actions are
  69.      * performed in the order of iteration, if that order is specified.
  70.      * Exceptions thrown by the action are relayed to the caller.
  71.      *
  72.      * @implSpec
  73.      * <p>The default implementation behaves as if:
  74.      * <pre>{@code
  75.      *     while (hasNext())
  76.      *         action.accept(next());
  77.      * }</pre>
  78.      *
  79.      * @param action The action to be performed for each element
  80.      * @throws NullPointerException if the specified action is null
  81.      * @since 1.8
  82.      */
  83.     default void forEachRemaining(Consumer<? super E> action) {
  84.         Objects.requireNonNull(action);
  85.         while (hasNext())
  86.             action.accept(next());
  87.     }
  88. }
复制代码
英语好的同学已经看懂了,而我只能百度翻译了。总结一下这些方法:


  • next() 每次调用都给出聚集的下一项。
  • hasNext() 用来告诉是否存在下一项。
  • remove() 删除有next()最新返回的项。
  • forEachRemaining 对聚集中剩余的元素进行操作,直到元素完毕或者抛出非常
这里这个 forEachRemaining 的是JDK1.8新增的方法,很有意思,它的作用是,当前 iterator遍历了聚集后,iterator中就没有剩余元素了,所以就不执行了。
  1.         List<String> list = new ArrayList<>();
  2.         list.add("a");
  3.         list.add("b");
  4.         list.add("c");
  5.         list.add("d");
  6.         Iterator<String> iterator = list.iterator();
  7.         while(iterator.hasNext()){
  8.             String item = iterator.next();
  9.             System.out.println(item);
  10.         }
  11.         System.out.println("再次执行...");
  12.         while(iterator.hasNext()){
  13.             String item = iterator.next();
  14.             System.out.println(item);
  15.         }
  16.         System.out.println("创建一个新的iterator,在执行");
  17.         Iterator<String> iterator2 = list.iterator();
  18.         while(iterator2.hasNext()){
  19.             String item = iterator2.next();
  20.             System.out.println(item);
  21.         }
复制代码
 效果:
  1. a
  2. b
  3. c
  4. d
  5. 再次执行...
  6. 创建一个新的iterator,在执行
  7. a
  8. b
  9. c
  10. d
复制代码
 好了,Iterable 接口研究完毕,回归正题。在增强 for 循环中为什么删除元素为什么会报错?
分析:

界说一个ArrayList,在增强for循环中删除元素。
  1.         List<String> list = new ArrayList<>();
  2.         list.add("a");
  3.         list.add("b");
  4.         list.add("c");
  5.         list.add("d");
  6.         for(String x : list){
  7.             list.remove(x);
  8.         }
复制代码
强调!!!

如果数组只有2个元素,则可以成功删除一个!
  1.         List<String> list = new ArrayList<>();
  2.         list.add("a");
  3.         list.add("b");
  4.         for(String x : list){
  5.             list.remove(x);
  6.         }
  7.         System.out.println(list.get(0));
复制代码
 输出:b
为什么会这样呐?
排查上个的报错信息:
  1. Exception in thread "main" java.util.ConcurrentModificationException
  2.         at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
  3.         at java.util.ArrayList$Itr.next(ArrayList.java:851)
  4.         at com.fan.Main.main(Main.java:17)
复制代码
好家伙,报错信息整整齐齐,那我们就来看看,这个 ConcurrentModificationException 非常是怎么触发的。
按照报错顺序先进入报错代码段 Itr.next方法
  1. public E next() {
  2.             checkForComodification();
  3.             int i = cursor;
  4.             if (i >= size)
  5.                 throw new NoSuchElementException();
  6.             Object[] elementData = ArrayList.this.elementData;
  7.             if (i >= elementData.length)
  8.                 throw new ConcurrentModificationException();
  9.             cursor = i + 1;
  10.             return (E) elementData[lastRet = i];
  11.         }
复制代码
可以看到,首先引入眼帘的是  checkForComodification() 方法,也就是报错信息第二行的非常。
  1.         final void checkForComodification() {
  2.             if (modCount != expectedModCount)
  3.                 throw new ConcurrentModificationException();
  4.         }
复制代码
这里只有两个变量,也不知道是什么,查看其他方法,比方 add()
  1.     public boolean add(E e) {
  2.         ensureCapacityInternal(size + 1);  // Increments modCount!!
  3.         elementData[size++] = e;
  4.         return true;
  5.     }
复制代码
可以看到,elementData 是ArrayList存放元素的数组,modCount 这个变量并没有明确界说,它只是通过方法传递进来的,根据字面意思,它是一个增量,也就是对 elementData 中的结构添加、删除等操作的标记。
再来看 expectedModCount  
  1.     private class Itr implements Iterator<E> {
  2.         int cursor;       // index of next element to return
  3.         int lastRet = -1; // index of last element returned; -1 if no such
  4.         int expectedModCount = modCount;
  5.         public boolean hasNext() {
  6.             return cursor != size;
  7.         }
复制代码
在创建 itr 对象的时候,就会将 modCount 的值赋给 expectedModCount  的,所以expectedModCount  是记录实例化迭代器Itr时,elementData容量的修改次数,。这里有俩个变量。
这里先记住,再来看 ArrayList.remove 方法,注意,这里的是 ArrayList.remove 方法,不是 Itr.remove 方法
  1.     public boolean remove(Object o) {
  2.         if (o == null) {
  3.             for (int index = 0; index < size; index++)
  4.                 if (elementData[index] == null) {
  5.                     fastRemove(index);
  6.                     return true;
  7.                 }
  8.         } else {
  9.             for (int index = 0; index < size; index++)
  10.                 if (o.equals(elementData[index])) {
  11.                     fastRemove(index);
  12.                     return true;
  13.                 }
  14.         }
  15.         return false;
  16.     }
复制代码
进入 fastRemove ,这个是现实操作删除方法
  1.     private void fastRemove(int index) {
  2.         modCount++;
  3.         int numMoved = size - index - 1;
  4.         if (numMoved > 0)
  5.             System.arraycopy(elementData, index+1, elementData, index,
  6.                              numMoved);
  7.         elementData[--size] = null; // clear to let GC do its work
  8.     }
复制代码
可以看到,首先将 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.         List<String> list = new ArrayList<>();
  2.         list.add("a");
  3.         list.add("b");
  4.         list.add("c");
  5.         list.add("d");
  6.         Iterator<String> iterator = list.iterator();
  7.         while (iterator.hasNext()) {
  8.             if(iterator.next().equals("a")){
  9.                 iterator.remove();
  10.             }
  11.         }
  12.         for(String x : list){
  13.             System.out.println(x);
  14.         }
复制代码
效果:
  1. b
  2. c
  3. d
复制代码
面试官又问了,在循环里修改元素,会发生什么?

字符串类型:
  1.         List<String> list = new ArrayList<>();
  2.         list.add("a");
  3.         list.add("b");
  4.         for(String x : list){
  5.             if(x.equals("a")){
  6.                 x = "c";
  7.             }
  8.         }
  9.         System.out.println(list);
复制代码
  1. [a, b]
复制代码
数字类型:
  1.         List<Integer> list = new ArrayList<>();
  2.         list.add(1);
  3.         list.add(2);
  4.         for(Integer x : list){
  5.             if(x.equals(1)){
  6.                 x = 6;
  7.             }
  8.         }
  9.         System.out.println(list);
复制代码
  1. [1, 2]
复制代码
对象类型:
界说Book类
  1. package com.fan.esjavaapi.bean;
  2. import java.util.Date;
  3. public class Book {
  4.     private String name;
  5.     private String author;
  6.     private String publisher;
  7.     private String isbn;
  8.     public Date getData() {
  9.         return data;
  10.     }
  11.     public void setData(Date data) {
  12.         this.data = data;
  13.     }
  14.     private Date data;
  15.     public String getName() {
  16.         return name;
  17.     }
  18.     public void setName(String name) {
  19.         this.name = name;
  20.     }
  21.     public String getAuthor() {
  22.         return author;
  23.     }
  24.     public void setAuthor(String author) {
  25.         this.author = author;
  26.     }
  27.     public String getPublisher() {
  28.         return publisher;
  29.     }
  30.     public void setPublisher(String publisher) {
  31.         this.publisher = publisher;
  32.     }
  33.     public String getIsbn() {
  34.         return isbn;
  35.     }
  36.     public void setIsbn(String isbn) {
  37.         this.isbn = isbn;
  38.     }
  39. }
复制代码
 修改:
  1.         List<Book> bookList = new ArrayList<>();
  2.         Book book = new Book();
  3.         book.setName("十万个为什么");
  4.         book.setAuthor("埃斯托洛凡");
  5.         book.setIsbn("10110");
  6.         book.setPublisher("人民出版社");
  7.         Book book2 = new Book();
  8.         book2.setName("海底世界");
  9.         book2.setAuthor("奥夫佗罗夫斯基");
  10.         book2.setIsbn("1011s0");
  11.         book2.setPublisher("人民出版社");
  12.         bookList.add(book);
  13.         bookList.add(book2);
  14.         for(Book bk : bookList){
  15.             if(bk.getName().equals("十万个为什么")){
  16.                 bk.setPublisher("明湖");
  17.             }
  18.         }
  19.         bookList.forEach(s -> System.out.println(s.getName()+"--"+s.getPublisher()));
复制代码
效果
  1. 十万个为什么--明湖
  2. 海底世界--人民出版社
复制代码
可以看到,java对象类型的数构成功了!对于基本类型和对象类型是差别的!
先理解值传递和引用传递:


  • 值传递是指在调用函数时将现实参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到现实参数
  • 引用传递是指在调用函数时将现实参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到现实参数
而增强for循环中的单个类型变量(以x为例) 是值传递!相当于:
  1.         List<Integer> list = new ArrayList<>();
  2.         list.add(1);
  3.         list.add(2);
  4.         for(Integer x : list){
  5.             if(x.equals(1)){
  6.                 x = 6;
  7.             }
  8.         }
  9.         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.         for(int i =0;i<bookList.size();i++){
  2.             Book b = bookList.get(i);
  3.             System.out.println(Objects.equals(b,bookList.get(i))); //true
  4.             if(b.getName().equals("十万个为什么")){
  5.                 b.setPublisher("明湖");   
  6.             }
  7.         }
复制代码
bookList.get(i)给 Book b赋值,其实它们的引用是一个地址。所以修改 b就是修改 bookList.get(i)
至此 疑问解除。
补充:

既然都看了ArrayList的删除源码了,不妨在看看删除时的这个方法:arraycopy
  1.     private void fastRemove(int index) {
  2.         modCount++;
  3.         int numMoved = size - index - 1;
  4.         if (numMoved > 0)
  5.             System.arraycopy(elementData, index+1, elementData, index,
  6.                              numMoved);
  7.         elementData[--size] = null; // clear to let GC do its work
  8.     }
复制代码
elementData: ArrayList聚集
index+1:从ArrayList聚集的起始位置开始
elementData:要复制的目的数组(也是elementData,自己复制自己)
index:目的数组的开始起始位置
numMoved:要复制的数组的长度
所以,一目了然,使用 arraycopy 将原ArrayList从第二个元素开始复制,再将自己的前三个元素更换为复制的元素,末了通过  elementData[--size] = null 将末了的一个元素 赋空值。
各位同学也要记住 arraycopy 这个方法啊!
至此结束!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

兜兜零元

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表