ArrayList 源码阅读
一、概述
在实际工作中我们使用最多的集合恐怕就是 ArrayList 了,但是这个集合类该怎么用呢,借此我们通过阅读它的源码来一探究竟
二、ArrayList 成员变量介绍
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{ // 8683452581122892189L 唯一的序列化身份标识,主要用于程序的版本控制,如果不显示定义,Jvm 在编译时也会自动生成一个唯一的 serialVersionUID,为了提高工作效率,实际工作中应显示定义 private static final long serialVersionUID = 8683452581122892189L; // 初始化数组容量 private static final int DEFAULT_CAPACITY = 10 ; // 默认大小的空实例数组,在第一次调用 ensureCapacityInternal 时会初始化长度为 10 private static final Object[] EMPTY_ELEMENTDATA = {}; // 默认大小的空实例数组,上面已经有了一个空实例数组 EMPTY_ELEMENTDATA,之所以再多定义一个的目的是为了将其与 EMPTY_ELEMENTDATA 区分开来,以了解添加第一个元素时要扩容多少 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // transient: 字面意思翻译为短暂的,转瞬即逝,使用此关键字修饰后的成员变量不能被序列化;注意使用 static 修饰的成员变量也是天然不能被序列化的(因为 static 是描述类的,序列化是描述实例对象的) // elementData: 存放元素的数组 transient Object[] elementData; // 当前数组元素的个数 private int size; // modCount 是用来记录数组的结构变化次数(添加、删除的时候均会做 modCount++ 操作),ArrayList 不是线程安全的,如果有多个线程同时操作一个共享的 ArrayList // 可能会导致异常,因此 ArrayList 采用了快速失败的机制,通过记录 modCount 参数来实现,在面对并发修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险 protected transient int modCount = 0 ; // Hotspot 虚拟机中数组的对象头需要使用 4 个字节保存长度,以及 4 个填充字节 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 ; } |
例如下面这个数组
数组实例对象为 elementData,数组的容量大小为 10,数组中元素个数为 7,需要注意的是数组的起始索引位置是 0
三、构造方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | // List list = new ArrayList(); public ArrayList() { this .elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 创建对象时如果没有显示指定容量,那么使用默认的空数组 } // List list = new ArrayList(initialCapacity); public ArrayList( int initialCapacity) { if (initialCapacity > 0 ) { this .elementData = new Object[initialCapacity]; // 如果传入的初始化容量大于 0 则创建一个容量为 initialCapacity 的新数组,并把该新数组赋值给 elementData } else if (initialCapacity == 0 ) { this .elementData = EMPTY_ELEMENTDATA; // 如果传入的初始化容量等于 0 则使用空数组实例对象 } else { throw new IllegalArgumentException( "Illegal Capacity: " + initialCapacity); // 如果传入的参数小于 0,则抛出异常 } } // List list = new ArrayList(HashSet hashSet) public ArrayList(Collection<? extends E> c) { Object[] a = c.toArray(); // 将传入的集合对象转成 Object 类型数组 if ((size = a.length) != 0 ) { // 将数组的容量赋值给 size,如果数组不是空数组 if (c.getClass() == ArrayList. class ) { // 判断传入的集合类型是否是 ArrayList 类型,如果是直接赋值即可,不需要任何转换 elementData = a; } else { elementData = Arrays.copyOf(a, size, Object[]. class ); // 如果不是 ArrayList 类型,那么需要创建一个新的 Object 类型数组,初始化容量为 size,并且把数组 a 中的元素复制到新的 Object 类型数组中 } } else { elementData = EMPTY_ELEMENTDATA; // 如果集合 c 中原先没有任何元素,直接返回一个空的 Object 类型的数组 } } 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); // 创建一个传入参数类型的数组,容量为 newLength // original: 源数组 // 第一个 0: 从源数组索引位置 0 开始 // copy: 目标数组 // 第二个 0: 目标数组的 0 开始 // Math.min(original.length, newLength): 需要复制的元素个数 // 例如有两个数组 original = [1,3,5,7,9] copy = [2,4,6,8,10,12] // 调用 System.arraycopy(original, 1, copy, 2,3) 之后, 它会将源数组(original)从索引位置为 1 开始,复制 3 个元素(即 3,5,7) 到新的数组(copy)从索引位置为 2 的地方开始(注意: 数组的长度是不可变的,这里是覆盖,而不是增加) // 操作完成之后源数组不会发生改变,目标数组会有 3 个元素被覆盖(3,5,7),两个数组最终如下: original = [1,3,5,7,9] copy = [2,4,3,5,7,12],源数组不会改变 System.arraycopy(original, 0 , copy, 0 ,Math.min(original.length, newLength)); return copy; } |
ArrayList 有三个构造方法
1、如果创建对象时不显示指定容量,那么会创建一个默认长度为 10 的数组
2、如果创建对象时显示指定了容量,那么会根据显示指定的初始化容量创建对应长度的数组
3、以其它集合对象作为原材料创建数组,数组长度为其它集合的 size 大小
下面就以第三种构造方法为例
示例代码
1 2 3 4 5 6 7 8 9 10 | public class ArrayListSourceRead { public static void main(String[] args) { Set<String> hashSet = new HashSet<>(); hashSet.add( "A" ); hashSet.add( "B" ); hashSet.add( "C" ); List<String> list = new ArrayList<>(hashSet); } } |
四、查询、修改方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // 根据指定索引位置获取数组元素 public E get( int index) { rangeCheck(index); // 校验索引下标是否越界 return elementData(index); // 根据数组的索引下标直接获取元素 } // 将指定索引位置的元素修改为 element public E set( int index, E element) { rangeCheck(index); // 校验索引下标是否越界 E oldValue = elementData(index); // 获取 index 位置的旧值 elementData[index] = element; // 使用新值替换旧值 return oldValue; // 将旧值返回 } // 调用 get 和 set 方法时都需要调用此方法来判断数组下标是否合法 private void rangeCheck( int index) { if (index >= size) // 正常情况下 index 的最大值是 size - 1 throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } |
从上面可以看出查询和修改方法都是基于索引下标直接操作的,性能比较好
五、添加方法
添加元素有 4 个方法,add(E e)、add(int index,E element)、addALL(Collection<? extends E> c)、addAll(int index, Collection<? extends E> c),前两个方法是添加单个元素,后两个是将传入的整个集合都添加进去
上述所有的添加方法都需要调用 ensureCapacityInternal(int minCapacity) 进行扩容判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | public boolean add(E e) { ensureCapacityInternal(size + 1 ); // 扩容判断 elementData[size++] = e; // 在数组的尾部添加元素 e,添加完成之后执行 size++ 操作 return true ; // 添加成功之后返回 true } public void add( int index, E element) { rangeCheckForAdd(index); // 判断数组下标是否越界 ensureCapacityInternal(size + 1 ); // 扩容判断 System.arraycopy(elementData, index, elementData, index + 1 ,size - index); // 将指定索引位置及之后的元素向后移动一位 elementData[index] = element; // 指定索引位置的元素使用新传入的值 element 进行覆盖 size++; // 添加成功之后,数组中元素的个数加 1 } public boolean addAll(Collection<? extends E> c) { Object[] a = c.toArray(); // 将传入的集合转成 Object 类型数组 int numNew = a.length; // 计算需要操作的元素个数 ensureCapacityInternal(size + numNew); // 扩容判断 System.arraycopy(a, 0 , elementData, size, numNew); // 将集合转成的数组 a 中的元素复制到 elementData 的指定位置 size += numNew; // 操作完成之后数组元素增加 numNew return numNew != 0 ; // 返回添加成功的元素数量 } public boolean addAll( int index, Collection<? extends E> c) { rangeCheckForAdd(index); // 校验索引是否越界 Object[] a = c.toArray(); // 将原集合转成 Object 类型数组,并添加对应元素 int numNew = a.length; // 计算需要操作的元素个数 ensureCapacityInternal(size + numNew); // 扩容判断 int numMoved = size - index; // 计算需要移动的元素个数 if (numMoved > 0 ) System.arraycopy(elementData, index, elementData, index + numNew,numMoved); // 移动指定索引及其后面的元素 System.arraycopy(a, 0 , elementData, index, numNew); // 将集合转成的数组 a 中的元素复制到 elementData 的指定位置 size += numNew; // 操作完成之后数组元素增加 numNew return numNew != 0 ; // 返回添加成功的元素数量 } // 确保数组内部容量可用 private void ensureCapacityInternal( int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } // 计算数组最小需要的容量 private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); // 如果创建 ArrayList 对象时未指定容量,使用默认容量 10 进行初始化数组 } return minCapacity; // 如果显示传入了最小容量,直接返回 } // 确保显示容量 private void ensureExplicitCapacity( int minCapacity) { modCount++; // 该变量用于记录数组结构的变化次数(调用 添加、移除 方法均会做加 1 操作) if (minCapacity - elementData.length > 0 ) grow(minCapacity); // 如果计算出来的数组最小需要的容量大于数组实际的容量,则需要进行扩容操作 } // 扩容逻辑 private void grow( int minCapacity) { int oldCapacity = elementData.length; // 老容量 int newCapacity = oldCapacity + (oldCapacity >> 1 ); // 新容量 = 老容量 + 老容量 / 2 (例如老容量为 5,扩容后的新容量是 5 + 5/2 = 7,老容量为 10,扩容后的新容量是 10 + 10/2 = 15 if (newCapacity - minCapacity < 0 ) newCapacity = minCapacity; // 如果扩容之后的新容量还是小于数据实际需要的最小容量,则新容量为数组最小需要的容量 if (newCapacity - MAX_ARRAY_SIZE > 0 ) newCapacity = hugeCapacity(minCapacity); // 如果新容量 > Integer.MAX_VALUE - 8,则调用 hugeCapacity() 方法来设置合适的容量 elementData = Arrays.copyOf(elementData, newCapacity); // 将原数组中的元素复制到扩容后的新数组中,数组元素的索引保持不变 } // 大容量设置 private static int hugeCapacity( int minCapacity) { if (minCapacity < 0 ) throw new OutOfMemoryError(); // 数组需要的最小容量不可能小于 0,如果是则抛出异常 return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; // 如果 minCapacity > MAX_ARRAY_SIZE 则设置数组容量为 MAX_VALUE,否则设置为 Integer.MAX_VALUE - 8 } // 复制数组元素 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; } |
示例代码 1
1 2 3 4 5 6 7 | public class ArrayListSourceRead { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add( "A" ); list.add( "B" ); } } |
示例代码 2
1 2 3 4 5 6 7 8 9 | public class ArrayListSourceRead { public static void main(String[] args) { List<String> list = new ArrayList<>( 3 ); list.add( "A" ); list.add( "B" ); list.add( "C" ); list.add( 1 , "Y" ); } } |
示例代码 3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class ArrayListSourceRead { public static void main(String[] args) { Set<String> hashSet = new HashSet<>(); hashSet.add( "a" ); hashSet.add( "b" ); hashSet.add( "c" ); hashSet.add( "d" ); hashSet.add( "e" ); List<String> list = new ArrayList<>( 3 ); list.add( "A" ); list.add( "B" ); list.add( "C" ); list.addAll( 1 , hashSet); } } |
六、删除方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | public void clear() { modCount++; // 循环遍历数组,将每个元素都置为 null,等待垃圾回收器进行回收 for ( int i = 0 ; i < size; i++) elementData[i] = null ; size = 0 ; } public E remove( int index) { rangeCheck(index); // 校验索引下标是否越界 modCount++; E oldValue = elementData(index); // 根据指定索引获取该位置的旧值 int numMoved = size - index - 1 ; // 计算出要移动的元素的个数 if (numMoved > 0 ) System.arraycopy(elementData, index+ 1 , elementData, index,numMoved); elementData[--size] = null ; // 将 index = size -1 处的元素置空,等待垃圾回收器进行回收 return oldValue; // 返回被移除的旧值 } public boolean remove(Object o) { if (o == null ) { // 当指定要移除的元素是 null for ( int index = 0 ; index < size; index++) if (elementData[index] == null ) { fastRemove(index); return true ; } } else { // 当指定要移除的元素不为 null for ( int index = 0 ; index < size; index++) if (o.equals(elementData[index])) { // 循环遍历数组 elementData,找出符合条件的元素 fastRemove(index); // 根据指定元素在数组中的索引位置进行 fastRemove 操作 return true ; } } return false ; } private void fastRemove( int index) { modCount++; int numMoved = size - index - 1 ; // 计算出要移动的元素的个数 if (numMoved > 0 ) System.arraycopy(elementData, index+ 1 , elementData, index,numMoved); // 将指定元素所在的索引后面的元素向前移动 1 位 elementData[--size] = null ; // 将 index = size -1 处的元素置空,等待垃圾回收器进行回收 } |
示例代码
1 2 3 4 5 6 7 8 9 10 11 | public class ArrayListSourceRead { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add( "A" ); list.add( "B" ); list.add( "C" ); list.add( "D" ); list.remove( "B" ); } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?