前言:作为一个常用的List接口实现类,日常开发过程中使用率非常高,因此有必要对其原理进行分析。
注:本文jdk源码版本为jdk1.8.0_172
1.ArrayList介绍
ArrayList底层数据结构是数组(数组是一组连续的内存空间),默认容量为10,它具有动态扩容的能力,线程不安全,元素可以为null。
笔者在一次使用ArrayList的时候引起了一次线上OOM,分析传送门:记一次ArrayList产生的线上OOM问题
1 java.lang.Object
2 ↳ java.util.AbstractCollection<E>
3 ↳ java.util.AbstractList<E>
4 ↳ java.util.ArrayList<E>
5
6 public class ArrayList<E> extends AbstractList<E>
7 implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
2.主要源码分析
add(e):
1 public boolean add(E e) {
2 // 确认容量
3 ensureCapacityInternal(size + 1); // Increments modCount!!
4 // 直接将元素添加在数组中
5 elementData[size++] = e;
6 return true;
7 }
8
9 private void ensureCapacityInternal(int minCapacity) {
10 // 进一步确认ArrayList的容量,看是否需要进行扩容
11 ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
12 }
13
14 private static int calculateCapacity(Object[] elementData, int minCapacity) {
15 // 如果elementData为空,则返回默认容量和minCapacity中的最大值
16 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
17 return Math.max(DEFAULT_CAPACITY, minCapacity);
18 }
19 // 否则直接返回minCapacity
20 return minCapacity;
21 }
22
23 private void ensureExplicitCapacity(int minCapacity) {
24 // 修改次数自增
25 modCount++;
26
27 // overflow-conscious code
28 // 判断是否需要扩容
29 if (minCapacity - elementData.length > 0)
30 grow(minCapacity);
31 }
32
33 private void grow(int minCapacity) {
34 // overflow-conscious code
35 // 原容量
36 int oldCapacity = elementData.length;
37 // 扩容,相当于扩大为原来的1.5倍
38 int newCapacity = oldCapacity + (oldCapacity >> 1);
39 // 确认最终容量
40 if (newCapacity - minCapacity < 0)
41 newCapacity = minCapacity;
42 if (newCapacity - MAX_ARRAY_SIZE > 0)
43 newCapacity = hugeCapacity(minCapacity);
44 // minCapacity is usually close to size, so this is a win:
45 // 将旧数据拷贝到新数组中
46 elementData = Arrays.copyOf(elementData, newCapacity);
47 }
48
49
分析:
其实add方法整体逻辑还是比较简单。主要注意扩容条件:只要插入数据size比原来大就会进行扩容。因此如果在循环中使用ArrayList时需要特别小心,避免频繁扩容造成OOM异常。
add(int index, E element):
1 public void add(int index, E element) {
2 // 越界检查
3 rangeCheckForAdd(index);
4
5 // 确认容量
6 ensureCapacityInternal(size + 1); // Increments modCount!!
7 // 将index及其之后的元素往后移动一位,将index位置空出来
8 System.arraycopy(elementData, index, elementData, index + 1,
9 size - index);
10 // 在index插入元素
11 elementData[index] = element;
12 // 元素个数自增
13 size++;
14 }
分析:
整体逻辑简单:越界检查->确认容量->元素后移->插入元素。
get函数:
1 public E get(int index) {
2 // 越界检查
3 rangeCheck(index);
4 // 获取对应位置上的数据
5 return elementData(index);
6 }
7
8 private void rangeCheck(int index) {
9 if (index >= size)
10 throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
11 }
12
13 E elementData(int index) {
14 return (E) elementData[index];
15 }
分析:
get操作简单,理解容易。
remove(index):
1 public E remove(int index) {
2 // 越界检查
3 rangeCheck(index);
4
5 // 修改次数自增
6 modCount++;
7 // 获取对应index上的元素
8 E oldValue = elementData(index);
9
10 // 判断index是否在最后一个位置
11 int numMoved = size - index - 1;
12 // 如果不是,则需要将index之后的元素往前移动一位
13 if (numMoved > 0)
14 System.arraycopy(elementData, index+1, elementData, index,
15 numMoved);
16 // 将最后一个元素删除,帮助GC
17 elementData[--size] = null; // clear to let GC do its work
18
19 return oldValue;
20 }
分析:
remove逻辑还是比较简单,但是这里需要注意一点是ArrayList在remove的时候,并没有进行缩容。
remove(o):
1 public boolean remove(Object o) {
2 // 如果被移除元素为null
3 if (o == null) {
4 // 循环遍历
5 for (int index = 0; index < size; index++)
6 // 注意这里判断null是用的“==”
7 if (elementData[index] == null) {
8 // 快速remove元素
9 fastRemove(index);
10 return true;
11 }
12 } else {
13 for (int index = 0; index < size; index++)
14 // 这里判断相等是用的equals方法,注意和上面对比
15 if (o.equals(elementData[index])) {
16 fastRemove(index);
17 return true;
18 }
19 }
20 return false;
21 }
22
23 private void fastRemove(int index) {
24 // 注意这里并未做越界检查,毕竟叫fastRemove
25 // 修改次数自增
26 modCount++;
27 // 判断是否是最后一个元素,这里的操作和remove(index)是一样的
28 int numMoved = size - index - 1;
29 if (numMoved > 0)
30 System.arraycopy(elementData, index+1, elementData, index,
31 numMoved);
32 elementData[--size] = null; // clear to let GC do its work
33 }
分析:
remove元素的时候分为null和非null,并且是快速remove,并未做越界检查。
retainAll:求交集
1 public boolean retainAll(Collection<?> c) {
2 // 判空
3 Objects.requireNonNull(c);
4 // 批量remove complement为true表示保存包含在c集合的元素,这样就求出交集了
5 return batchRemove(c, true);
6 }
7
8 private boolean batchRemove(Collection<?> c, boolean complement) {
9 final Object[] elementData = this.elementData;
10 // 读写指针 读指针遍历,写指针只有在条件符合时才自增,这样不需要额外的空间
11 int r = 0, w = 0;
12 boolean modified = false;
13 try {
14 // 遍历
15 for (; r < size; r++)
16 // 如果c集合中包含遍历元素,则把元素放入写指针位置(以complement为准)
17 if (c.contains(elementData[r]) == complement)
18 elementData[w++] = elementData[r];
19 } finally {
20 // Preserve behavioral compatibility with AbstractCollection,
21 // even if c.contains() throws.
22 // 正常情况下,r与size是相等的,这里是对异常的判断
23 if (r != size) {
24 // 将未读的元素拷贝到写指针后面
25 System.arraycopy(elementData, r,
26 elementData, w,
27 size - r);
28 w += size - r;
29 }
30 // 将写指针后的元素全部置空
31 if (w != size) {
32 // clear to let GC do its work
33 for (int i = w; i < size; i++)
34 elementData[i] = null;
35 modCount += size - w;
36 size = w;
37 modified = true;
38 }
39 }
40 return modified;
41 }
分析:
将集合与另一个集合求交集,整体逻辑比较简单的。通过读写指针进行操作,不用额外空间。注意complement为true,则将包含在c中的元素写入相应位置。这样就求出了交集,这里还要注意finally中的操作,异常与置空操作。
removeAll:求差集,但是这里只保留当前集合不在C中的元素,不保留C中不在当前集合中的元素。
1 public boolean removeAll(Collection<?> c) {
2 // 判空
3 Objects.requireNonNull(c);
4 // 批量remove,注意这里complement为false,表示保存不在c中的元素,这样就求出差集了
5 return batchRemove(c, false);
6 }
分析:
逻辑和retainAll刚好相反,complement为false,保存不包含在C中的元素,这样就求出差集了,注意这里是单向差集。
3.总结
以上分析了ArrayList的主要源码,下面对其进行总结:
#1.ArrayList的底层数据结构为数组(数组是一组连续的内存空间),默认容量为10,线程不安全,可以存储null值。
#2.ArrayList扩容条件,只要增加容量大于现有容量就会进行扩容,扩容量为原来的1.5倍,但是ArrayList不会进行缩容。
#3.ArrayList中有求交集(retainAll)和求差集(removeAll),注意这里的差集是单向交集。
by Shawn Chen,2019.09.14日,下午。