【JDK源码】ArrayList 源码分析

怼烎@ 2023-10-01 11:36 116阅读 0赞

文章目录

  • ArrayList 源码分析
    • 1.简介
    • 2.属性
    • 3.构造方法
      • ArrayList(int initialCapacity)
      • ArrayList()
      • ArrayList(Collection c)
    • 4.相关操作方法
      • add(E e)
      • add(int index, E element)
      • addAll(Collection c)
      • get(int index)
      • remove(int index)
      • remove(Object o)
      • retainAll(Collection c)
      • removeAll(Collection c)
    • 5.总结

ArrayList 源码分析

1.简介

ArrayList是一种以数组实现的List,与数组相比,它具有动态扩展的能力,因此也可称之为动态数组。

\[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Be1GBvYx-1641040712338)(ArrayList 源码分析.assets/image-20211228094028357.png)\]

  1. public class ArrayList<E> extends AbstractList<E>
  2. implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
  3. }

ArrayList继承于 AbstractList ,实现了 List, RandomAccess, Cloneable, java.io.Serializable 这些接口。

实现这些接口有什么作用呢?

  • ArrayList实现了List,提供了基础的添加、删除、遍历等操作。
  • ArrayList实现了RandomAccess,提供了随机访问的能力。
  • ArrayList实现了Cloneable,可以被克隆。
  • ArrayList实现了Serializable,可以被序列化。

2.属性

  1. //默认容量,10。也就是通过new ArrayList()创建时的默认容量。
  2. private static final int DEFAULT_CAPACITY = 10;
  3. //空数组,用于空实例。这种是通过new ArrayList(0)创建时用的是这个空数组。
  4. private static final Object[] EMPTY_ELEMENTDATA = {
  5. };
  6. //1.用于默认大小空实例的共享空数组实例。通过new ArrayList()创建时用的是这个空数组
  7. //2.我们把它与EMPTY_ELEMENTDATA数组区分出来,以知道在添加第一个元素时容量需要增加多少。与EMPTY_ELEMENTDATA的区别是在添加第一个元素时使用这个空数组的会初始化为DEFAULT_CAPACITY(10)个元素。
  8. private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
  9. };
  10. //1.数组列表中存储元素的数组缓冲区。数组列表的容量是数组缓冲区的长度
  11. //2.将在添加第一个元素时,长度扩展为DEFAULT_CAPACITY。
  12. //3.使用transient是为了不序列化这个字段
  13. transient Object[] elementData;
  14. //ArrayList 所包含的元素个数,而不是elementData数组的长度。
  15. private int size;
  • DEFAULT_CAPACITY:默认容量为10,也就是通过new ArrayList()创建时的默认容量。
  • EMPTY_ELEMENTDATA:空的数组,这种是通过new ArrayList(0)创建时用的是这个空数组。
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA:也是空数组,这种是通过new ArrayList()创建时用的是这个空数组,与EMPTY_ELEMENTDATA的区别是在添加第一个元素时使用这个空数组的会初始化为DEFAULT_CAPACITY(10)个元素。
  • elementData:真正存放元素的地方,使用transient是为了不序列化这个字段。
  • size:真正存储元素的个数,而不是elementData数组的长度。

3.构造方法

ArrayList(int initialCapacity)

传入初始容量,如果大于0就初始化elementData为对应大小,如果等于0就使用EMPTY_ELEMENTDATA空数组,如果小于0抛出异常。

  1. //带初始容量的构造函数(用户可以在创建ArrayList对象时自己指定集合的初始大小)
  2. public ArrayList(int initialCapacity) {
  3. if (initialCapacity > 0) {
  4. //如果传入的初始容量大于0,创建initialCapacity大小的数组
  5. this.elementData = new Object[initialCapacity];
  6. } else if (initialCapacity == 0) {
  7. //如果传入的初始容量等于0,使用空数组EMPTY_ELEMENTDATA
  8. this.elementData = EMPTY_ELEMENTDATA;
  9. } else {
  10. //如果传入的初始容量小于0,抛出异常
  11. throw new IllegalArgumentException("Illegal Capacity: "+
  12. initialCapacity);
  13. }
  14. }

ArrayList()

不传初始容量,初始化为DEFAULTCAPACITY_EMPTY_ELEMENTDATA空数组,会在添加第一个元素的时候扩容为默认的大小,即10

  1. //默认无参构造函数
  2. public ArrayList() {
  3. //DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10
  4. this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
  5. }

ArrayList(Collection c)

传入集合并初始化elementData,这里会使用拷贝把传入集合的元素拷贝到elementData数组中,如果元素个数为0,则初始化为EMPTY_ELEMENTDATA空数组。

  1. //构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。
  2. public ArrayList(Collection<? extends E> c) {
  3. //将集合转换为数组
  4. elementData = c.toArray();
  5. //如果elementData数组的长度不为0
  6. if ((size = elementData.length) != 0) {
  7. // 检查c.toArray()返回的是不是Object[]类型
  8. if (elementData.getClass() != Object[].class)
  9. //如果不是,重新拷贝成Object[].class类型
  10. elementData = Arrays.copyOf(elementData, size, Object[].class);
  11. } else {
  12. //如果c的空集合,则初始化为空数组EMPTY_ELEMENTDATA
  13. this.elementData = EMPTY_ELEMENTDATA;
  14. }
  15. }

为什么 c.toArray()返回的有可能不是Object[]类型呢?

这里我们看一个例子:

  1. /**
  2. * @author xppll
  3. * @date 2021/12/28 10:01
  4. */
  5. public class ArrayListTest {
  6. public static void main(String[] args) {
  7. Father[] fathers = new Son[]{
  8. };
  9. //打印结果为:class [Lcom.itheima.test.Son;
  10. System.out.println(fathers.getClass());
  11. List<String> strList = new MyList();
  12. //打印结果为:class [Ljava.lang.String;
  13. System.out.println(strList.toArray().getClass());
  14. }
  15. }
  16. class Father {
  17. }
  18. class Son extends Father {
  19. }
  20. class MyList extends ArrayList<String> {
  21. /**
  22. * 子类重写父类的方法,返回值可以不一样
  23. * 但这里只能用数组类型,换成Object就不行
  24. * 这应该算是java本身的bug
  25. *
  26. * @return
  27. */
  28. @Override
  29. public String[] toArray() {
  30. // 为了方便举例直接写死
  31. return new String[]{
  32. "a", "b", "c"};
  33. }
  34. }

4.相关操作方法

add(E e)

添加元素到末尾,平均时间复杂度为O(1)。

  1. public boolean add(E e) {
  2. //检查是否需要扩容
  3. ensureCapacityInternal(size + 1);
  4. //把元素插到最后一位
  5. elementData[size++] = e;
  6. return true;
  7. }

检查是否需要扩容ensureCapacityInternal(int minCapacity)

  1. private void ensureCapacityInternal(int minCapacity) {
  2. ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
  3. }

接着看calculateCapacity(Object[] elementData, int minCapacity)

  1. private static int calculateCapacity(Object[] elementData, int minCapacity) {
  2. //如果是空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA,就初始化为默认大小10
  3. //获取“默认的容量”和“传入参数”两者之间的最大值
  4. if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
  5. return Math.max(DEFAULT_CAPACITY, minCapacity);
  6. }
  7. return minCapacity;
  8. }

接着看ensureExplicitCapacity(int minCapacity)

  1. private void ensureExplicitCapacity(int minCapacity) {
  2. modCount++;
  3. if (minCapacity - elementData.length > 0)
  4. //扩容
  5. grow(minCapacity);
  6. }

扩容方法grow(minCapacity)

  1. private void grow(int minCapacity) {
  2. //oldCapacity为旧容量,newCapacity为新容量
  3. int oldCapacity = elementData.length;
  4. //将oldCapacity 右移一位,其效果相当于oldCapacity /2,
  5. //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
  6. int newCapacity = oldCapacity + (oldCapacity >> 1);
  7. ///检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
  8. if (newCapacity - minCapacity < 0)
  9. newCapacity = minCapacity;
  10. //再检查新容量是否超出了ArrayList所定义的最大容量,
  11. //若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
  12. //如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
  13. if (newCapacity - MAX_ARRAY_SIZE > 0)
  14. newCapacity = hugeCapacity(minCapacity);
  15. // 以新容量拷贝出来一个新数组
  16. elementData = Arrays.copyOf(elementData, newCapacity);
  17. }
  18. //使用最大容量
  19. private static int hugeCapacity(int minCapacity) {
  20. if (minCapacity < 0) // overflow
  21. throw new OutOfMemoryError();
  22. return (minCapacity > MAX_ARRAY_SIZE) ?
  23. Integer.MAX_VALUE :
  24. MAX_ARRAY_SIZE;
  25. }

扩容的整个过程:

  1. 检查是否需要扩容
  2. 如果elementData等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA则初始化容量大小为DEFAULT_CAPACITY
  3. 新容量是老容量的1.5倍(oldCapacity + (oldCapacity >> 1)),如果加了这么多容量发现比需要的容量还小,则以需要的容量为准
  4. 创建新容量的数组并把老数组拷贝到新数组

add(int index, E element)

添加元素到指定位置,平均时间复杂度为O(n)。

  1. /**
  2. * 添加元素到指定位置,平均时间复杂度为O(n)
  3. *
  4. * @param index 指定元素插入的位置
  5. * @param element 要插入的元素
  6. */
  7. public void add(int index, E element) {
  8. // 检查是否越界
  9. rangeCheckForAdd(index);
  10. // 检查是否需要扩容
  11. ensureCapacityInternal(size + 1); // Increments modCount!!
  12. // 将index及其以后的元素都往后移一位,此时inex位置就空出来了
  13. System.arraycopy(elementData, index, elementData, index + 1,
  14. size - index);
  15. //将元素插入到index位置
  16. elementData[index] = element;
  17. //元素数量加一
  18. size++;
  19. }
  20. //检查是否越界
  21. private void rangeCheckForAdd(int index) {
  22. if (index > size || index < 0)
  23. throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
  24. }

整个流程:

  1. 检查索引是否越界
  2. 检查是否需要扩容
  3. 把插入索引位置后的元素都往后挪一位
  4. 在插入索引位置放置插入的元素
  5. 元素数量加1

addAll(Collection c)

求两个集合的并集。

  1. /**
  2. * 将集合c中所有元素添加到当前ArrayList中
  3. *
  4. * @param c
  5. * @return
  6. */
  7. public boolean addAll(Collection<? extends E> c) {
  8. //将集合c转为数组
  9. Object[] a = c.toArray();
  10. int numNew = a.length;
  11. //检查是否需要扩容
  12. ensureCapacityInternal(size + numNew); // Increments modCount
  13. //将c中的元素全部拷贝到数组的最后
  14. System.arraycopy(a, 0, elementData, size, numNew);
  15. //集合中元素的大小增加c的大小
  16. size += numNew;
  17. //如果c不为空就返回true,否则返回false
  18. return numNew != 0;
  19. }

整个流程:

  1. 拷贝c中的元素到数组a中
  2. 检查是否需要扩容
  3. 把数组a中的元素拷贝到elementData的尾部
  4. 元素数量加c的大小

get(int index)

获取指定索引位置的元素,时间复杂度为O(1)。

  1. public E get(int index) {
  2. //检查是否越界
  3. rangeCheck(index);
  4. //返回数组index位置的元素
  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. @SuppressWarnings("unchecked")
  14. E elementData(int index) {
  15. return (E) elementData[index];
  16. }

整个流程:

  1. 检查索引是否越界,这里只检查是否越上界,如果越上界抛出IndexOutOfBoundsException异常,如果越下界抛出的是ArrayIndexOutOfBoundsException异常
  2. 返回索引位置处的元素

remove(int index)

删除指定索引位置的元素,时间复杂度为O(n)。

  1. /**
  2. * 删除指定索引位置的元素,时间复杂度为O(n)。
  3. * @param index 指定索引位置
  4. * @return
  5. */
  6. public E remove(int index) {
  7. // 检查是否越界
  8. rangeCheck(index);
  9. modCount++;
  10. //获取index位置的元素
  11. E oldValue = elementData(index);
  12. //如果index不是最后一位,则将index之后的元素往前移一位
  13. int numMoved = size - index - 1;
  14. if (numMoved > 0)
  15. System.arraycopy(elementData, index + 1, elementData, index,
  16. numMoved);
  17. //将最后一个元素删除,帮助GC
  18. elementData[--size] = null; // clear to let GC do its work
  19. //返回删除的值
  20. return oldValue;
  21. }

整个流程:

  1. 检查索引是否越界
  2. 获取指定索引位置的元素
  3. 如果删除的不是最后一位,则其它元素往前移一位
  4. 将最后一位置为null,方便GC回收
  5. 返回删除的元素

可以看到,ArrayList删除元素的时候并没有缩容。

remove(Object o)

删除指定元素值的元素,时间复杂度为O(n)。

  1. /**
  2. * 删除指定元素值的元素,时间复杂度为O(n)。
  3. * @param o
  4. * @return
  5. */
  6. public boolean remove(Object o) {
  7. if (o == null) {
  8. //遍历整个数组,找到元素第一次出现的位置,并将其快速删除
  9. for (int index = 0; index < size; index++)
  10. //如果要删除的元素为null,则以null进行比较,使用==
  11. if (elementData[index] == null) {
  12. fastRemove(index);
  13. return true;
  14. }
  15. } else {
  16. //遍历整个数组,找到元素第一次出现的位置,并将其快速删除
  17. for (int index = 0; index < size; index++)
  18. //如果要删除的元素不为null,则进行比较,使用equals()方法
  19. if (o.equals(elementData[index])) {
  20. fastRemove(index);
  21. return true;
  22. }
  23. }
  24. return false;
  25. }
  26. /**
  27. * 专用的remove方法,跳过边界检查,并且不返回删除的值。
  28. *
  29. * @param index
  30. */
  31. private void fastRemove(int index) {
  32. //少了一个越界的检查
  33. modCount++;
  34. //如果index不是最后一位,则将index之后的元素往前移一位
  35. int numMoved = size - index - 1;
  36. if (numMoved > 0)
  37. System.arraycopy(elementData, index + 1, elementData, index,
  38. numMoved);
  39. //将最后一位元素删除,帮助GC
  40. elementData[--size] = null; // clear to let GC do its work
  41. }

整个流程:

  1. 找到第一个等于指定元素值的元素
  2. 快速删除

fastRemove(int index)相对于remove(int index)少了检查索引越界的操作,可见jdk将性能优化到极致。

retainAll(Collection c)

求两个集合的交集。

  1. /**
  2. * 批量删除元素
  3. * complement为true表示删除c中不包含的元素
  4. * complement为false表示删除c中包含的元素
  5. *
  6. * @param c
  7. * @param complement
  8. * @return
  9. */
  10. private boolean batchRemove(Collection<?> c, boolean complement) {
  11. final Object[] elementData = this.elementData;
  12. //使用读写两个指针同时遍历数组
  13. //读指针每次自增1,写指针放入元素的时候才加一
  14. //这样不需要额外的空间,只需要在原有的数组上操作就可以了
  15. int r = 0, w = 0;
  16. boolean modified = false;
  17. try {
  18. //遍历整个数组,如果c中包含该元素,则把该元素放到写指针的位置(以complement为准)
  19. for (; r < size; r++)
  20. if (c.contains(elementData[r]) == complement)
  21. elementData[w++] = elementData[r];
  22. } finally {
  23. //正常来说r最后是等于size的,除非c.contains()抛出了异常
  24. if (r != size) {
  25. // 如果c.contains()抛出了异常,则把未读的元素都拷贝到写指针之后
  26. System.arraycopy(elementData, r,
  27. elementData, w,
  28. size - r);
  29. w += size - r;
  30. }
  31. if (w != size) {
  32. // 将写指针之后的元素置为空,帮助GC
  33. for (int i = w; i < size; i++)
  34. elementData[i] = null;
  35. modCount += size - w;
  36. // 新大小等于写指针的位置(因为每写一次写指针就加1,所以新大小正好等于写指针的位置)
  37. size = w;
  38. modified = true;
  39. }
  40. }
  41. // 有修改返回true
  42. return modified;
  43. }

整个流程:

  1. 遍历elementData数组
  2. 如果元素在c中,则把这个元素添加到elementData数组的w位置并将w位置往后移一位
  3. 遍历完之后,w之前的元素都是两者共有的,w之后(包含)的元素不是两者共有的
  4. 将w之后(包含)的元素置为null,方便GC回收;

removeAll(Collection c)

求两个集合的单方向差集,只保留当前集合中不在c中的元素,不保留在c中不在当前集体中的元素。

  1. public boolean removeAll(Collection<?> c) {
  2. //集合c不能为空
  3. Objects.requireNonNull(c);
  4. //同样调用批量删除方法,这时complement传入false,表示删除包含在c中的元素
  5. return batchRemove(c, false);
  6. }

retainAll(Collection c)方法类似,只是这里保留的是不在c中的元素。

5.总结

  1. ArrayList内部使用数组存储元素,当数组长度不够时进行扩容,每次加一半的空间,ArrayList不会进行缩容
  2. ArrayList支持随机访问,通过索引访问元素极快,时间复杂度为O(1)
  3. ArrayList添加元素到尾部极快,平均时间复杂度为O(1)
  4. ArrayList添加元素到中间比较慢,因为要搬移元素,平均时间复杂度为O(n)
  5. ArrayList从尾部删除元素极快,时间复杂度为O(1)
  6. ArrayList从中间删除元素比较慢,因为要搬移元素,平均时间复杂度为O(n)
  7. ArrayList支持求并集,调用addAll(Collection c)方法即可
  8. ArrayList支持求交集,调用retainAll(Collection c)方法即可
  9. ArrayList支持求单向差集,调用removeAll(Collection c)方法即可

在这里插入图片描述

发表评论

表情:
评论列表 (有 0 条评论,116人围观)

还没有评论,来说两句吧...

相关阅读

    相关 JDK分析-ArrayList

    继承结构 我们都知道数组定义了长度就不可以改变了,而List其实就是可延长的数组,内部就是采用数组结构来实现的,具体怎么实现的,我们往下来看源码,首先是ArrayList