不花时间的导读:这是《好好面试系列》第27篇原创文,该系列主要分享小饭饭面试别人、和被别人面试的经历,该篇文章主要分享ArrayList高频面试题,有兴趣的看看,已经知道的可以无视。

前几天H同学和我聊了下去谷歌的面试经验,令我诧异的是,没想到谷歌也问ArrayList???

仔细一想也正常,毕竟集合是Java程序员逃不掉的金光咒。

看文章前可以先看看以下几个问题,如果觉得莫得问题,可以直接跳过该篇文章了,不用浪费大家时间。

  • ArrayList使用无参构造函数的时候什么时候进行扩容?

  • 说说看ArrayList是扩容的时候是怎么复制数组的?

  • ArrayList遍历删除的时候会触发什么机制?为什么用迭代器遍历删除不会?

好了,接下来继续聊聊高频面试题 ArrayList。

ArrayList的扩容机制

// 存储数组元素的缓冲区
transient Object[] elementData;
// 默认空数组元素
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 默认初始化容量
private static final int DEFAULT_CAPACITY = 10;
// 数组的大小
private int size;
// 记录被修改的次数
protected transient int modCount = 0;
// 数组的最大值
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8

底层ArrayList使用数组实现,不设置的话,默认初始容量为10

// 数组扩容方法
// minCapacity = size + 1
private int newCapacity(int minCapacity) {// 当前数组长度int oldCapacity = elementData.length;// 新的数组容量 = 旧数组长度 + 旧数组长度 / 2 // oldCapacity = 10   oldCapacity >> 1   --- 5// 例如10的二进制为 :  0000 1010 >> 1  ----->  0000 0101 = 5int newCapacity = oldCapacity + (oldCapacity >> 1);if (newCapacity - minCapacity <= 0) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)// 如果一开始没有定义初始容量这时newCapacity=0,返回默认容量10// 可以得出当无参new 一个ArrayList()时候,这个ArrayList()为空集合,size为0return Math.max(DEFAULT_CAPACITY, minCapacity);if (minCapacity < 0) // overflowthrow new OutOfMemoryError();return minCapacity;}return (newCapacity - MAX_ARRAY_SIZE <= 0)? newCapacity    // 这里返回的长度为原数组的1.5倍: hugeCapacity(minCapacity);
}

当增加元素的时候发现底层数组的需要的容量(size+1)大于数组的容量的时候,就会触发扩容,在首次调用add()方法之后,返回一个容量为10的数组,后面每次扩容后新数组的长度为原数组长度的 「1.5」 倍,并调用底层原生的System.arraycopy将旧数组的数据copy到新的数组中,完成整个扩容。

所以日常开发中,在知道初始值的时候先设置初始值,因为扩容是比较耗性能的。

「不用脑子的总结:首次扩容为10 ,后面每次扩容为原数组的1.5倍,调用底层原生的System.arraycopy将旧数组的数据copy到新的数组中,完成整个扩容。」

ArrayList添加元素与扩容

ArrayList.add(E e)源码:

public boolean add(E e) {ensureCapacityInternal(size + 1);  // Increments modCount!!elementData[size++] = e;return true;
}

add()elementData[size++] = e很好理解,就是将元素插入第size个位置,然后将size++,我们重点来看看ensureCapacityInternal(size + 1)方法;

private void ensureCapacityInternal(int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);}ensureExplicitCapacity(minCapacity);
}

ensureCapacityInternal()方法中判断缓存变量elementData是否为空,也就是判断是否是第一次添加元素,如果是第一次添加元素,则设置初始化大小为默认容量10,否则为传入的参数。这个方法的目的就是「获取初始化数组容量」。获取到初始化容量后调用ensureExplicitCapacity(minCapacity)方法;

private void ensureExplicitCapacity(int minCapacity) {modCount++;// overflow-conscious codeif (minCapacity - elementData.length > 0)grow(minCapacity);
}

ensureExplicitCapacity(minCapacity)方法用来判断是否需要扩容,假如第一次添加元素,minCapacity10elementData容量为0,那么就需要去扩容。调用grow(minCapacity)方法。

// 数组的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length;// 扩容大小为原来数组长度的1.5倍int newCapacity = oldCapacity + (oldCapacity >> 1);// 扩容容量比需要扩容的长度小,则使用需要扩容的容量if (newCapacity - minCapacity < 0)newCapacity = minCapacity;// 扩容容量比最大数组长度大,则使用最大整数长度if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win:elementData = Arrays.copyOf(elementData, newCapacity);
}

grow(minCapacity)方法对数组进行扩容,扩容大小为原数组的1.5倍,如果计算出的扩容容量比需要的容量小,则扩容大小为需要的容量,可以看到,第一次扩容的时候其实是10。如果扩容容量比数组最大容量大,则调用hugeCapacity(minCapacity)方法,将数组扩容为整数的最大长度,然后将elemetData数组指向新扩容的内存空间并将元素复制到新空间,这里使用的是 Arrays.copyOf(elementData, newCapacity)

public static int[] copyOf(int[] original, int newLength) {int[] copy = new int[newLength];System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength));return copy;
}

可以看到底层使用的是System.arraycopy,而这个copy的过程是比较耗性能的,因此建议初始化时预估一个容量大小。

「不用脑子的总结:用无参构造函数创建ArrayList后进行第一次扩容容量是10,后续则是1.5倍,底层调用的是System.arraycopy,而这个copy的过程是比较耗性能的,因此建议初始化时预估一个容量大小。」

ArrayList删除元素

ArrayList提供两种删除元素的方法,可以通过索引元素进行删除。两种删除大同小异,删除元素后,将后面的元素一次向前移动。

ArrayList.remove(int index)源码:

public E remove(int index) {rangeCheck(index);modCount++;E oldValue = elementData(index);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 workreturn oldValue;
}

删除元素时,首先会判断索引是否大于ArrayList的大小,如果索引范围正确,则将索引位置的下一个元素赋值到索引位置,将ArrayList的大小-1,最后返回移除的元素。

「不用脑子的总结:删除后底层调用的依旧是System.arraycopy,而这个copy的过程是比较耗性能的,因此才说频繁增删的尽量别用ArrayList。」

ArrayList遍历删除

@Override
public void forEach(Consumer<? super E> action) {Objects.requireNonNull(action);// 预设值了一个expectedModCount值final int expectedModCount = modCount;@SuppressWarnings("unchecked")final E[] elementData = (E[]) this.elementData;final int size = this.size;// 遍历过程中拿出来判断for (int i=0; modCount == expectedModCount && i < size; i++) {action.accept(elementData[i]);}// 如果对不上则报错if (modCount != expectedModCount) {throw new ConcurrentModificationException();}
}
public E remove(int index) {rangeCheck(index);// 修改了modCountmodCount++;E oldValue = elementData(index);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 workreturn oldValue;
}

从代码就可以看出来了,在遍历的时候会率先 预设值了一个expectedModCount值,然后再遍历拿出来判断,如果不一样了,则中断流程并且报错,而这个过程则涉及到了快速失败机制了,正常来说,ArrayList不允许遍历删除。

「不用脑子的总结:ArrayList通过预设值expectedModCount实现了快速失败机制,避免了多线程遍历删除或者增加,以及遍历过程中增删元素。」

集合的快速失败(fail-fast)

它是 Java 集合的一种错误检测机制,当多个线程对集合进行结构上的改变操作时,有可能会产生 fail-fast 机制。

迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。

场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

「不用脑子的总结:我们日常看到的Concurrent Modification Exception,其实就是触发了快速失败机制的表现,做法也很简单:在遍历的时候给你给modCount设置个备份expectedModCount,如果有多线程在搞,那么必定会导致modCount被改,那么就容易了,每次遍历的时候都检测下modCount变量是否为expectedModCount就可以了,如果不是意味着被改了,那我就不管,我就要报错。」

集合的安全失败(fail-safe)

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。

缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

「不用记忆的总结:那么为啥并发容器的时候不怕呢?简单,因为采用了安全失败机制,在遍历的时候直接拷贝了一份出来,这样就不会触发了。」

使用ArrayList的subList()需要注意的地方

public List<E> subList(int fromIndex, int toIndex) {subListRangeCheck(fromIndex, toIndex, size);return new SubList(this, 0, fromIndex, toIndex);
}
SubList(AbstractList<E> parent,int offset,int fromIndex,int toIndex) {this.parent = parent;this.parentOffset = fromIndex;this.offset = offset + fromIndex;this.size = toIndex - fromIndex;this.modCount = ArrayList.this.modCount;
}

subList()返回结果不可强制转为ArrayList类型,因为该方法实质是创建一个内部类SubList实例,这个SubList是AbstractList的实现类,并不继承于ArrayList。

通过上面源码可以看出,通过parent属性指定父类并直接引用了原有的List,并返回该父类的部分视图,只是指定了他要使用的元素的范围fromIndex(包含),endIndex(不包含)。

那么,如果对其原有或者子List做数据性修改,则会互相影响。如果对原有List进行结构性修改,则会踩坑Fast-fail,报错会抛出异常ConcurrentModification Exception。

ArrayList迭代器

看下迭代器的遍历和删除相关的源码

public boolean hasNext() {return cursor != size;
}@SuppressWarnings("unchecked")
public E next() {// 同样判断modCount != expectedModCount,不同则报错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];
}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();}
}

通过代码我们也可以看出ArrayList的迭代器是支持遍历删除的,因为在删除后会重新赋一次值给expectedModCount。

ArrayList和LinkedList的优劣

其实就是数组和链表的优劣势,ArrayList优点,支持随机访问,get(i)的时间复杂度为O(1),而缺点就是需要扩容,要复制数组,而且内部插入数据需要移动数据,插入删除的性能差;

对于LinkedList来说,优点就是容量理论上来说是无限,不存在扩容,而且可以很方便的插入和删除数据(性能损失在查找),而缺点就是不能随机访问,get(i)需要遍历。

貌似就是反过来的,所以在实际开发中也很容易区别,看是查找频繁、还是增删频繁,如果是查找频繁就用ArrayList,如果增删频繁就用LinkedList即可。

最后,对开头几道题的标准答案拿不准的,可以公众号后台输入:ArrayList

《好好面试》系列文目前已经连载26篇啦,

分享了自己多年来面试别人和被别人面试的经验,相信我,这是一个不管你在职、求职,都值得关注的一个系列。

往期推荐

为什么要破坏双亲委派模型,它不香吗?

阿里高频面试题,热部署了解吗?

Σ(っ°Д°;)っ找个对象"Object"还要用八股文?

《面试八股文》之Dubbo17卷

第一次面试,我差点被面试官打,就因为Collections.sort

高级开发竟然被构造器循环依赖难住了?

肥肥的主管和帅气的小饭饭讨论了下ForkJoinPool

聊聊Autowired的常考面试题

面试官告诉你什么是JMM和常考面试题

去年面了多个候选人,看看我挖的坑还有他们应该要补的Java基础(二)

去年面了多个候选人,看看我挖的坑还有他们应该要补的Java基础(一)

list中for循环删除多个元素为何报错?

手把手带你阅读源码,看看IoC容器的实现

【好好面试】学完Aop,连动态代理的原理都不懂?

你必须要懂的Spring-Aop之源码跟踪分析Aop

【好好面试】你必须要懂的Spring-Aop

你所不知道的HelloWorld背后的原理

连引用都答不上,凭什么说你是Java服务端开发

小饭饭:某游戏大厂高级开发,专门和主管抬杠的小组长。 目前写了三个系列文《全网最全的Caffeine教程》、《一起玩dubbo》和《好好面试》,《好好面试》分享了自己多年来面试别人和被别人面试的经验,有兴趣看面经、学dubbo和Caffeine的可以微信搜:稀饭下雪

对你有用是这篇文章值得被分享的唯一标准

万万没想到!!! 谷歌面试原来也问ArrayList相关推荐

  1. 万万没想到,面试中,连 ClassLoader类加载器 也能问出这么多问题.....

    1.类加载过程 类加载时机 「加载」 将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存上创建一个java.lang.Class对象用来封装类在方法区内的数据 ...

  2. python这个软件学会能做什么工作-万万没想到,学会Python即使不做程序员都能月入过万!...

    昨天,我公司面试了1个同学,应聘新媒体运营,专业能力还不错.他简历上技能栏还写着会Python,我问了他一个通过爬虫采集数据的问题,他都顺畅的答出来了. 最后聊薪资时,他说期待7000,我直接给他开了 ...

  3. 万万没想到,坤坤洗白的第一步是周琦…

    前晚(2日)中国男篮与波兰队的关键一战惜败后,几乎所有中国球迷都在哭"琦","琦"到一夜未眠. 周琦关键时刻边线发球失误,硬生生把中国男篮提前出线的jio给拖了 ...

  4. 【杂谈】万万没想到,有三还有个保密的‘朋友圈’,那里面都在弄啥!

    万万没想到,有一天我们也会标题党,透过标题看本质,今天说的确实是那么回事儿也. 一直以来我们都有一个不公开的私密社区叫有三AI知识星球,但是经常遇到一些朋友,甚至是加入季划的同学都在问我,那是个啥?看 ...

  5. 万万没想到,我的炼丹炉玩坏了

    一只小狐狸带你解锁NLP/ML/DL秘籍 作者:夕小瑶 前记 众所周知,夕小瑶是个做NLP的小可爱. 虽然懂点DL框架层知识,懂点CUDA和底层,但是我是做算法的哎,平时debug很少会遇到深度学习框 ...

  6. 万万没想到,一个技术方案帮实习生追到了运营妹子!

    上回说到,公司的新业务增长速度放缓,运营部门提出要发展短视频来促进更快的业务增长,而我也因为提前准备好了技术预案再一次得到老板的赞赏(了解详情请看上集:一个技术预案,让老板当场喊出了奥利给 ). 既然 ...

  7. 《万万没想到》读后感

    在 得到APP中订阅了万维钢精英日课,随后就买了这本<万万没想到>.读完后整理了这篇读书笔记. 匹夫怎么逆袭 战胜巨人的关键: 1.要知道你的不利条件,在某些情况下可能是你的有利条件:而巨 ...

  8. 显卡暴涨,这我万万没想到啊

    点击上方"视学算法",选择加"星标"或"置顶" 重磅干货,第一时间送达 梦晨 晓查 发自 凹非寺  量子位 报道 | 公众号 QbitAI ...

  9. 科学家们竟用乐高观察细胞,网友:万万没想到啊

    杨净 子豪 发自 凹非寺 量子位 报道 | 公众号 QbitAI 玩乐高还能玩出个显微镜?! 原本以为是一个普普通通的玩具. 没想到,还真能当成显微镜来用,是能看到细胞的那种. 真·高端新玩法! 比如 ...

最新文章

  1. 算法工程师的落地应用公开课
  2. 关于SOCKET资源堆栈
  3. 设计模式之UML关系图
  4. K8S终端UI界面--K9S
  5. GitHub 项目精选(2022.5.18更新)
  6. Charles抓取https请求及常见问题解决
  7. 机器朗读发音电脑说话
  8. MongoDB入门+深入(二)--项目实战
  9. 人类有两大学习能力,即记忆力和理解力
  10. VS 错误: cout 不明确
  11. P01914100尹自杨
  12. 百兆以太网口通信速率_以太网发送速率(传输速率)和传播速率
  13. python numpy.arry, pytorch.Tensor及原生list相互转换
  14. openlayers3中geowebcache的使用
  15. 百度离线地图示例之三:矢量图
  16. mingw-w64安装
  17. AM335x Beaglebone black 蚂蚁矿机L3+控制板 资源下载
  18. dagum基尼系数分析全流程
  19. 冰炭不投day博客导航
  20. 三子棋游戏(人机对战)

热门文章

  1. G7400参数 奔腾G7400处理器怎么样
  2. OI退役记,第三部分,2017省选季(上)
  3. 行测:言语理解和表达
  4. 关于超变态的装备改造脚本
  5. python tello 教育版 编队飞行_tello edu 官方python接口的改进
  6. 转:inverse = “true” example and explanation
  7. Thinkpad T490安装Ubuntu18.04问题总结
  8. 快速画圆切线lisp_【求助】关于AutoLISP command “line”命令问题(画圆内接正五边形) 不要推荐我使用“polygon”命令...
  9. verilog学习笔记(1) vivado 仿真小例子
  10. 合创科技C4D设计师网站大全