我是靠谱客的博主 糟糕服饰,最近开发中收集的这篇文章主要介绍面试题:在增强 for 循环中为什么删除元素为什么会报错?如果是修改元素,会发生什么?,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

前言:

今天面试时,面试官问了一个问题:在增强 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 循环中为什么删除元素为什么会报错?如果是修改元素,会发生什么?所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(74)

评论列表共有 0 条评论

立即
投稿
返回
顶部