ArrayList源码分析

别说谁变了你拦得住时间么 提交于 2019-11-29 14:12:20

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!