ArrayList源码分析
前言:作为一个常用的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日,下午。