#1024程序员节 | 征文#
基本介绍
ArrayList是Java中的一个集合类,这篇文章就先由ArrayList和Array的区别开始吧!
- Array是一个静态数组,ArrayList是一个动态数组(可扩容)
- Array可以存储基本数据类型和引用数据类型,ArrayList只能存储引用数据类型(可以用包装类)
- Array只能通过下标获取值,ArrayList有丰富的api增删。
- ArrayList支持泛型
在了解了这些内容后,你一定会好奇 ArrayList 是如何实现动态长度的。本文将分为两个章节:源码分析和扩容机制。对于已有源码阅读基础的读者,可以直接阅读源码分析章节来深入了解扩容机制;而对于刚开始学习 Java 的朋友,建议先阅读扩容机制章节,然后再回过头来阅读源码部分。
源码分析
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;
/**
* 如果你传入0构造0大小的数组,就返回这个给你。
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
//当你无参构造后返回这个默认的给你,再你传入第一个值后去扩容
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 保存ArrayList数据的数组,transient:不进行序列化
*/
transient Object[] elementData;
/**
* ArrayList 所包含的元素个数
*/
private int size;
/**
* 通过有参构造用户可以自定义ArrayList数组的大小
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//如果传入的参数大于0,创建参数大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//如果传入的参数等于0,创建空数组
//EMPTY_ELEMENTDATA是提前创好的,避免在创建空数组时浪费不必要的内存。
this.elementData = EMPTY_ELEMENTDATA;
} else {
//其他情况,抛出异常
throw new IllegalArgumentException("Illegal Capacity: " +
initialCapacity);
}
}
/**
* 默认无参构造函数
* 无参构造时返回一个空的数组,当加入第一个数据后才扩容到10
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 构造一个包含指定集合的元素的列表
*/
public ArrayList(Collection<? extends E> c) {
//将指定集合转换为数组
elementData = c.toArray();
//如果elementData数组的长度不为0
if ((size = elementData.length) != 0) {
// 如果elementData不是Object类型数据
if (elementData.getClass() != Object[].class)
//将原来不是Object类型的elementData数组的内容,赋值给新的Object类型的elementData数组
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 如果传入集合长度为0,返回空数组。
this.elementData = EMPTY_ELEMENTDATA;
}
}
/**
* 如果ArrayList中的元素比当前数组容量少,就会减少数组的大小。
* 当大量清理数据后调用此方法节省内存。
*/
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
//扩容机制
/**
* 作用:在添加大量元素之前扩容,从而避免每次添加元素前都引发数组的扩容增大性能开销
*/
public void ensureCapacity(int minCapacity) {
//如果存储数据的数组不是空数组,则minExpand为0,否则为10
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
? 0
: DEFAULT_CAPACITY;
//如果存储数据需要的容量大于已有的空间
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
// 根据给定的最小容量和当前数组元素来计算所需容量。
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果当前数组是默认的空数组,那么返回默认容量和所需空间的最大值。
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 否则直接返回最小容量
return minCapacity;
}
private void ensureCapacityInternal(int minCapacity) {
//calculateCapacity(elementData, minCapacity),计算当前所需的最小容量
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果所需最小容量大于数组的长度就扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* 要分配的最大数组大小
* 为什么要减8,是因为java中每个对象都有一个对象头,通常对象头的开销为8字节。所以最大存储大小要减去8.
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* ArrayList扩容的核心。
*/
private void grow(int minCapacity) {
// oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
//扩容到旧容量的1.5倍
//将oldCapacity 右移一位,其效果相当于oldCapacity /2,
int newCapacity = oldCapacity + (oldCapacity >> 1);
//扩容后检查一下扩完的容量是否满足需求,如果还是不够需要容量的话直接把需要的容量赋值给new数组容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//再检查新容量是否超出了ArrayList所定义的最大容量,
//若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//把旧数组copy到新数组中
elementData = Arrays.copyOf(elementData, newCapacity);
}
//比较minCapacity和 MAX_ARRAY_SIZE确保传入的数合理,确保创建的数组是在JVM能处理的范围中。
//如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 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;
}
扩容机制
ArrayList的创建
无参
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
有的同学会说ArrayList的默认空间不是10吗?怎么无参构造返回的是空数组?
当你调用无参构造方法初始化ArrayList,会返回给你一个默认创建好的空数组。等到你添加第一个元素进去的时候他才会扩容到默认容量(一般是10)。
为什么空数组提前创建好了?
为了避免初始化ArrayList时创建空数组的性能开销。
有参
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
当你用有参构造方法初始化ArrayList,那它会帮你创建一个对应大小的数组。
ArrayList的扩容
add
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
调用add方法后会调用ensureCapacityInternal方法并传入当前集合的长度。
ensureCapacityInternal
private void ensureCapacityInternal(int minCapacity) {
//calculateCapacity(elementData, minCapacity),计算当前所需的最小容量
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 根据给定的最小容量和当前数组元素来计算所需容量。
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果当前数组是默认的空数组,那么返回默认容量和所需空间的最大值。
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 否则直接返回最小容量
return minCapacity;
}
根据calculateCapacity计算当前集合所需列表,然后再传入ensureExplicitCapacity中。
ensureExplicitCapacity
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果所需最小容量大于数组的长度就扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
如果需要容量小于集合长度就进行扩容
核心方法:grow
/**
* ArrayList扩容的核心。
*/
private void grow(int minCapacity) {
// oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
//扩容到旧容量的1.5倍
//将oldCapacity 右移一位,其效果相当于oldCapacity /2,
int newCapacity = oldCapacity + (oldCapacity >> 1);
//扩容后检查一下扩完的容量是否满足需求,如果还是不够需要容量的话直接把需要的容量赋值给new数组容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//再检查新容量是否超出了ArrayList所定义的最大容量,
//若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//把旧数组copy到新数组中
elementData = Arrays.copyOf(elementData, newCapacity);
}
//比较minCapacity和 MAX_ARRAY_SIZE确保传入的数合理,确保创建的数组是在JVM能处理的范围中。
//如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 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;
}
- 通过位运算把长度扩为原来的1.5倍。
- 判断当前容量是否够用,如不够直接将所需容量赋给newCapacity
- 判断当前容量是否超出MAX_ARRAY_SIZE(ArrayList最大容量),未超出就扩容到MAX_ARRAY_SIZE。
MAX_ARRAY_SIZE
/**
* 要分配的最大数组大小
* 为什么要减8,是因为java中每个对象都有一个对象头,通常对象头的开销为8字节。所以最大存储大小要减去8.
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
对象头开销: 在 Java 中,每个对象(包括数组)都有一个对象头,用于存储对象的元数据。这个对象头的大小通常为 8 字节(在 64 位 JVM 中)。因此,为了保证有效的数组大小,必须考虑到这个开销。
为了防止数组溢出,需要确保数组创建时能容纳头部的开销。
ensureCapacity
/**
* 作用:在添加大量元素之前扩容,从而避免每次添加元素前都引发数组的扩容增大性能开销
*/
public void ensureCapacity(int minCapacity) {
//如果存储数据的数组不是空数组,则minExpand为0,否则为10
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
? 0
: DEFAULT_CAPACITY;
//如果存储数据需要的容量大于已有的空间
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
预防扩容: 通过在添加新元素之前确保容量,ensureCapacity
可以减少动态扩容的频率,从而避免在每次添加元素时都引发数组复制的性能开销。