• 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日,下午。

  • 相关阅读:
    Tensorboard返回的网址打不开问题
    css的常用知识点
    js的基础知识
    html的常用标签
    python的进程与线程
    python的socket的学习
    python的异常处理
    python类的相关知识第二部分
    python类的相关知识第一部分
    python装饰器的学习笔记
  • 原文地址:https://www.cnblogs.com/developer_chan/p/11496431.html
Copyright © 2020-2023  润新知