ArrayList 是 Java 集合框架中比较常用的数据结构,底层基于数组实现容量大小的动态变化,所以会占用一块连续的内存空间。ArrayList 是线程不安全的,允许元素为 null。它继承了 AbstractList,实现了 List,RandomAccess,Cloneable,java.io.Serializable 接口,所以ArrayList 是支持快速访问、复制、序列化的。
一、数据结构
ArrayList 的数据结构如图所示:
二、成员变量
/** * 默认初始容量大小为 10 */ private static final int DEFAULT_CAPACITY = 10; /** * 指定该ArrayList容量为0时,返回该空数组。 */ private static final Object[] EMPTY_ELEMENTDATA = {}; /** * 当调用无参构造方法,返回的是该数组。刚创建一个ArrayList 时,其内数据量为0。 * 它与EMPTY_ELEMENTDATA的区别就是:该数组是默认返回的,而后者是在用户指定容量为0时返回。 */ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** * 存储集合元素的底层实现:真正存放元素的数组。 * 被标记为transient,在对象被序列化的时候不会被序列化。 */ transient Object[] elementData; /** * 实际元素数量 */ private int size;
三、构造函数分析
1、无参构造函数
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
注释是说构造一个容量大小为 10 的空的 list 集合,但构造函数了只是给 elementData 赋值了一个空的数组,其实是在第一次添加元素时容量扩大至 10 的。
2、指定容量的构造函数
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); } }
由以上源码可见: 当使用无参构造函数时是把 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 赋值给 elementData。 当 initialCapacity 为零时则是把 EMPTY_ELEMENTDATA 赋值给 elementData。 当 initialCapacity 大于零时初始化一个大小为 initialCapacity 的 object 数组并赋值给 elementData。
3、传入一个集合类作为参数的构造函数
构造一个包含指定 collection 的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的。
public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } }
四、核心方法分析
1、add(E e)
public boolean add(E e) { // 容量空间检查,如果不够,容量加1。注意:只加1,保证资源不被浪费 ensureCapacityInternal(size + 1); // Increments modCount!! // 数组末尾增加一个元素,并且修改 size elementData[size++] = e; return true; }
从源码中可以看出,add 方法有两个步骤:
① 空间检查,如果需要则扩容。
② 插入元素。
数组容量检查方法源码如下:
private void ensureCapacityInternal(int 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; } public void ensureCapacity(int minCapacity) { int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) // any size if not default element table ? 0 // larger than default for default empty table. It's already // supposed to be at default size. : DEFAULT_CAPACITY; if (minCapacity > minExpand) { ensureExplicitCapacity(minCapacity); } }
数组扩容操作:
private void ensureExplicitCapacity(int minCapacity) { modCount++; // 确保指定的最小容量 > 数组缓冲区当前的长度 if (minCapacity - elementData.length > 0) // 扩容 grow(minCapacity); } private void grow(int minCapacity) { // 获取当前数组的容量 int oldCapacity = elementData.length; // 扩容。新的容量=当前容量+当前容量/2.即将当前容量增加一半。 int newCapacity = oldCapacity + (oldCapacity >> 1); //如果扩容后的容量还是小于想要的最小容量 if (newCapacity - minCapacity < 0) //将扩容后的容量再次扩容为想要的最小容量 newCapacity = minCapacity; //如果扩容后的容量大于临界值,则进行大容量分配 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 数组重新复制 elementData = Arrays.copyOf(elementData, newCapacity); }
对于扩容方法的总结如下:
- 进行空间检查,决定是否进行扩容,以及确定最少需要的容量。
- 如果确定扩容,就执行grow(int minCapacity),minCapacity为最少需要的容量。
- 第一次扩容,逻辑为newCapacity = oldCapacity + (oldCapacity >> 1);即在原有的容量基础上增加一半。
- 第一次扩容后,如果容量还是小于minCapacity,就将容量扩充为minCapacity。
- 对扩容后的容量进行判断,如果大于允许的最大容量MAX_ARRAY_SIZE,则将容量再次调整为MAX_ARRAY_SIZE。至此扩容操作结束。
2、add(int index, E element)
添加元素 element 到 index 位置。
/** * 在制定位置插入元素。当前位置的元素和index之后的元素向后移一位 * * @param index 即将插入元素的位置 * @param element 即将插入的元素 * @throws IndexOutOfBoundsException 如果索引超出size */ public void add(int index, E element) { //越界检查 rangeCheckForAdd(index); //确认list容量,如果不够,容量加1。注意:只加1,保证资源不被浪费 ensureCapacityInternal(size + 1); // Increments modCount!! // 对数组进行复制处理,目的就是空出index的位置插入element,并将index后的元素位移一个位置 System.arraycopy(elementData, index, elementData, index + 1,size - index); //将指定的index位置赋值为element elementData[index] = element; //实际容量+1 size++; }
add(int index, E e)需要先对元素进行移动,然后完成插入操作,也就意味着该方法有着线性的时间复杂度,即O(n)。
3、remove(int index)
ublic 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); // size减一,然后将索引为size-1处的元素置为null。为了让GC起作用,必须显式的为最后一个位置赋null值 elementData[--size] = null; return oldValue; }
注意:为了让GC起作用,必须显式的为最后一个位置赋null值。上面代码中如果不手动赋null值,除非对应的位置被其他元素覆盖,否则原来的对象就一直不会被回收。
4、get(int index)
不会修改modCount,相对增删是高效的操作。
public E get(int index) { rangeCheck(index); return elementData(index); } E elementData(int index) { return (E) elementData[index]; }
由于 ArrayList 底层是基于数组实现的,所以获取元素就相当简单了,直接调用数组随机访问即可。
5、void clear()
清空数组中的数据,modCount 会修改。
public void clear() { modCount++; // clear to let GC do its work for (int i = 0; i < size; i++) elementData[i] = null; size = 0; }
6、set( int index, E element)
/** * 替换指定索引的元素 * * @param 被替换元素的索引 * @param element 即将替换到指定索引的元素 * @return 返回被替换的元素 * @throws IndexOutOfBoundsException 如果参数指定索引index>=size,抛出一个越界异常 */ public E set(int index, E element) { //检查索引是否越界。如果参数指定索引index>=size,抛出一个越界异常 rangeCheck(index); //记录被替换的元素 E oldValue = elementData(index); //替换元素 elementData[index] = element; //返回被替换的元素 return oldValue; }
参考文章:
https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html
https://blog.csdn.net/panweiwei1994/article/details/76760238