您现在的位置是:亿华云 > IT科技
万万没想到!!! 谷歌面试原来也问ArrayList
亿华云2025-10-08 21:09:48【IT科技】2人已围观
简介本文转载自微信公众号「稀饭下雪」,作者帅气的小饭饭 。转载本文请联系稀饭下雪公众号。前几天H同学和我聊了下去谷歌的面试经验,令我诧异的是,没想到谷歌也问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; // 新的b2b供应网数组容量 = 旧数组长度 + 旧数组长度 / 2 // oldCapacity = 10 oldCapacity >> 1 --- 5 // 例如10的二进制为 : 0000 1010 >> 1 -----> 0000 0101 = 5 int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity <= 0) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) // 如果一开始没有定义初始容量这时newCapacity=0,返回默认容量10 // 可以得出当无参new 一个ArrayList()时候,这个ArrayList()为空集合,size为0 return Math.max(DEFAULT_CAPACITY, minCapacity); if (minCapacity < 0) // overflow throw 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 code if (minCapacity - elementData.length > 0) grow(minCapacity); }ensureExplicitCapacity(minCapacity)方法用来判断是否需要扩容,假如第一次添加元素,minCapacity为10,elementData容量为0,那么就需要去扩容。调用grow(minCapacity)方法。
// 数组的最大容量 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; private void grow(int minCapacity) { // overflow-conscious code int 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 work return 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); // 修改了modCount 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 work return 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即可。
很赞哦!(6517)
上一篇: 4.选择顶级的域名注册服务商
相关文章
- 公司名字不但要与其经营理念、活动识别相统一,还要能反映公司理念,服务宗旨、商品形象,从而才能使人看到或听到公司的名称就能产生愉快的联想,对商店产生好感。这样有助于公司树立良好的形象。
- 教你如何在 Linux 中锁定和解锁多个用户
- 一文彻底搞懂JS前端5大模块化规范及其区别
- C语言边角料2:用纯软件来代替Mutex互斥锁
- 并非一个好米任何人都会给你一个好的价格。那你该如何用以有的好米卖出最理想的价格呢?
- Rtos 任务堆栈大小与代码量有关吗?
- 前端在线开发工具,有这6款足够了
- 前端在线开发工具,有这6款足够了
- 以上的就是为大家介绍的关于域名的详解域名注册:域名注册0
- 一篇带给你Swift — 协议(Protocol)