☆* o(≧▽≦)o *☆嗨~我是小奥🍹
📄📄📄个人博客:小奥的博客
📄📄📄优快云:个人优快云
📙📙📙Github:传送门
📅📅📅面经分享(牛客主页):传送门
🍹文章作者技术和水平有限,如果文中出现错误,希望大家多多指正!
📜 如果觉得内容还不错,欢迎点赞收藏关注哟! ❤️
文章目录
Java集合之ArrayList源码详解
概述
ArrayList
实现了List
接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null
元素,底层通过数组实现。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
- 继承
AbstractList
类,提供了相关修改、删除、遍历等功能 - 实现
RandomAccess
,空接口,作为随机访问的标志,代表只要实现了这个接口,就能支持快速随机访问。在ArrayList中,我们可以通过元素的序号快速获取元素对象,这就是快速随机访问。 - 实现
Cloneable
,空接口,作为支持克隆的标志,实现这个接口后,在类中重写Object的clone方法,后面通过调用clone()才能克隆成功,如果实现这个接口,则会抛出CloneNotSupportedException
(克隆不被支持)异常。 - 实现
Serialization
,空接口,作为可以实现序列化和反序列化的标志。
底层数据结构
构造函数
ArrayList有三个构造函数,参数分别是 空参
、initialCapacity
、Collection<? extends E> c
。
/**
* 带初始容量参数的构造函数(用户可以在创建ArrayList对象时自己指定集合的初始大小)
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 如果传入参数大于0,创建initialCapacity大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 如果传入参数等于0,则创建空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 其他情况则抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
* 默认无参构造函数
* DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0,初始化为10,
* 也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 构造一个包含指定集合元素的列表。按照指定集合的迭代器返回的顺序。
*/
public ArrayList(Collection<? extends E> c) {
// 将指定集合转换为数组
Object[] a = c.toArray();
if ((size = a.length) != 0) {
// 如果elementData数组的长度不为0
if (c.getClass() == ArrayList.class) {
// 如果数组元素是ArrayList类型,则直接覆盖elementData
elementData = a;
} else {
// 如果不是ArrayList类型,赋值给新的Object类型的elementData
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// 其他情况,用空数组代替
elementData = EMPTY_ELEMENTDATA;
}
}
根据源码可以得知:
- 如果指定容量:
initialCapacity
> 0,则使用initialCapacity
容量创建数组;initialCapacity
= 0, 则默认创建空数组;initialCapacity
< 0,抛出Illegal ArgumentException
异常。
- 如果不指定容量:
- 直接默认使用创建空数组。
扩容原理
扩容流程如下:
1、ArrayList
的扩容是通过add方法来触发的,在add元素之前首先尝试将容量 + 1,判断是否需要扩容。
2、通过计算得到最小扩容量。如果底层数组为空,则得到传入的容量与默认初始容量10之间的最大值。
3、判断是否需要扩容,如果需要的最小容量大于目前的长度,那么就去扩容,否则就不进行扩容。
4、扩容的核心是将容量扩大到原来的1.5倍,如果新容量比最小扩容容量还要小,那么就把新容量 = 最小扩容容量;如果新容量大于最大的容量(Integer.MAX_VALUE - 8
),那么将进行判断,如果指定的容量小于0,则抛出异常,如果大于最大容量,则新容量为 Integer.MAX_VALUE
,否则等于最大容量。
5、扩容之后将数组拷贝返回。调用Arrays.copyOf()
方法复制数组,其底层还是调用了本地方法System.arraycopy()
。
下面我们一起分析一下这个流程。
add触发扩容
ArrayList
的扩容是通过add方法来触发的,在add元素之前首先尝试将容量 + 1,判断是否需要扩容。
/**
* 将指定的元素追加到此列表的末尾。
*/
public boolean add(E e) {
// 确保容量足够
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
ensureCapacityInternal检查
// 确保内部容量达到指定的最小容量。
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
// 调用grow方法进行扩容
grow(minCapacity);
}
calculateCapacity 计算容量
通过计算得到最小扩容量。如果底层数组为空,则得到传入的容量与默认初始容量10之间的最大值。
// 根据给定的最小容量和当前数组元素来计算所需的容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 如果当前数组元素为空数组(初始情况),则返回 max(默认容量,最小容量)
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 否则直接返回最小容量
return minCapacity;
}
grow进行扩容
扩容的核心是将容量扩大到原来的1.5倍,如果新容量比最小扩容容量还要小,那么就把新容量 = 最小扩容容量;如果新容量大于最大的容量(
Integer.MAX_VALUE - 8
),那么将进行判断,如果指定的容量小于0,则抛出异常,如果大于最大容量,则新容量为Integer.MAX_VALUE
,否则等于最大容量。
/**
* 要分配的最大数组大小
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 增加容量以确保它至少可以容纳最小容量参数指定的元素数量。
*/
private void grow(int minCapacity) {
// oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
// oldCapacity >> 1,相当于 oldCapacity/2
// 将新容量更新为旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 检查新容量
if (newCapacity - minCapacity < 0)
// 如果小于最小需要容量,那么将最小需要容量当作新容量
newCapacity = minCapacity;
// 检查新容量是否超出了ArrayList定义的最大容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 如果超出了,则调用hugeCapacity()来比较minCapacity和MAX_ARRAY_SIZE
newCapacity = hugeCapacity(minCapacity);
// 调用Arrays.copyOf复制数组元素
elementData = Arrays.copyOf(elementData, newCapacity);
}
// 比较minCapacity和 MAX_ARRAY_SIZE
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
Arrays.copy数组拷贝
扩容之后将数组拷贝返回。调用
Arrays.copyOf()
方法复制数组,其底层还是调用了本地方法System.arraycopy()
。
// java.util.Arrays#copyOf(U[], int, java.lang.Class<? extends T[]>)
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
/**
* 复制数组,属于浅拷贝
* @param src 源数组
* @param srcPos 源数组中的起始位置
* @param dest 目标数组
* @param destPos 目标数组中的起始位置
* @param length 要复制的数组元素的数量
*/
// java.lang.System#arraycopy
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
验证System.arraycopy是浅拷贝
public class Test {
public static void main(String[] args) {
Student[] students = new Student[]{
new Student(1,"小明"),
new Student(2,"小红"),
new Student(3,"小黑"),
new Student(4,"小白")
};
Student[] target = new Student[students.length];
System.arraycopy(students, 0, target, 0, students.length);
System.out.println("源对象是否和复制后的目标对象一致: " + ((students[0].equals(target[0])) ? "浅拷贝" : "深拷贝"));
// 打印复制后的目标数组
System.out.println(Arrays.toString(target));
// 更改target数组
target[0].setName("测试");
System.out.println("student[0].getName() = " + students[0].getName());
}
}
控制台输出以下内容:
源对象是否和复制后的目标对象一致: 浅拷贝
[Student{
id=1, name='小明'}, Student{
id=2, name='小红'}, Student{
id=3, name='小黑'}, Student{
id=4, name='小白'}]
student[0].getName() = 测试
复制的过程如下:
ArrayList面试题总结
ArrayList在JDK1.7和JDK1.8的区别
在JDK1.7的时候,创建集合不指定容量时,底层直接创建了长度为10的Object数组,然后在调用add()方法向集合中添加元素时,才会考虑扩容的问题。
在JDK1.8的时候,创建集合不指定容量时,底层的Object数组初始化为空,只有在第一次调用add()方法的时候,才会初始化数组。
这样做的目的可以节省内存的消耗,因为在添加元素时数组名将指针指向了新的数组且老数组是一个空数组这样有利于System.gc(),并不会一直占据内存。
JDK1.7中list的创建,类似于单例模式中的饿汉式,在初始化的时候就创建好;而1.8中,则类似于单例模式中的懒汉式,采用延迟加载,节省内存空间。
ArrayList和LinkedList的区别
(1)是否保证线程安全: ArrayList
和 LinkedList
都是不同步的,也就是不保证线程安全;
(2)底层数据结构: Arraylist
底层使用的是 Object
数组;LinkedList
底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
(3)存取效率:ArrayList底层使用数组实现,所以它的查找时间复杂度是O(1),插入和删除时间复杂度是O(n);而LinkedList底层使用链表实现的,所以它的查找时间复杂度是O(n),插入和删除只需要改变元素指针的指向即可,所以是O(1)。
(4)内存空间占用: ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
ArrayList和Vector的区别
(1)是否线程安全:ArrayList线程不安全,而Vector是线程安全的。
(2)底层数据结构:两者底层都是使用数组实现的。
ArrayList扩容流程
(1)第一步,在add方法添加元素的时候,首先尝试将size + 1
,判断是否需要扩容;
(2)第二步,通过计算得到最小扩容量。如果数组是默认的空数组,那么会取最小扩容量和默认容量10之间的最大值作为新的最小容量,然后拿这个最小容量去判断是否需要扩容。
(3)第三步,如果最小容量比当前数组的长度还大,则进行扩容。
(4)第四步,先将旧容量扩大1.5倍作为新容量,如果新容量小于最小容量,则将最小容量作为新容量;如果新容量大于最大容量(Integer.MAX_VALUE - 8
),那么去调整新容量。如果新容量小于0,则抛出OutOfMemoryError()
异常,否则将新容量设置为Integer.MAX_VALUE
。
(5)第五步,调用Arrays.copyOf()
方法拷贝数组到原数组。
ArrayList可以添加null值吗
ArrayList
中可以存储任何类型的对象,包括 null
值。不过,不建议向ArrayList
中添加 null
值, null
值无意义,会让代码难以维护比如忘记做判空处理就会导致空指针异常。
源码
以下ArrayList源码版本为JDK8
package java.util;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import sun.misc.SharedSecrets;
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
/**
* 默认初始容量大小
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空数组(用于空实例)
*/
private static final Object[] EMPTY_ELEMENTDATA = {
};
/**
* 用于默认大小空实例的共享空数组实例。
* 我们把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
};
/**
* 存储ArrayList元素的数组缓冲区。
* ArrayList容量就是这个数组缓冲区的长度。任何带有 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* 的空数组列表将在添加第一个元素时扩展为DEFAULT_CAPACITY。
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* ArrayList 所包含的元素个数
*/
private int size;
/**
* 带初始容量参数的构造函数(用户可以在创建ArrayList对象时自己指定集合的初始大小)
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 如果传入参数大于0,创建initialCapacity大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 如果传入参数等于0,则创建空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 其他情况则抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
* 默认无参构造函数
* DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0,初始化为10,
* 也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 构造一个包含指定集合元素的列表。按照指定集合的迭代器返回的顺序。
*/
public ArrayList(Collection<? extends E> c) {
// 将指定集合转换为数组
Object[] a = c.toArray();
if ((size