Literacy Program - ArrayList Common Methods Principle Literacy

ArrayList sweep

Get into the habit of writing together! This is the first day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event .

Introduction

ArrayList is a commonly used implementation class of List. From the name, it can be seen that its bottom layer is implemented through arrays. Therefore, it has a disadvantage that there can be no gaps between elements, and when inserting and deleting elements in the middle, you need to perform operations such as moving and copying the array, which takes a long time. But its query efficiency is high, because it is implemented through an array, so it supports fast random access. If there are fewer modifications to the List in a List, and the number of queries is more, ArrayList is recommended.

Important properties in ArrayList

//默认容量大小
private static final int DEFAULT_CAPACITY = 10;
//空数组
private static final Object[] EMPTY_ELEMENTDATA = {};

//这个也是一个空数组,不过我没有理解为什么会有两个空的数组,不过在看源码时发现DEFAULTCAPACITY_EMPTY_ELEMENTDATA
//是用在无参构造里面的,EMPTY_ELEMENTDATA是用在有参构造中,如果参数不规范就会赋值这个空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//存储元素的数组,在第一次添加元素的时候被初始化
transient Object[] elementData;

//ArrayList的数组大小(ArrayList的元素数量)
private int size;
复制代码

Constructor in ArrayList

There are three constructors in ArrayList: two with parameters and one without parameters

  • ArrayList()
  • ArrayList(Collection<? extends E> c)
  • ArrayList(int initialCapacity)

Null parameter construction

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
复制代码

There is nothing to say about the null parameter construction, it is to assign an empty array to the array that stores the elements.

Constructed with parameters: initialize the size of the ArrayList

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
复制代码

The logic in this is pretty simple:

  1. Determine whether the size of the current array to be created is greater than 0. If it is greater than 0, an array will be created normally and assigned to elementData
  2. Determine whether the size of the current array to be created is equal to 0, and if so, assign EMPTY_ELEMENTDATA to elementData
  3. When the above two If branches are completed, it can be determined that the number passed in must be a number less than 0. The capacity size of the array cannot be less than 0 when it is created, so an illegal parameter exception is thrown here.

Constructed with parameters: pass in an implementation class of the Collection interface

image-20220403165636455

As long as it is a subclass of Collections, it can be passed in. Here I only list Set and List, and some others.

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}
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;
    }
}
复制代码

这个构造方法可以传一个实现了Collection的类。在这个构造里面会将传入类的数组赋值给elementData.但是传入的这个它的泛形是一是E的,如果这里传入的参数长度是0那么就会赋值一个空的数组。

还有一点要说的:这里有一个条件判断elementData是不是Object[]数组(这里一般都会是Object数组),如果不是的话会将其传成Object数组,通过Arrays.copyOf重新赋值。Arrays工具在java.util包里面有兴趣可以看下

ArrayList中的添加元素的方法

在使用ArrayList的时候用的最多的方法就是添加和查询了吧。那就先来看下添加是如何实现的。

插入的方法分为两种:

  • 直接插入
  • 指定位置插入

直接插入元素的方法

public boolean add(E e) {
    ensureCapacityInternal(size + 1); 
    elementData[size++] = e;
    return true;
}
复制代码

插入方法总结后大至分为两步:

  • 确保数组的容量是否能够插入一个元素
  • 将当前需要插入的元素放到数组指定位置

如何检查数组容量

在添加元素的时候会调用这么一个方法,用来在添加数据前确保数组的容量,它在这里需要传入一个最小的空间数值(这个数值计算的方式是在原数组大小的基础上加1,因为会插入一个元素嘛所以要加1),这个ensureCapacityInternal方法主要是用来检查当前的数组是否需要扩容的方法。还会有一个计算容量的方法calculateCapacity

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
复制代码

计算插入数组所需的容量

检查数组是否需要扩容前,首先需要一个数值用来判断当前是否需要扩容。这个数值就是通过calculateCapacity方法计算出来了。

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
复制代码

这个方法没有太难的逻辑,步骤大概如下:

  • 先判断当前数组是否是一个空的数组,如果是的话,它会将默认的容量和传入来的minCapacity做对比,取出一个最大值,做为扩容后的数组大小。(这里就就是在创建ArrayList如果没有传入大小时,为啥ArrayList的容量是10的原因)
  • 如果不是一个空的数组,就会将当前传入的minCapacity直接返回

检查是否需要扩容

当确定完插入数据后所需要的容量,就要开始判断当前数组的大小是否可以将这个元素插入了。经过判断后如果容量不足就会触发扩容。

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
复制代码

这个方法判断是否需要扩容的条件是:

使用minCapacity减去当前数组的大小如果大于0就说明当前数组不足以插入一个元素,就需要进行扩容操作

扩容方法

这个扩容的方法需要一个参数,这个参数就是插入数据后的数组所需要的最小容量

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    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);
}
复制代码

这个扩容的方法大至分为5步:

  • 第一步:首先会将未扩容之前的数组长度记录下来
  • 第二步:会生成一个新的数组长度,这个长度就是数组扩容之后的长度。新的长度是(旧容量 + 旧容量 / 2),因为 左移操作就等于除2嘛
  • 第三步:检查新生成的数组容量减去最小容量是否大于0,否就说明当前生成新的容量并不能容下新插入的数据
  • 第四步:检查新生成的长度减最大数组容量是否大于0。为真就会通过调用hugeCapacity方法生成新的数组长度
    • 在个别的JVM里面,如果将Integer.MAX_VALUE结果作为数组的长度有可能会发生OOM,所以这里将MAX_ARRAY_SIZE设置成了Integer.MAX_VALUE - 8
    • 当newCapacity比MAX_ARRAY_SIZE大时,调用hugeCapacity方法。这个方法原理是判断minCapacity 是否大于MAX_ARRAY_SIZE,如果小于就会将MAX_ARRAY_SIZE返回作为新的数组长度。否则就会将Integer.MAX_VALUE 返回。
    • 这里有些伙伴可能存在疑惑,为啥这里又把Integer.MAX_VALUE 返回了,不是要避免OOM吗?原因是minCapacity 都比MAX_ARRAY_SIZE大了,那就直接把Integer.MAX_VALUE返回,也不管什么OOM,爱咋咋滴吧。
  • 第五步:到了这一步,就是将通过Arrays.copyOf方法,把老的数组和新的容量传入,就会生成一个新的数组并赋值给elementData,完成扩容。

插入数据

上面的过程完成后就可以调用add方法里面赋值的过程。

public boolean add(E e) {
    //.... 
    elementData[size++] = e;
    //....
}
复制代码

将当前的元素赋值给elementData中的某一个位置。并且将数组元素的数量加1(size++)

就完成了插入操作。

指定位置插入元素

先过一遍源码,这个里面源码好说一些,因为ensureCapacityInternal在上一章已经说过了,这里就不重复了。

public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}
复制代码

这个指定位置插入数据大致可以分成四步:

  • 检查索引是否存在错误
  • 是否需要扩容
  • 数组的拷贝
  • 在数组的指定位置赋值

检查索引是否存在错误

在指定位置插入之前首先要判断要插入的位置是否越界。

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
复制代码

原理就是判断了index是否大于数组的最大长度,或者是index小于0,如果这个条件成立,就说明不能将该元素插入这个位置。

拷贝数组

public void add(int index, E element) {
    //....
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    //....
}
复制代码

拷贝数组的时候主要是调用了System.arraycopy之个方法,这个方法是一个native方法。只来看一下它要传的参数就好:

public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);
复制代码
  • src: 源数组
  • srcPos:从源数组的那个位置开始拷贝
  • dest:目标数组
  • destPos: 目标数组的起始位置
  • length: 需要拷贝的长度

这样拷贝完成后,要插入元素的index位置就空了出来,然后将这个要插入的元素赋值到数组的index位置就好了。然后将数组元素的数量(size)加1。

查询元素

ArrayList除了添加放法,常用到的还有一个查询元素的方法get()

public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}
复制代码

这个方法就比较简单了,首先检查下传入index是否会存在越界,如果越界就会抛出 IndexOutOfBoundsException异常。不存在越界问题就直接将数组该位置的元素返回。

移除元素

移除元素也有多个方法,下面一一介绍下:

  • 移除指定位置元素
  • 移除指定数据

移除指定位置元素

移除指定位置的元素只需要将元素在数组中的下标传入即可。源码如下:

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; 

    return oldValue;
}
复制代码

移除指定位置的元素大至可以分为以下几步:

  • 检查当前传入的索引位置是否越界,通过rangeCheck方法进行检查

  • 保存当前所要删除的值,在return的时候将其返回

  • 计算出所要移动数组的长度(size - index - 1) ,你会发现这个公式的结果就是在index元素之后的元素个数。

  • Check whether numMoved is greater than 0. If it is not greater than 0, it means that there is one or no element in the array at this time. If it is greater than 0, the array copy method will be executed, as shown in the figure above.

  • Finally, reduce the number of elements in the array by 1, and empty the last element to facilitate GC recycling.

remove specified data

There is also a method to remove elements in ArrayList, you can pass in the value of the element to remove it

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;
}
复制代码

Passing in the value of an element removes it by looping over the array.

It checks whether the incoming value is empty before looping.

Q: Why is it necessary to distinguish whether it is empty or not?

Answer: If a null is passed in, a null pointer exception will occur when calling the equals method.

Then after entering a branch of if, it starts to traverse the array. If the incoming value is found to be equal to the value traversed by the array, the fastRemovemethod will be called to delete it.

The fastRemove method will not be explained too much here, because the code of this method is the same as the code for removing the specified position element mentioned above.

replace the element at the specified position

When you use ArrayList, the following set method must be used, so let's see how this method is implemented.

 public static void main(String[] args) {
     List<String> l = new ArrayList<>(10);
     l.add("2");
     l.set(0,"4");
     System.out.println("替换后的元素值:" + l.get(0));
 }

替换后的元素值:4
复制代码

Like many methods in ArrayList, we first judge whether the incoming index is out of bounds.

public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}
复制代码

If the index is not out of bounds, it will save the old value. Then replace it with the new incoming value and it's OK.

Then return the old value.

Here only the implementation principles of several methods we commonly use are described. If you are interested in other methods, you can check them yourself.

Guess you like

Origin juejin.im/post/7082708521313107981