@TOC 本文编写灵感来源我之前整理的面经的题目之一:
2020年11月最新互联网大厂面试经验分享【网易、阿里、腾讯、京东、百度、爱奇艺、字节、小米、美团、搜狐、58】 其他相关文章 一文干翻Integer、int等基础数据类型和包装类型相关问题
实现原理
以下所有内容都是基于Jdk1.8
底层实现
底层是基于数组实现的,自带扩容机制。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
ArrayList 实现了 List 接口,继承了 AbstractList 抽象类,底层是数组实现的,并且实现了
自增扩容数组大小。
ArrayList 还实现了 Cloneable 接口和 Serializable 接口,所以他可以实现克隆和序列化。
ArrayList 还实现了 RandomAccess 接口。你可能对这个接口比较陌生,不知道具体的用
处。通过代码我们可以发现,这个接口其实是一个空接口,什么也没有实现,那 ArrayList
为什么要去实现它呢?
其实 RandomAccess 接口是一个标志接口,他标志着“只要实现该接口的 List 类,都能
实现快速随机访问”。
核心属性
ArrayList 属性主要由数组长度 size、对象数组 elementData、初始化容量default_capacity 等组成, 其中初始化容量默认大小为 10。
/ 默认初始化容量
private static final int DEFAULT_CAPACITY = 10;
// 对象数组
transient Object[] elementData;
// 数组长度
private int size;
常用方法
构造器
有参构造方法,初始化容量根据入参创建。
无参构造初始化的是空数组,只有在第一次调用add()时,才会创建一个数组长度为DEFAULT_CAPACITY = 10。
public ArrayList(int initialCapacity) {
// 初始化容量不为零时,将根据初始化值创建数组大小
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// private static final Object[] EMPTY_ELEMENTDATA = {};
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList() {
// 初始化默认为空数组,第一次add的时候实例化一个长度为10的数组
// private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
新增元素
1、直接将元素加到数组的末尾
时间复杂度O(1),不会产生数据移动
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
2、添加元素到任意位置。
根据插入位置,会产生数据移动,插入位置越靠前,移动数据越多
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
- 两个方法都会判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 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;
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);
}
扩容时机:实际存储数据size+1是否大于设定容量,size + 1 - elementData.length > 0
扩容大小:为原来的1.5倍, int newCapacity = oldCapacity + (oldCapacity >> 1);
- 任意位置添加的方法,会导致数组数据移动
查询元素
由于底层是用数组存储,所以支持随机访问,效率很高。
时间复杂度O(1)
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
删除元素
1、根据索引删除
由于底层是数组存储,为了保障数组的连续性,删除数据会导致数据移动,删除位置越靠前,移动数据越多。
时间复杂度(1)
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;
}
2、根据元素对象删除
实际是遍历整个ArrayList,使用对象的equals()判断是否相等,先查出该对象所在的索引位置,然后根据索引位置删除元素。
由于底层是数组存储,为了保障数组的连续性,删除数据会导致数据移动,删除位置越靠前,移动数据越多。
时间复杂度O(n)
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;
}
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
}
包含元素
遍历整个ArrayList,使用对象的equals()判断是否相等,来进行查找。
时间复杂度O(n)
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
常见问题
1.为什么ArrayList查询速度快?
ArrayList底层是基于数组实现,可以根据元素下标进行查询,查询方式为(数组首地址+元素长度*下标,基于这个位置读取相应的字节数就可以了),如果数组存的是对象,怎么根据下标定位元素所在位置?(对象数组每个元素存放的是对象的引用,而引用类型如果开启指针压缩占用4字节,不开启则占用8字节,所以对象数组同样适用上面的公式
2.讲一讲ArrayList的扩容?
ArrayList底层是基于数组实现,所以创建ArrayList会给数组指定一个初始容量,默认值为10,因为必须指明数组的长度才能给数组分配空间;由于数组的特性,ArrayList扩容是创建一个更大的数组,然后将原来的元素拷贝到更大的数组中。
扩容时机:实际存储数据size+1是否大于设定容量,size + 1 - elementData.length > 0
扩容大小:为原来的1.5倍, int newCapacity = oldCapacity + (oldCapacity >> 1);
采用了位运算,右移一位,其实就是除以2,重点:位运算效率高,装逼炫技必备啊;
3.说说快速失败机制 “fail-fast”?
Java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
其实关键就是这个modCount 变量,就是在调用add(),remove()等方法时,会增加modCount的值,每当迭代器使用next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
private class Itr implements Iterator<E> {
int expectedModCount = modCount;
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];
}
final void checkForComodification() {
if (modCount != expectedModCount)// 看看吧,大兄弟
throw new ConcurrentModificationException();
}
4.讲一讲迭代器Iterator遍历?
Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。
Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。
代码如下:
List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()){
String obj = it. next();
System. out. println(obj);
}
遍历删除元素
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
*// do something*
it.remove();
}
5.List遍历的几种方式,来详细聊聊?
-
for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。
for(初始化; 布尔表达式; 更新) { //代码语句 } -
迭代器iterator遍历。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。
Iterator it = list.iterator(); while(it.hasNext()) { Object obj = it.next(); } -
foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。
for(元素类型t 元素变量x : 遍历对象obj){ 引用了x的java语句; }字节码对比
foreach,iterator//使用Iterator的字节码: Code: 0: new #16 // class java/util/ArrayList 3: dup 4: invokespecial #18 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: invokeinterface #19, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator; 14: astore_2 15: goto 25 18: aload_2 19: invokeinterface #25, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object; 24: pop 25: aload_2 26: invokeinterface #31, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z 31: ifne 18 34: return //使用foreach的字节码: Code: 0: new #16 // class java/util/ArrayList 3: dup 4: invokespecial #18 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: invokeinterface #19, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator; 14: astore_3 15: goto 28 18: aload_3 19: invokeinterface #25, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object; 24: checkcast #31 // class loop/Model 27: astore_2 28: aload_3 29: invokeinterface #33, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z 34: ifne 18 37: return总结:
-
如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。
-
如果没有实现该接口,表示不支持 Random Access,如LinkedList。
推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。
if (list instanceof RandomAccess) {
//使用传统的for循环遍历。
} else {
//使用Iterator或者foreach。
}
6.说一下ArrayList 的优缺点?
优点:
- ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。
- ArrayList 在顺序添加一个元素的时候非常方便。
缺点
- 删除元素的时候,可能需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
- 插入元素的时候,可能也需要做一次元素复制操作,缺点同上。
ArrayList比较适合尾部添加、删除,随机访问的场景。
7.ArrayList 和 LinkedList 的区别是什么?
- 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
- 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
- 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
- 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
- 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
综合来说,更推荐使用 ArrayList,而在非尾部插入和删除操作较多时,更推荐使用 LinkedList。
8.ArrayList是线程安全的么?
当然不是,线程安全版本的数组容器是Vector。
Vector的实现很简单,就是把所有的方法统统加上synchronized就完事了。
你也可以不使用Vector,用Collections.synchronizedList把一个普通ArrayList包装成一个线程安全版本的数组容器也可以,原理同Vector是一样的,就是给所有的方法套上一层synchronized。
扩展
1、HikariCP连接池中自定义扩展基于ArrayList的FastList
FastList是一个List接口的精简实现,只实现了接口中必要的几个方法。JDK ArrayList每次调用get()方法时都会进行rangeCheck检查索引是否越界,FastList的实现中去除了这一检查,只要保证索引合法那么rangeCheck就成为了不必要的计算开销(当然开销极小)。此外,HikariCP使用List来保存打开的Statement,当Statement关闭或Connection关闭时需要将对应的Statement从List中移除。通常情况下,同一个Connection创建了多个Statement时,后打开的Statement会先关闭。ArrayList的remove(Object)方法是从头开始遍历数组,而FastList是从数组的尾部开始遍历,因此更为高效。
简而言之就是 自定义数组类型(FastList)代替ArrayList:避免每次get()调用都要进行range check,避免调用remove()时的从头到尾的扫描
public final class FastList<T> implements List<T>, RandomAccess, Serializable
public T get(int index)
{
return elementData[index];
}
public boolean remove(Object element)
{
for (int index = size - 1; index >= 0; index--) {
if (element == elementData[index]) {
final int numMoved = size - index - 1;
if (numMoved > 0) {
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
}
elementData[--size] = null;
return true;
}
}
return false;
}
参考:
极客时间相关课程
QQ群【837324215】 关注我的公众号【Java大厂面试官】,回复:架构、资源等关键词(更多关键词,关注后注意提示信息)获取更多免费资料。
公众号也会持续输出高质量文章,和大家共同进步。