Java面试为何阿里强制要求不在foreach里执行删除操作

小二听完就面露喜色,因为两年前,也就是 2021 年,他在《Java 程序员进阶之路》专栏上的第 63 篇看到过这题😆。

PS:star 这种事,只能求,不求没效果,铁子们,《Java 程序员进阶之路》在 GitHub 上已经收获了 523 枚星标,小伙伴们赶紧去点点了,冲 600 star!

https://github.com/itwanger/toBeBetterJavaer

为了镇楼,先搬一段英文来解释一下 fail-fast。

In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system's state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them.

这段话的大致意思就是,fail-fast 是一种通用的系统设计思想,一旦检测到可能会发生错误,就立马抛出异常,程序将不再往下执行。

public void test(Wanger wanger) {
    if (wanger == null) {
        throw new RuntimeException("wanger 不能为空");
    }

    System.out.println(wanger.toString());
}

一旦检测到 wanger 为 null,就立马抛出异常,让调用者来决定这种情况下该怎么处理,下一步 wanger.toString() 就不会执行了——避免更严重的错误出现。

很多时候,我们会把 fail-fast 归类为 Java 集合框架的一种错误检测机制,但其实 fail-fast 并不是 Java 集合框架特有的机制。

之所以我们把 fail-fast 放在集合框架篇里介绍,是因为问题比较容易再现。

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");

for (String str : list) {
	if ("沉默王二".equals(str)) {
		list.remove(str);
	}
}

System.out.println(list);

这段代码看起来没有任何问题,但运行起来就报错了。

根据错误的堆栈信息,我们可以定位到 ArrayList 的第 901 行代码。

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

也就是说,remove 的时候触发执行了 checkForComodification 方法,该方法对 modCount 和 expectedModCount 进行了比较,发现两者不等,就抛出了 ConcurrentModificationException 异常。

为什么会执行 checkForComodification 方法呢?

是因为 for-each 本质上是个语法糖,底层是通过迭代器 Iterator配合 while 循环实现的,来看一下反编译后的字节码。

List<String> list = new ArrayList();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");
Iterator var2 = list.iterator();

while(var2.hasNext()) {
    String str = (String)var2.next();
    if ("沉默王二".equals(str)) {
        list.remove(str);
    }
}

System.out.println(list);

来看一下 ArrayList 的 iterator 方法吧:

public Iterator<E> iterator() {
    return new Itr();
}

内部类 Itr 实现了 Iterator 接口。

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;

    Itr() {}

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        int i = cursor;
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }
}

也就是说 new Itr() 的时候 expectedModCount 被赋值为 modCount,而 modCount 是 List 的一个成员变量,表示集合被修改的次数。由于 list 此前执行了 3 次 add 方法。

  • add 方法调用 ensureCapacityInternal 方法
  • ensureCapacityInternal 方法调用 ensureExplicitCapacity 方法
  • ensureExplicitCapacity 方法中会执行 modCount++

所以 modCount 的值在经过三次 add 后为 3,于是 new Itr() 后 expectedModCount 的值也为 3。

执行第一次循环时,发现“沉默王二”等于 str,于是执行 list.remove(str)

  • remove 方法调用 fastRemove 方法
  • fastRemove 方法中会执行 modCount++
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 的值变成了 4。

执行第二次循环时,会执行 Itr 的 next 方法(String str = (String) var3.next();),next 方法就会调用 checkForComodification 方法,此时 expectedModCount 为 3,modCount 为 4,就只好抛出 ConcurrentModificationException 异常了。

那其实在阿里巴巴的 Java 开发手册里也提到了,不要在 for-each 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式。

那原因其实就是我们上面分析的这些,出于 fail-fast 保护机制。

那该如何正确地删除元素呢?

1)remove 后 break

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");

for (String str : list) {
	if ("沉默王二".equals(str)) {
		list.remove(str);
		break;
	}
}

break 后循环就不再遍历了,意味着 Iterator 的 next 方法不再执行了,也就意味着 checkForComodification 方法不再执行了,所以异常也就不会抛出了。

但是呢,当 List 中有重复元素要删除的时候,break 就不合适了。

2)for 循环

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");
for (int i = 0, n = list.size(); i < n; i++) {
	String str = list.get(i);
	if ("沉默王二".equals(str)) {
		list.remove(str);
	}
}

for 循环虽然可以避开 fail-fast 保护机制,也就说 remove 元素后不再抛出异常;但是呢,这段程序在原则上是有问题的。为什么呢?

第一次循环的时候,i 为 0,list.size() 为 3,当执行完 remove 方法后,i 为 1,list.size() 却变成了 2,因为 list 的大小在 remove 后发生了变化,也就意味着“沉默王三”这个元素被跳过了。能明白吗?

remove 之前 list.get(1) 为“沉默王三”;但 remove 之后 list.get(1) 变成了“一个文章真特么有趣的程序员”,而 list.get(0) 变成了“沉默王三”。

3)使用 Iterator

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");

Iterator<String> itr = list.iterator();

while (itr.hasNext()) {
	String str = itr.next();
	if ("沉默王二".equals(str)) {
		itr.remove();
	}
}

为什么使用 Iterator 的 remove 方法就可以避开 fail-fast 保护机制呢?看一下 remove 的源码就明白了。

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

删除完会执行 expectedModCount = modCount,保证了 expectedModCount 与 modCount 的同步。

简单地总结一下,fail-fast 是一种保护机制,可以通过 for-each 循环删除集合的元素的方式验证这种保护机制。

那也就是说,for-each 本质上是一种语法糖,遍历集合时很方面,但并不适合拿来操作集合中的元素(增删)。

这是《Java 程序员进阶之路》专栏的第 63 篇。Java 程序员进阶之路,风趣幽默、通俗易懂,对 Java 初学者极度友好和舒适😘,内容包括但不限于 Java 语法、Java 集合框架、Java IO、Java 并发编程、Java 虚拟机等核心知识点。

https://github.com/itwanger/toBeBetterJavaer

亮白版和暗黑版的 PDF 也准备好了呢,一起成为更好的 Java 工程师,冲!

到此这篇关于Java面试为何阿里强制要求不在foreach里执行删除操作的文章就介绍到这了,更多相关Java 面试内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 面试题:java中为什么foreach中不允许对元素进行add和remove

    目录 1.foreach遍历ArrayList过程中使用 add 和 remove 2.追根溯源 2.1.modCount是什么? 2.2.expectedModCount 是什么? 2.3.熟悉的checkForComodification方法 2.4.流程回顾 3.避免fail-fast 机制 3.1.使用listIterator或iterator 3.2.使用CopyOnWriteArrayList 3.2.1.CopyOnWriteArrayList的add方法 3.2.2.CopyOn

  • 面试官:java ThreadLocal真的会造成内存泄露吗

    目录 1.ThreadLocal知识体系 2.为什么会被设计为弱引用呢? 3.大量Entry造成的内存溢出问题探讨 总结 1.ThreadLocal知识体系 本文还是不能免俗,在回答这个问题之前需要先和大家介绍一下ThreadLocal的知识,使大家对ThreadLocal有一个相对全面的认识. ThreadLocal本地线程变量,主要用于解决数据访问的竞争,通常用于多租户.全链路压测.链路跟踪中保存线程上下文环境,在一个请求流转中非常方便的获取一些关键信息,例如当前的租户信息.压测标记. Th

  • Java面试最容易被刷的重难点之锁的使用策略

    目录 一. 乐观锁和悲观锁 1. 字面理解 2. 生活实例 3. 基于版本号方式实现乐观锁 二. 读写锁 1. 理解 2. 用法 三. 重量级锁和轻量级锁 1. 原理 2. 理解 3. 区分用户态和内核态 四. 自旋锁 1. 理解 2. 实现方式 3. 优缺点 五. 公平锁和非公平锁 1. 理解 2. 注意事项 六. 可重入锁和不可重入锁 1. 为什么要引入这两把锁 (1)实例一 (2)实例二 2. 实现方案 七. 面试题 第一题 第二题 第三题 第四题 在多线程的学习中,很多时候都要用到锁,但

  • JAVA JVM面试题总结

    目录 JVM 的主要作用是什么? 请你描述一下 Java 的内存区域? 请你描述一下 Java 中的类加载机制? 加载 验证 文件格式验证 元数据验证 字节码验证 符号引用验证 准备 解析 初始化 使用 卸载 在 JVM 中,对象是如何创建的? 内存分配方式有哪些呢? 请你说一下对象的内存布局? 对象头 Header 锁的两个列表 实例数据 Instance Data 对齐 Padding 对象访问定位的方式有哪些? 如何判断对象已经死亡? JVM 的主要作用是什么? JVM 就是 Java V

  • Java面试必问之ThreadLocal终极篇分享

    目录 前言 ThreadLocal是什么 ThreadLoalMap hash冲突 内存泄露 如何避免内存泄露 总结 前言 在面试环节中,考察"ThreadLocal"也是面试官的家常便饭,所以对它理解透彻,是非常有必要的. 有些面试官会开门见山的提问: "知道ThreadLocal吗?" "讲讲你对ThreadLocal的理解" 当然了,也有面试官会慢慢引导到这个话题上,比如提问"在多线程环境下,如何防止自己的变量被其它线程篡改&qu

  • Java面试为何阿里强制要求不在foreach里执行删除操作

    小二听完就面露喜色,因为两年前,也就是 2021 年,他在<Java 程序员进阶之路>专栏上的第 63 篇看到过这题

  • java对XML文件的解析、节点的增加、删除操作总结

    1.java代码: 主要采用dom来进行操作 复制代码 代码如下: package test; import java.io.IOException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.T

  • 吊打Java面试官!整理了一周的Spring面试大全(附答案)

    目录 Q1:什 么 是 spring? Q2:使 用 Spring 框 架 的 好 处 是 什 么 ? Q3:使 用 Spring 缺点是什么? Q4:IoC 是什么? Q5:IOC的优点是什么 Q6:IoC 容器初始化过程? Q7:依赖注⼊的实现方法有哪些? Q8:依赖注入的相关注解? Q9:依赖注入的过程? Q10:Bean 的生命周期? Q11:Bean 的作⽤范围? Q12:如何通过 XML ⽅式创建 Bean? Q13:Spring 有几种配置方式? Q14:如何用基于 XML 配置的

  • java面试常见问题之Hibernate总结

    主要从以下十几个方面对Hibernate做总结,包括Hibernate的检索方式,Hibernate中对象的状态,Hibernate的3种检索策略是什么,分别适用于哪种场合,ORM解决的不匹配问题, Hibernate映射继承关系的3种方式,Session的find()方法以及Query接口的区别等方面问题的总结,具体内容如下: 1  Hibernate的检索方式 Ø  导航对象图检索(根据已经加载的对象,导航到其他对象.) Ø  OID检索(按照对象的OID来检索对象.) Ø  HQL检索(使

  • Java面试问题知识点总结

    本篇文章会对面试中常遇到的Java技术点进行全面深入的总结(阅读本文需要有一定的Java基础:若您初涉Java,可以通过这些问题建立起对Java初步的印象,待有了一定基础后再后过头来看收获会更大),喜欢的朋友可以参考下. 1. Java中的原始数据类型都有哪些,它们的大小及对应的封装类是什么? (1)boolean boolean数据类型非true即false.这个数据类型表示1 bit的信息,但是它的大小并没有精确定义. <Java虚拟机规范>中如是说:"虽然定义了boolean这

  • 详解Java面试官最爱问的volatile关键字

    本文向大家分享的主要内容是Java面试中一个常见的知识点:volatile关键字.本文详细介绍了volatile关键字的方方面面,希望大家在阅读过本文之后,能完美解决volatile关键字的相关问题.  在Java相关的岗位面试中,很多面试官都喜欢考察面试者对Java并发的了解程度,而以volatile关键字作为一个小的切入点,往往可以一问到底,把Java内存模型(JMM),Java并发编程的一些特性都牵扯出来,深入地话还可以考察JVM底层实现以及操作系统的相关知识. 下面我们以一次假想的面试过

  • Java面试之动态规划与组合数

    最近在刷力扣上的题目,刷到了65不同路径,当初上大学的时候,曾在hihocoder上刷到过这道题目,但是现在已经几乎全忘光了,大概的知识点是动态规划,如今就让我们一起来回顾一下. 从题目说起 题目原文是: 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为"Start" ). 机器人每次只能向下或者向右移动一步.机器人试图达到网格的右下角(在下图中标记为"Finish"). 问总共有多少条不同的路径? 例如,上图是一个7 x 3 的网格.有多少可能

  • Java面试知识点总结

    目录 基础篇 一.JAVA: 二.JDK JRE JVM的区别: 三.java的基本数据类型 四.switch默认接受的几种数据类型 五.修饰符的作用 六.Static修饰符 七.final,finally,finalize 三者区别 八.StringBuffer StringBuilder String 区别 九.String的常用方法 十.Java面向对象的特征 十一.抽象类与接口的区别 十二.List,Set,Collection,Collections 十三.Io流 十四.java Ex

  • Java面试必备之AQS阻塞队列和条件队列

    一.AQS入队规则 我们仔细分析一下AQS是如何维护阻塞队列的,在独占方式获取资源的时候,是怎么将竞争锁失败的线程丢到阻塞队列中的呢? 我们看看acquire方法,这里首先会调用子类实现的tryAcquire方法尝试修改state,修改失败的话,说明线程竞争锁失败,于是会走到后面的这个条件: 这个addWaiter方法就是将当前线程封装成一个Node.EXCLUSIVE类型的节点,然后丢到阻塞队列中: 第一次还没有阻塞队列的时候,会到enq方法里面,我们仔细看看enq方法 enq()方法中,我们

  • 吊打Java面试官之Lambda表达式 Stream API

    目录 一.jdk8新特性简介 二.Lambda表达式 简单理解一下Lambda表达式 Lambda表达式的使用 三.函数式接口 1.什么是函数式接口 2.如何理解函数式接口 3.Java内置四大核心函数式接口 四.方法引用与构造器引用 方法引用 构造器引用和数组引用 五.Stream API 1.Stream API的说明 2.为什么要使用Stream API 3.创建Stream的四种方式 4.Stream的中间操作及其测试 5.Stream的终止操作及其测试 六.Optional类的使用 O

随机推荐