【日拱一卒进击大厂系列】ArrayList的面试陷阱别跳进去了

简介: 【日拱一卒进击大厂系列】ArrayList的面试陷阱别跳进去了

背景

昨天小枫接到了一个公司的面试电话,其中一道面试题觉得有点意思,在这里和大家一起分享下。面试题是ArrayList如何删除指定元素。乍听很简单的问题,但是如果没有实际踩过坑很容易掉进面试官的陷阱中,我们一起来分析下吧。

问题分析

疑惑满满

小枫听到这个面试题的时候,心想这是什么水面试官,怎么问这么简单的题目,心想一个for循环加上equal判断再删除不就完事了吗?但是转念一想,不对,这里面肯定有陷阱,不然不会问这么看似简单的问题。小枫突然想起来之前写代码的时候好像遇到过这个问题,也是在ArrayList中删除指定元素,但是直接for循环remove元素的时候还抛出了异常,面试官的陷阱估计在这里。小枫暗自窃喜,找到了面试官埋下的陷阱。

小枫回想起当天的的测试情况,代码进行了脱敏改造。当初是要在ArrayList中删除指定元素,小枫三下五除二,酣畅淋漓的写下了如下的代码,信心满满的点了Run代码的按钮,结果尴尬了,抛异常了。

public class TestListMain {
    public static void main(String[] args) {
        List<String> result = new ArrayList<>();
        result.add("a");
        result.add("b");
        result.add("c");
        result.add("d");
        for (String s : result) {
            if ("b".equals(s)) {
                result.remove("b");
            }
        }
    }
}

一个大大红色的异常马上就出来了,OMG,怎么会这样呢,感觉代码没什么问题啊,赶紧看看抛了什么异常,在哪里抛的异常吧。可以看出来抛了一个ConcurrentModificationException的异常,而且是在Itr这个类中的一个检测方法中抛出来的异常,这是怎么回事呢?我们的原始代码中并没有这个Itr代码,真是百思不得其解。

image.png

拨云见日

既然从源代码分析不出来,我们就看下源代码编译后的class文件中的内容是怎样的吧,毕竟class文件才是JVM真正执行的代码,不看不知道,一看吓一跳,JDK原来是这么玩的。原来如此,我们原始代码中的for-each语句,编译后的实际是以迭代器来代替执行的。

public class TestListMain {
    public TestListMain() {
    }
    public static void main(String[] args) {
        List<String> result = new ArrayList();
        result.add("a");
        result.add("b");
        result.add("c");
        result.add("d");
        //创建迭代器
        Iterator var2 = result.iterator();
        while(var2.hasNext()) {
            String s = (String)var2.next();
            if ("b".equals(s)) {
                result.remove("b");
            }
        }
    }
}

通过ArrayList创建的Itr这个内部类迭代器,于是for-each循环就转化成了迭代器加while循环的方式,原来看上去的for-each循环被挂羊头卖狗肉了。

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

Itr这个内部类迭代器,通过判断hasNext()来判断迭代器是否有内容,而next()方法则获取迭代器中的内容。

 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;
            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];
        }
     ...
 }

大致的过程如下所示:

image.png

真正抛异常的地方是这个检测方法, 当modCount与expectedModCount不相等的时候直接抛出异常了。那我们要看下modCount以及expectedModCount分别是什么。这里的modCount代表ArrayList的修改次数,而expectedModCount代表的是迭代器的修改次数,在创建Itr迭代器的时候,将modCount赋值给了expectedModCount,因此在本例中一开始modCount和expectedModCount都是4(添加了四次String元素)。但是在获取到b元素之后,ArrayList进行了remove操作,因此modCount就累加为5了。因此在进行检查的时候就出现了不一致,最终导致了异常的产生。到此我们找到了抛异常的原因,循环使用迭代器进行循环,但是操作元素却是使用的ArrayList操作,因此迭代器在循环的时候发现元素被修改了所以抛出异常。

image.png

我们再来思考下,为什么要有这个检测呢?这个异常到底起到什么作用呢?我们先来开下ConcurrentModificationException的注释是怎么描述的。简单理解就是不允许一个线程在修改集合,另一个线程在集合基础之上进行迭代。一旦检测到了这种情况就会通过fast-fail机制,抛出异常,防止后面的不可知状况。

/**
 ***
 * For example, it is not generally permissible for one thread to modify a Collection
 * while another thread is iterating over it.  In general, the results of the
 * iteration are undefined under these circumstances.  Some Iterator
 * implementations (including those of all the general purpose collection implementations
 * provided by the JRE) may choose to throw this exception if this behavior is
 * detected.  Iterators that do this are known as <i>fail-fast</i> iterators,
 * as they fail quickly and cleanly, rather that risking arbitrary,
 * non-deterministic behavior at an undetermined time in the future.
 ***
**/
public class ConcurrentModificationException extends RuntimeException {
    ...
}

回顾整个过程

image.png

如何正确的删除

既然抛异常的原因是循环使用了迭代器,而删除使用ArrayList导致检测不通过。那么我们就循环使用迭代器,删除也是用迭代器,这样就可以保证一致了。

public class TestListMain {
    public static void main(String[] args) {
        List<String> result = new ArrayList<>();
        result.add("a");
        result.add("b");
        result.add("c");
        result.add("d");
       Iterator<String> iterator = list.iterator();
    while (iterator .hasNext()) {
      String str = iterator.next();
      if ("b".equals(str)) {
        iterator.remove();
      }
    }
}

总结

本文主要对于ArrayList在for循环中进行元素删除出现的异常进行源码分析,这也是面试的时候经常出现的面试陷阱题,面试官通过这样看似简单的题目考察候选者的JDK源码的掌握程度。

相关文章
|
存储 监控 NoSQL
【日拱一卒进击大厂系列】面试官:为什么单线程的Redis可以实现高并发访问
【日拱一卒进击大厂系列】面试官:为什么单线程的Redis可以实现高并发访问
【日拱一卒进击大厂系列】面试官:为什么单线程的Redis可以实现高并发访问
|
Java 程序员
【日拱一卒进击大厂系列】面试官:服务器CPU使用率达到了90%以上,该怎么排查问题?
【日拱一卒进击大厂系列】面试官:服务器CPU使用率达到了90%以上,该怎么排查问题?
【日拱一卒进击大厂系列】面试官:服务器CPU使用率达到了90%以上,该怎么排查问题?
|
4月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
20天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
21天前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
43 4
|
2月前
|
算法 Java 数据中心
探讨面试常见问题雪花算法、时钟回拨问题,java中优雅的实现方式
【10月更文挑战第2天】在大数据量系统中,分布式ID生成是一个关键问题。为了保证在分布式环境下生成的ID唯一、有序且高效,业界提出了多种解决方案,其中雪花算法(Snowflake Algorithm)是一种广泛应用的分布式ID生成算法。本文将详细介绍雪花算法的原理、实现及其处理时钟回拨问题的方法,并提供Java代码示例。
78 2
|
2月前
|
JSON 安全 前端开发
第二次面试总结 - 宏汉科技 - Java后端开发
本文是作者对宏汉科技Java后端开发岗位的第二次面试总结,面试结果不理想,主要原因是Java基础知识掌握不牢固,文章详细列出了面试中被问到的技术问题及答案,包括字符串相关函数、抽象类与接口的区别、Java创建线程池的方式、回调函数、函数式接口、反射以及Java中的集合等。
31 0
|
4月前
|
存储 安全 Java
这些年背过的面试题——Java基础及面试题篇
本文是技术人面试系列Java基础及面试题篇,面试中关于Java基础及面试题都需要了解哪些内容?一文带你详细了解,欢迎收藏!
|
4月前
|
XML 存储 JSON
【IO面试题 六】、 除了Java自带的序列化之外,你还了解哪些序列化工具?
除了Java自带的序列化,常见的序列化工具还包括JSON(如jackson、gson、fastjson)、Protobuf、Thrift和Avro,各具特点,适用于不同的应用场景和性能需求。
|
4月前
|
Java
【Java基础面试三十七】、说一说Java的异常机制
这篇文章介绍了Java异常机制的三个主要方面:异常处理(使用try、catch、finally语句)、抛出异常(使用throw和throws关键字)、以及异常跟踪栈(异常传播和程序终止时的栈信息输出)。