深度解析 ArrayList:揭开源码背后的设计与实现原理

一、ArrayList 简介

ArrayList 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。

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


public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}
  • List : 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。

  • RandomAccess这是一个标志接口,表明实现这个接口的 List 集合是支持 快速随机访问 的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。

  • Cloneable表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。

  • Serializable : 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。

ArrayList 类图

1、ArrayList 和 Vector 的区别?(了解即可)

  • ArrayListList 的主要实现类,底层使用 Object[]存储,适用于频繁的查找工作,线程不安全 。

  • VectorList 的古老实现类,底层使用Object[] 存储,线程安全。

2、ArrayList 可以添加 null 值吗?

ArrayList 中可以存储任何类型的对象,包括 null 值。不过,不建议向ArrayList 中添加 null 值, null 值无意义,会让代码难以维护比如忘记做判空处理就会导致空指针异常。

示例代码:

    public static void main(String[] args) {ArrayList<String> listOfStrings = new ArrayList<>();listOfStrings.add(null);listOfStrings.add("java");System.out.println(listOfStrings);}

输出:

[null, java]

3、ArrayList 与 LinkedList 区别?

  • 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;

  • 底层数据结构: ArrayList 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)

  • 插入和删除是否受元素位置的影响:

    • ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element)),时间复杂度就为 O(n)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。

    • LinkedList 采用链表存储,所以在头尾插入或者删除元素不受元素位置的影响(add(E e)、addFirst(E e)、addLast(E e)、removeFirst()、 removeLast()),时间复杂度为 O(1),如果是要在指定位置 i 插入和删除元素的话(add(int index, E element),remove(Object o),remove(int index)), 时间复杂度为 O(n) ,因为需要先移动到指定位置再插入和删除。

  • 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList(实现了 RandomAccess 接口) 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。

  • 内存空间占用: ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

二、ArrayList 核心源码解读

这里以 JDK1.8 为例,分析一下 ArrayList 的底层源码。

public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable {private static final long serialVersionUID = 8683452581122892189L;/*** 默认初始容量大小*/private static final int DEFAULT_CAPACITY = 10;/*** 空数组(用于空实例)。*/private static final Object[] EMPTY_ELEMENTDATA = {};//用于默认大小空实例的共享空数组实例。//我们把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};/*** 保存ArrayList数据的数组*/transient Object[] elementData; // non-private to simplify nested class access/*** ArrayList 所包含的元素个数*/private int size;/*** 带初始容量参数的构造函数(用户可以在创建ArrayList对象时自己指定集合的初始大小)*/public ArrayList(int initialCapacity) {if (initialCapacity > 0) {//如果传入的参数大于0,创建initialCapacity大小的数组this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {//如果传入的参数等于0,创建空数组this.elementData = EMPTY_ELEMENTDATA;} else {//其他情况,抛出异常throw new IllegalArgumentException("Illegal Capacity: " +initialCapacity);}}/*** 默认无参构造函数* DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10*/public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}/*** 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。*/public ArrayList(Collection<? extends E> c) {//将指定集合转换为数组elementData = c.toArray();//如果elementData数组的长度不为0if ((size = elementData.length) != 0) {// 如果elementData不是Object类型数据(c.toArray可能返回的不是Object类型的数组所以加上下面的语句用于判断)if (elementData.getClass() != Object[].class)//将原来不是Object类型的elementData数组的内容,赋值给新的Object类型的elementData数组elementData = Arrays.copyOf(elementData, size, Object[].class);} else {// 其他情况,用空数组代替this.elementData = EMPTY_ELEMENTDATA;}}/*** 修改这个ArrayList实例的容量是列表的当前大小。 应用程序可以使用此操作来最小化ArrayList实例的存储。*/public void trimToSize() {modCount++;if (size < elementData.length) {elementData = (size == 0)? EMPTY_ELEMENTDATA: Arrays.copyOf(elementData, size);}}
//下面是ArrayList的扩容机制
//ArrayList的扩容机制提高了性能,如果每次只扩充一个,
//那么频繁的插入会导致频繁的拷贝,降低性能,而ArrayList的扩容机制避免了这种情况。/*** 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量** @param minCapacity 所需的最小容量*/public void ensureCapacity(int minCapacity) {//如果是true,minExpand的值为0,如果是false,minExpand的值为10int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)// any size if not default element table? 0// larger than default for default empty table. It's already// supposed to be at default size.: DEFAULT_CAPACITY;//如果最小容量大于已有的最大容量if (minCapacity > minExpand) {ensureExplicitCapacity(minCapacity);}}// 根据给定的最小容量和当前数组元素来计算所需容量。private static int calculateCapacity(Object[] elementData, int minCapacity) {// 如果当前数组元素为空数组(初始情况),返回默认容量和最小容量中的较大值作为所需容量if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}// 否则直接返回最小容量return minCapacity;}// 确保内部容量达到指定的最小容量。private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}//判断是否需要扩容private void ensureExplicitCapacity(int minCapacity) {modCount++;// overflow-conscious codeif (minCapacity - elementData.length > 0)//调用grow方法进行扩容,调用此方法代表已经开始扩容了grow(minCapacity);}/*** 要分配的最大数组大小*/private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;/*** ArrayList扩容的核心方法。*/private void grow(int minCapacity) {// oldCapacity为旧容量,newCapacity为新容量int oldCapacity = elementData.length;//将oldCapacity 右移一位,其效果相当于oldCapacity /2,//我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,int newCapacity = oldCapacity + (oldCapacity >> 1);//然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,if (newCapacity - minCapacity < 0)newCapacity = minCapacity;//再检查新容量是否超出了ArrayList所定义的最大容量,//若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,//如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win:elementData = Arrays.copyOf(elementData, newCapacity);}//比较minCapacity和 MAX_ARRAY_SIZEprivate static int hugeCapacity(int minCapacity) {if (minCapacity < 0) // overflowthrow new OutOfMemoryError();return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;}/*** 返回此列表中的元素数。*/public int size() {return size;}/*** 如果此列表不包含元素,则返回 true 。*/public boolean isEmpty() {//注意=和==的区别return size == 0;}/*** 如果此列表包含指定的元素,则返回true 。*/public boolean contains(Object o) {//indexOf()方法:返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1return indexOf(o) >= 0;}/*** 返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1*/public int indexOf(Object o) {if (o == null) {for (int i = 0; i < size; i++)if (elementData[i] == null)return i;} else {for (int i = 0; i < size; i++)//equals()方法比较if (o.equals(elementData[i]))return i;}return -1;}/*** 返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1。.*/public int lastIndexOf(Object o) {if (o == null) {for (int i = size - 1; i >= 0; i--)if (elementData[i] == null)return i;} else {for (int i = size - 1; i >= 0; i--)if (o.equals(elementData[i]))return i;}return -1;}/*** 返回此ArrayList实例的浅拷贝。 (元素本身不被复制。)*/public Object clone() {try {ArrayList<?> v = (ArrayList<?>) super.clone();//Arrays.copyOf功能是实现数组的复制,返回复制后的数组。参数是被复制的数组和复制的长度v.elementData = Arrays.copyOf(elementData, size);v.modCount = 0;return v;} catch (CloneNotSupportedException e) {// 这不应该发生,因为我们是可以克隆的throw new InternalError(e);}}/*** 以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。* 返回的数组将是“安全的”,因为该列表不保留对它的引用。* (换句话说,这个方法必须分配一个新的数组)。* 因此,调用者可以自由地修改返回的数组结构。* 注意:如果元素是引用类型,修改元素的内容会影响到原列表中的对象。* 此方法充当基于数组和基于集合的API之间的桥梁。*/public Object[] toArray() {return Arrays.copyOf(elementData, size);}/*** 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素);* 返回的数组的运行时类型是指定数组的运行时类型。 如果列表适合指定的数组,则返回其中。* 否则,将为指定数组的运行时类型和此列表的大小分配一个新数组。* 如果列表适用于指定的数组,其余空间(即数组的列表数量多于此元素),则紧跟在集合结束后的数组中的元素设置为null 。* (这仅在调用者知道列表不包含任何空元素的情况下才能确定列表的长度。)*/@SuppressWarnings("unchecked")public <T> T[] toArray(T[] a) {if (a.length < size)// 新建一个运行时类型的数组,但是ArrayList数组的内容return (T[]) Arrays.copyOf(elementData, size, a.getClass());//调用System提供的arraycopy()方法实现数组之间的复制System.arraycopy(elementData, 0, a, 0, size);if (a.length > size)a[size] = null;return a;}// Positional Access Operations@SuppressWarnings("unchecked")E elementData(int index) {return (E) elementData[index];}/*** 返回此列表中指定位置的元素。*/public E get(int index) {rangeCheck(index);return elementData(index);}/*** 用指定的元素替换此列表中指定位置的元素。*/public E set(int index, E element) {//对index进行界限检查rangeCheck(index);E oldValue = elementData(index);elementData[index] = element;//返回原来在这个位置的元素return oldValue;}/*** 将指定的元素追加到此列表的末尾。*/public boolean add(E e) {ensureCapacityInternal(size + 1);  // Increments modCount!!//这里看到ArrayList添加元素的实质就相当于为数组赋值elementData[size++] = e;return true;}/*** 在此列表中的指定位置插入指定的元素。* 先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大;* 再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。*/public void add(int index, E element) {rangeCheckForAdd(index);ensureCapacityInternal(size + 1);  // Increments modCount!!//arraycopy()这个实现数组之间复制的方法一定要看一下,下面就用到了arraycopy()方法实现数组自己复制自己System.arraycopy(elementData, index, elementData, index + 1,size - index);elementData[index] = element;size++;}/*** 删除该列表中指定位置的元素。 将任何后续元素移动到左侧(从其索引中减去一个元素)。*/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; // clear to let GC do its work//从列表中删除的元素return oldValue;}/*** 从列表中删除指定元素的第一个出现(如果存在)。 如果列表不包含该元素,则它不会更改。* 返回true,如果此列表包含指定的元素*/public boolean remove(Object o) {if (o == null) {for (int index = 0; index < size; index++)if (elementData[index] == null) {fastRemove(index);return true;}} else {for (int index = 0; index < size; index++)if (o.equals(elementData[index])) {fastRemove(index);return true;}}return false;}/** Private remove method that skips bounds checking and does not* return the value removed.*/private void fastRemove(int index) {modCount++;int numMoved = size - index - 1;if (numMoved > 0)System.arraycopy(elementData, index + 1, elementData, index,numMoved);elementData[--size] = null; // clear to let GC do its work}/*** 从列表中删除所有元素。*/public void clear() {modCount++;// 把数组中所有的元素的值设为nullfor (int i = 0; i < size; i++)elementData[i] = null;size = 0;}/*** 按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾。*/public boolean addAll(Collection<? extends E> c) {Object[] a = c.toArray();int numNew = a.length;ensureCapacityInternal(size + numNew);  // Increments modCountSystem.arraycopy(a, 0, elementData, size, numNew);size += numNew;return numNew != 0;}/*** 将指定集合中的所有元素插入到此列表中,从指定的位置开始。*/public boolean addAll(int index, Collection<? extends E> c) {rangeCheckForAdd(index);Object[] a = c.toArray();int numNew = a.length;ensureCapacityInternal(size + numNew);  // Increments modCountint numMoved = size - index;if (numMoved > 0)System.arraycopy(elementData, index, elementData, index + numNew,numMoved);System.arraycopy(a, 0, elementData, index, numNew);size += numNew;return numNew != 0;}/*** 从此列表中删除所有索引为fromIndex (含)和toIndex之间的元素。* 将任何后续元素移动到左侧(减少其索引)。*/protected void removeRange(int fromIndex, int toIndex) {modCount++;int numMoved = size - toIndex;System.arraycopy(elementData, toIndex, elementData, fromIndex,numMoved);// clear to let GC do its workint newSize = size - (toIndex - fromIndex);for (int i = newSize; i < size; i++) {elementData[i] = null;}size = newSize;}/*** 检查给定的索引是否在范围内。*/private void rangeCheck(int index) {if (index >= size)throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}/*** add和addAll使用的rangeCheck的一个版本*/private void rangeCheckForAdd(int index) {if (index > size || index < 0)throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}/*** 返回IndexOutOfBoundsException细节信息*/private String outOfBoundsMsg(int index) {return "Index: " + index + ", Size: " + size;}/*** 从此列表中删除指定集合中包含的所有元素。*/public boolean removeAll(Collection<?> c) {Objects.requireNonNull(c);//如果此列表被修改则返回truereturn batchRemove(c, false);}/*** 仅保留此列表中包含在指定集合中的元素。* 换句话说,从此列表中删除其中不包含在指定集合中的所有元素。*/public boolean retainAll(Collection<?> c) {Objects.requireNonNull(c);return batchRemove(c, true);}/*** 从列表中的指定位置开始,返回列表中的元素(按正确顺序)的列表迭代器。* 指定的索引表示初始调用将返回的第一个元素为next 。 初始调用previous将返回指定索引减1的元素。* 返回的列表迭代器是fail-fast 。*/public ListIterator<E> listIterator(int index) {if (index < 0 || index > size)throw new IndexOutOfBoundsException("Index: " + index);return new ListItr(index);}/*** 返回列表中的列表迭代器(按适当的顺序)。* 返回的列表迭代器是fail-fast 。*/public ListIterator<E> listIterator() {return new ListItr(0);}/*** 以正确的顺序返回该列表中的元素的迭代器。* 返回的迭代器是fail-fast 。*/public Iterator<E> iterator() {return new Itr();}

三、ArrayList 扩容机制分析

1、先从 ArrayList 的构造函数说起

ArrayList 有三种方式来初始化,构造方法源码如下(JDK8):

/*** 默认初始容量大小*/
private static final int DEFAULT_CAPACITY = 10;private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};/*** 默认构造函数,使用初始容量10构造一个空列表(无参数构造)*/
public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}/*** 带初始容量参数的构造函数。(用户自己指定容量)*/
public ArrayList(int initialCapacity) {if (initialCapacity > 0) {//初始容量大于0//创建initialCapacity大小的数组this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {//初始容量等于0//创建空数组this.elementData = EMPTY_ELEMENTDATA;} else {//初始容量小于0,抛出异常throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);}
}/***构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回*如果指定的集合为null,throws NullPointerException。*/
public ArrayList(Collection<? extends E> c) {elementData = c.toArray();if ((size = elementData.length) != 0) {// c.toArray might (incorrectly) not return Object[] (see 6260652)if (elementData.getClass() != Object[].class)elementData = Arrays.copyOf(elementData, size, Object[].class);} else {// replace with empty array.this.elementData = EMPTY_ELEMENTDATA;}
}

细心的同学一定会发现:以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。 下面在我们分析 ArrayList 扩容时会讲到这一点内容!

补充:JDK6 new 无参构造的 ArrayList 对象时,直接创建了长度是 10 的 Object[] 数组 elementData

2、一步一步分析 ArrayList 扩容机制

这里以无参构造函数创建的 ArrayList 为例分析。

add 方法

/**
* 将指定的元素追加到此列表的末尾。
*/
public boolean add(E e) {// 加元素之前,先调用ensureCapacityInternal方法ensureCapacityInternal(size + 1);  // Increments modCount!!// 这里看到ArrayList添加元素的实质就相当于为数组赋值elementData[size++] = e;return true;
}

注意:JDK11 移除了 ensureCapacityInternal() 和 ensureExplicitCapacity() 方法

ensureCapacityInternal 方法的源码如下:

// 根据给定的最小容量和当前数组元素来计算所需容量。
private static int calculateCapacity(Object[] elementData, int minCapacity) {// 如果当前数组元素为空数组(初始情况),返回默认容量和最小容量中的较大值作为所需容量if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}// 否则直接返回最小容量return minCapacity;
}// 确保内部容量达到指定的最小容量。
private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

ensureCapacityInternal 方法非常简单,内部直接调用了 ensureExplicitCapacity 方法:

//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {modCount++;//判断当前数组容量是否足以存储minCapacity个元素if (minCapacity - elementData.length > 0)//调用grow方法进行扩容grow(minCapacity);
}

我们来仔细分析一下:

  • 当我们要 add 进第 1 个元素到 ArrayList 时,elementData.length 为 0 (因为还是一个空的 list),因为执行了 ensureCapacityInternal() 方法 ,所以 minCapacity 此时为 10。此时,minCapacity - elementData.length > 0成立,所以会进入 grow(minCapacity) 方法。

  • add 第 2 个元素时,minCapacity 为 2,此时 elementData.length(容量)在添加第一个元素后扩容成 10 了。此时,minCapacity - elementData.length > 0 不成立,所以不会进入 (执行)grow(minCapacity) 方法。

  • 添加第 3、4···到第 10 个元素时,依然不会执行 grow 方法,数组容量都为 10。

直到添加第 11 个元素,minCapacity(为 11)比 elementData.length(为 10)要大。进入 grow 方法进行扩容。

grow 方法

/*** 要分配的最大数组大小*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;/*** ArrayList扩容的核心方法。*/
private void grow(int minCapacity) {// oldCapacity为旧容量,newCapacity为新容量int oldCapacity = elementData.length;// 将oldCapacity 右移一位,其效果相当于oldCapacity /2,// 我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,int newCapacity = oldCapacity + (oldCapacity >> 1);// 然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,if (newCapacity - minCapacity < 0)newCapacity = minCapacity;// 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,// 如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win:elementData = Arrays.copyOf(elementData, newCapacity);
}

int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)! 奇偶不同,比如:10+10/2 = 15, 33+33/2=49。如果是奇数的话会丢掉小数.

">>"(移位运算符):>>1 右移一位相当于除 2,右移 n 位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了 1 位所以相当于 oldCapacity /2。对于大数据的 2 进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源

我们再来通过例子探究一下grow() 方法:

  • add 第 1 个元素时,oldCapacity 为 0,经比较后第一个 if 判断成立,newCapacity = minCapacity(为 10)。但是第二个 if 判断不会成立,即 newCapacity 不比 MAX_ARRAY_SIZE 大,则不会进入 hugeCapacity 方法。数组容量为 10,add 方法中 return true,size 增为 1。

  • add 第 11 个元素进入 grow 方法时,newCapacity 为 15,比 minCapacity(为 11)大,第一个 if 判断不成立。新容量没有大于数组最大 size,不会进入 hugeCapacity 方法。数组容量扩为 15,add 方法中 return true,size 增为 11。

这里补充一点比较重要,但是容易被忽视掉的知识点:

  • Java 中的 length属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性.

  • Java 中的 length() 方法是针对字符串说的,如果想看这个字符串的长度则用到 length() 这个方法.

  • Java 中的 size() 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看!

hugeCapacity()  方法

从上面 grow() 方法源码我们知道:如果新容量大于 MAX_ARRAY_SIZE,进入(执行) hugeCapacity() 方法来比较 minCapacityMAX_ARRAY_SIZE,如果 minCapacity 大于最大容量,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8

private static int hugeCapacity(int minCapacity) {if (minCapacity < 0) // overflowthrow new OutOfMemoryError();// 对minCapacity和MAX_ARRAY_SIZE进行比较// 若minCapacity大,将Integer.MAX_VALUE作为新数组的大小// 若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小// MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;
}

3、System.arraycopy() 和 Arrays.copyOf()方法

阅读源码的话,我们就会发现 ArrayList 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及add(int index, E element)toArray() 等方法中都用到了该方法!

System.arraycopy() 方法

源码:

    // 我们发现 arraycopy 是一个 native 方法,接下来我们解释一下各个参数的具体意义/***   复制数组* @param src 源数组* @param srcPos 源数组中的起始位置* @param dest 目标数组* @param destPos 目标数组中的起始位置* @param length 要复制的数组元素的数量*/public static native void arraycopy(Object src,  int  srcPos,Object dest, int destPos,int length);

场景:

    /*** 在此列表中的指定位置插入指定的元素。*先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大;*再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。*/public void add(int index, E element) {rangeCheckForAdd(index);ensureCapacityInternal(size + 1);  // Increments modCount!!//arraycopy()方法实现数组自己复制自己//elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量;System.arraycopy(elementData, index, elementData, index + 1, size - index);elementData[index] = element;size++;}

我们写一个简单的方法测试以下:

public class ArraycopyTest {public static void main(String[] args) {// TODO Auto-generated method stubint[] a = new int[10];a[0] = 0;a[1] = 1;a[2] = 2;a[3] = 3;System.arraycopy(a, 2, a, 3, 3);a[2]=99;for (int i = 0; i < a.length; i++) {System.out.print(a[i] + " ");}}}

结果:

0 1 99 2 3 0 0 0 0 0

Arrays.copyOf()方法Arrays.copyOf()方法Arrays.copyOf()方法

源码:

    public static int[] copyOf(int[] original, int newLength) {// 申请一个新的数组int[] copy = new int[newLength];// 调用System.arraycopy,将源数组中的数据进行拷贝,并返回新的数组System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength));return copy;}

场景:

   /**以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 返回的数组的运行时类型是指定数组的运行时类型。*/public Object[] toArray() {//elementData:要复制的数组;size:要复制的长度return Arrays.copyOf(elementData, size);}

个人觉得使用 Arrays.copyOf()方法主要是为了给原有数组扩容,测试代码如下:

public class ArrayscopyOfTest {public static void main(String[] args) {int[] a = new int[3];a[0] = 0;a[1] = 1;a[2] = 2;int[] b = Arrays.copyOf(a, 10);System.out.println("b.length"+b.length);}
}

结果:

10

两者联系和区别

联系:

看两者源代码可以发现 copyOf()内部实际调用了 System.arraycopy() 方法

区别:

arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 copyOf() 是系统自动在内部新建一个数组,并返回该数组。

4、ensureCapacity方法

ArrayList 源码中有一个 ensureCapacity 方法不知道大家注意到没有,这个方法 ArrayList 内部没有被调用过,所以很显然是提供给用户调用的,那么这个方法有什么作用呢?

    /**如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。** @param   minCapacity   所需的最小容量*/public void ensureCapacity(int minCapacity) {int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)// any size if not default element table? 0// larger than default for default empty table. It's already// supposed to be at default size.: DEFAULT_CAPACITY;if (minCapacity > minExpand) {ensureExplicitCapacity(minCapacity);}}

理论上来说,最好在向 ArrayList 添加大量元素之前用 ensureCapacity 方法,以减少增量重新分配的次数

我们通过下面的代码实际测试以下这个方法的效果:

public class EnsureCapacityTest {public static void main(String[] args) {ArrayList<Object> list = new ArrayList<Object>();final int N = 10000000;long startTime = System.currentTimeMillis();for (int i = 0; i < N; i++) {list.add(i);}long endTime = System.currentTimeMillis();System.out.println("使用ensureCapacity方法前:"+(endTime - startTime));}
}

运行结果:

使用ensureCapacity方法前:2158


public class EnsureCapacityTest {public static void main(String[] args) {ArrayList<Object> list = new ArrayList<Object>();final int N = 10000000;long startTime1 = System.currentTimeMillis();list.ensureCapacity(N);for (int i = 0; i < N; i++) {list.add(i);}long endTime1 = System.currentTimeMillis();System.out.println("使用ensureCapacity方法后:"+(endTime1 - startTime1));}
}

运行结果:

使用ensureCapacity方法后:1773

通过运行结果,我们可以看出向 ArrayList 添加大量元素之前使用ensureCapacity 方法可以提升性能。不过,这个性能差距几乎可以忽略不计。而且,实际项目根本也不可能往 ArrayList 里面添加这么多元素。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/13804.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

网络安全应该学什么?别被培训机构这些内容给骗了!

了解过的朋友都知道&#xff0c;网络安全内容十分丰富&#xff0c;大大小小的知识点都包含。所以有的朋友就都想学&#xff0c;尤其一些培训机构的课程大纲介绍的特别详细&#xff0c;又包含这又包含那&#xff0c;但是这些内容真的都实用吗&#xff1f;如果想系统学习&#xf…

吴恩达LLM Agent工作流Prompt设计精解

在详解和实测吴恩达4种Agentic 工作流之中&#xff0c;我测试了各种框架诸如反思、工具调用、规划、多智能体&#xff0c;在学习了其中各种Prompt设计后&#xff0c;有了一些新的认识。 对于特定的任务来说&#xff0c;没有万能的Prompt&#xff0c;只有一些通用的模式&#xf…

除了 Mock.js,前端还有更方便的 Mock 数据工具吗?

在前端开发中&#xff0c;模拟数据&#xff08;Mock Data&#xff09;是不可或缺的一部分&#xff0c;它能够帮助开发者在后端接口未完成前进行界面和逻辑的测试。而 Mock.js 是一个广泛使用的库&#xff0c;它通过简洁的语法和强大的功能&#xff0c;让前端开发者可以轻松地创…

【原创】java+ssm+mysql高校学籍管理系统设计与实现

个人主页&#xff1a;程序猿小小杨 个人简介&#xff1a;从事开发多年&#xff0c;Java、Php、Python、前端开发均有涉猎 博客内容&#xff1a;Java项目实战、项目演示、技术分享 文末有作者名片&#xff0c;希望和大家一起共同进步&#xff0c;你只管努力&#xff0c;剩下的交…

pytorch深度学习环境安装 + 讲解【新手版】

不知道有没有学深度学习的小伙伴在安装深度学习环境时候很头疼&#xff0c;反正我在研一时候是很头疼很头疼的一件事&#xff0c;根本搞不清楚什么显卡、显卡驱动、pytorch版本、cuda、cudnn等等等&#xff0c;这些是不是非常的头疼。 好&#xff0c;你们的救星来了。我&#x…

zabbix搭建钉钉告警流程

目录 zabbix实验规划 zabbix实验步骤 1 使用钉钉添加一个自定义的机器人 ​编辑2在zabbix-server上编写钉钉信息发送脚本&#xff0c;设置钉钉报警媒介 设置钉钉报警媒介​编辑​编辑 在添加消息模板​编辑​编辑​编辑 3设置动作条件 触发后的行为&#xff1a;重新添加一…

无人机飞手考证,地面站培训技术详解

无人机飞手考证及地面站培训技术涉及多个关键方面&#xff0c;以下是对这些方面的详细解析&#xff1a; 一、无人机飞手考证流程与要求 1. 证书类型 民用无人机驾驶员证书&#xff1a;这是国家民航局颁发的无人机操作人员资质证书&#xff0c;分为视距内驾驶员、超视距驾驶员…

高颜值的卡片折叠效果(附源码)

预览效果 源码(html部分) <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><title>17sucai -Holiday Feature Folding Cards [Pure CSS]</title><meta charset"UTF-8"><meta name&qu…

Mybatis的执行流程解析

根据图中步骤&#xff0c;我们可以将这个执行流程分成了8个步骤。 1、读取MyBatis的核心配置文件。mybatis-config.xml为MyBatis的全局配置文件&#xff0c;用于配置数据库连接、属性、类型别名、类型处理器、插件、环境配置、映射器&#xff08;mapper.xml&#xff09;等信息…

24年下软考系统架构设计师真题及答案,估分、备考速看!

2024下半年软考考试已经圆满结束了&#xff0c;为大家整理了网友回忆版的软考高级系统架构设计师真题真题及答案。下半年考试的宝子们可以对答案预估分数&#xff01;准备明年考的宝子可以提前把握考试知识点和出题方向&#xff0c;说不定会遇到相同考点的题目&#xff01; 一、…

手把手教你用Coze零代码搭建一个智能搜索智能体,高时效性、保姆级!

随着大模型技术的发展&#xff0c;越来越多的技术开始涌现&#xff0c;从聊天助手&#xff0c;到智能体&#xff0c;再到工作流&#xff0c;最后到三者的整合。大模型技术朝着更加智能化、通用化、个性化的方向发展&#xff0c;为人们的生活和工作带来了更多的便利和创新。 今…

HTML之列表学习记录

练习题&#xff1a; 图所示为一个问卷调查网页&#xff0c;请制作出来。要求&#xff1a;大标题用h1标签&#xff1b;小题目用h3标签&#xff1b;前两个问题使用有序列表&#xff1b;最后一个问题使用无序列表。 代码&#xff1a; <!DOCTYPE html> <html> <he…

数据结构Python版

2.3.3 双链表 双链表和链表一样&#xff0c;只不过每个节点有两个链接——一个指向后一个节点&#xff0c;一个指向前一个节点。此外&#xff0c;除了第一个节点&#xff0c;双链表还需要记录最后一个节点。 每个结点为DLinkNode类对象&#xff0c;包括存储元素的列表data、…

Linux手动安装nginx

本次以安装nginx-1.12.2为例 1、首先说明一下,安装nginx之前需要安装如下素材: 2、开始安装 第一步,安装依赖yum -y install gcc zlib zlib-devel pcre-devel openssl openssl-devel第二步,下载并安装nginx安装包(nginx官网:http://nginx.org/)# 下载 wget http://nginx…

无线感知会议系列【14】SignFi: Sign Language Recognition Using WiFi

摘要&#xff1a; 这篇Paper 是用CNN 做的,用来做手语识别的 模型输入&#xff1a; csi_tensor [M,N,S,T] M: tx 发送天线数量 N: rx 天线数量 S: 幅度和相位信息 T: CSI matrix for each instance 数据集大小 模型结构,跟斯坦福的HAR LSTM 有较大差异[batch_size, time, carr…

详解AI产品经理的发展与规划(附完整PPT)

随着AI技术的逐渐普及与落地&#xff0c;AI产品经理在市场上也变得分外火热。那么在未来&#xff0c;这个职业将如何发展&#xff0c;它的工作要素有哪些&#xff0c;要怎么做才能成为一名AI产品经理呢&#xff1f; 大家好&#xff0c;近日分享一些关于AI产品经理的话题。这个…

【大数据技术基础 | 实验十】Hive实验:部署Hive

文章目录 一、实验目的二、实验要求三、实验原理四、实验环境五、实验内容和步骤&#xff08;一&#xff09;安装部署&#xff08;二&#xff09;配置HDFS&#xff08;三&#xff09;启动Hive 六、实验结果&#xff08;一&#xff09;启动结果&#xff08;二&#xff09;Hive基…

[⑧5G NR]: PBCH payload生成

本篇博客记录下5G PBCH信道中payload数据的生成方式。PBCH payload一共32个比特&#xff0c;基本结构如下图&#xff1a; 根据SSB PDU中bchPayloadFlag的值有三种方式得到PBCH payload。 bchPayloadFlag 0&#xff1a;全部32比特由MAC层提供。 bchPayloadFlag 1&#xff1a;M…

大模型面试熬夜爆肝整理,附八股文和答案,这次换我手撕面试官了吧?

导读 自ChatGPT开启大模型时代以来&#xff0c;大模型正迎来飞速发展&#xff0c;现在从事大模型开发相关工作可谓是处在时代的风口。那么大模型面试需要哪些技能和技巧呢&#xff0c;本文详细整理了全套的面试问题及答案&#xff0c;希望对大家有所帮助&#xff01; 目录 [x…

刷题笔记——栈和队列互相冒充

刷题笔记——栈和队列互相冒充 5.3 用队列实现栈两队列实现栈一个队列实现栈 5.4 用栈实现队列两栈实现队列push栈和pop栈一个栈实现队列 5.3 用队列实现栈 原OJ题&#xff1a;225. 用队列实现栈 - 力扣&#xff08;LeetCode&#xff09; 两队列实现栈 入栈的实现 选非空的…