Java ArrayList源码阅读笔记(基于JDK17)
虽然不喜欢看源码,但是据说会让人变强啊,看别的大佬的代码也许才知道怎么处理自己的一坨吧,因此冒着秃顶的风险还是来看看吧。。。
第一遍先简单看看吧,搞不清楚的地方也许会在遥远的将来再复习下吧
1、简介
其实在搞不清楚这玩意儿究竟是什么的情况下,已经稀里糊涂地用它写了好多代码了,而且每当找不到数据结构的时候,口袋里一摸就是ArrayList
,所以它到底是个啥?
ArrayList
类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,我们可以添加或删除元素
ArrayList
继承了 AbstractList
,并实现了 List
接口
以上便是比较标准的定义,大概就是一个特殊的数组,可以方便地变大变小
2、类图及类定义
ArrayList
继承自AbstractList
类,实现了List
、RandomAccess
、Cloneable
、java.io.Serializable
接口
- List接口:表明它是一个有序列表,支持添加、删除、查找等操作,并且可以通过下标进行访问
- RandomAccess接口:标记接口,表明实现这个接口的List支持快速随机访问,即直接通过索引下标访问列表的元素
- Cloneable接口:表明支持拷贝操作,支持深拷贝或浅拷贝操作
- java.io.Serializable接口:表明支持序列化操作,可以将对象转换为字节流进行持久化存储或网络传输,非常方便
3、扩容机制
这块经常会被提到,并且所谓“动态”指的也就是这个机制,是该数据结构设计的关键
3.1、构造函数
要看ArrayList
怎样进行扩容,那么自然绕不开创建类的实例
以下两个常量,其中DEFAULT_CAPACITY
表示创建ArrayList
默认的初始容量10,而EMPTY_ELEMENTDATA
表示的是一个空数组,这是一个共享的空对象数组
// 默认的初始容量 private static final int DEFAULT_CAPACITY = 10; // 共享的空数组实例 private static final Object[] EMPTY_ELEMENTDATA = {};
在ArrayList
中,有3个构造函数:
ArrayList()
ArrayList(int initialCapacity)
ArrayList(Collection<? extends E> c)
ArrayList()
是无参构造,默认会创建一个空对象数组,在第一次添加元素时会扩容到容量10
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
ArrayList(int initialCapacity)
构造函数需要指定一个初始容量,依据给定的初始容量的大小,创建不同长度的对象数组,或是抛出异常
public ArrayList(int initialCapacity) { if (initialCapacity > 0) { // 初始容量大于0 this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { // 创建空数组 this.elementData = EMPTY_ELEMENTDATA; } else { // 小于0抛出异常 throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }
ArrayList(Collection<? extends E> c)
构造函数可以指定另一个集合对象作为参数,将其转换为对象数组(存在空指针风险),按顺序进行返回
public ArrayList(Collection<? extends E> c) { Object[] a = c.toArray(); // 存在空指针风险 if ((size = a.length) != 0) { if (c.getClass() == ArrayList.class) { elementData = a; } else { elementData = Arrays.copyOf(a, size, Object[].class); } } else { // replace with empty array. elementData = EMPTY_ELEMENTDATA; } }
3.2、扩容时机
那么,到底什么时候需要扩容呢,以及怎样进行扩容呢?
接下来,借助ArrayList
的add(E e)
方法一探究竟
public boolean add(E e) { modCount++; // 修改次数加1 add(e, elementData, size); // 调用重载方法 return true; }
add(E e)
内部又调用了另一个重载方法add(E e, Object[] elementData, int s)
,是为了在字节码转化上优化,把方法进行了拆分
/** * This helper method split out from add(E) to keep method * bytecode size under 35 (the -XX:MaxInlineSize default value), * which helps when add(E) is called in a C1-compiled loop. */ private void add(E e, Object[] elementData, int s) { if (s == elementData.length) elementData = grow(); // 长度达到最大,调用grow()扩容 elementData[s] = e; // 数组索引赋值,并且size+1 size = s + 1; }
s代表size,用来记录元素的个数,当元素个数达到当前对象数组的长度的时候,就需要进行扩容了,此时会调用grow()
方法,扩容后,长度就够应付一阵子了,此时将元素填到对应位置上就可以了,结合前面默认容量为10,可知,第一次扩容将会是在增加第11个元素的时候
3.3、扩容方法
grow()
便是用来处理扩容的方法,内部又调用了另一个重载方法,将当前size + 1作为参数
private Object[] grow() { return grow(size + 1); }
oldCapacity
代表的就是旧对象数组的长度,判断其大于0也就是有元素,elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA
则是判断数组元素是否由默认无参构造器初始化
通过ArraysSupport.newLength(int oldLength, int minGrowth, int prefGrowth)
进行扩容
其中oldLength
代表的就是旧的数组容量,minGrowth
则是最少需要保证的扩容大小,没有富余,prefGrowth
则是表示推荐的扩容大小,一般会有富余oldCapacity >> 1
是一个位运算,表示0.5倍的原数组容量,那么对应的含义就是推荐扩容到原数组的1.5倍
private Object[] grow(int minCapacity) { int oldCapacity = elementData.length; // 原来数组的容量 if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, /* minimum growth */ oldCapacity >> 1 /* preferred growth */); return elementData = Arrays.copyOf(elementData, newCapacity); } else { return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; } }
newLength()
是有可能OOM的,如果成功的话就返回扩容后的长度
public static int newLength(int oldLength, int minGrowth, int prefGrowth) { // preconditions not checked because of inlining // assert oldLength >= 0 // assert minGrowth > 0 int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) { return prefLength; } else { // put code cold in a separate method return hugeLength(oldLength, minGrowth); } }
Arrays.copyOf(elementData, newCapacity)
将原数组依照新的容量进行拷贝
public static <T> T[] copyOf(T[] original, int newLength) { return (T[]) copyOf(original, newLength, original.getClass()); }
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { @SuppressWarnings("unchecked") T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; }
最终其实调用的是native
的arraycopy()
,将源数组src
从指定位置srcPos
拷贝指定长度length
到目标数组dest
的destPos
位置
最终,实现将数据从旧数组拷贝到扩容后的新数组,返回给elementData
这边应该是浅拷贝,但是引用的对象数组地址肯定变了
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
比如我们先创建ArrayList
,扔10个数据给它,此时引用的对象数组地址为728,然后再增加一个,触发扩容
新容量计算出来是15,然后创建一个地址在760的新数组,容量为15
拷贝完成后,原来的元素地址没有变,说明仅仅是拷贝了引用,内容没有变,因此拷贝效率比较高,但是elementData
的对象数组地址变了
先看这么多,如有问题和不足欢迎指正~~
https://www.runoob.com/manual/jdk11api/java.base/java/util/ArrayList.html
https://javaguide.cn/java/collection/arraylist-source-code.html
https://www.cnblogs.com/KRDecad3/p/17800902.html