ArrayList常用方法原理扫盲

ArrayList扫肓

简介

ArrayList是一个常用的List的实现类,从名字上就能看出来它的底层是通过数组实现的。所以它有一个缺点就是元素之间不能存在间隔,并且在中间插入元素和删 除的时候需要对数组进行移动、复制等操作,耗时比较久一些。但是它的查询时的效率是高的,因为它是通过数组实现,所以它支持快速的随机访问。如果一个List中对于修改List较少,查询的次数较多更加的推荐ArrayList。

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;

ArrayList中的构造方法

ArrayList中有三个构造方法:两个有参构造和一个无参构造

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

空参构造

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

空参构造没有什么好说的,它就是将一个空的数组赋值给了存储元素的数组。

有参构造:初始化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);
    }
}

这个里面的逻辑也挺简单的:

  1. 判断当前要创建数组的大小是否大于0,如果大于0就会正常创建一个数组并赋值给elementData
  2. 判断当前要创建数组的大小是否等于0,如果等于就将EMPTY_ELEMENTDATA赋值给elementData
  3. 当上面两个If分支走完,就可以段定传进来的这个数一定是一个小于0的数,数组创建时的容量大小是不可以小于0的,所以这里就抛出一个非法参数异常

有参构造:传入一个Collection接口的实现类

image-20220403165636455

只要是Collections的子类是都可以传入的。这里我只列出了Set和List,还有一些其它的。

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元素之后的元素个数。

  • 检查numMoved是否大于0,如果不大于0就说明数组内此时就一个或者是没有元素,如果大于0就会执行数组拷贝方法,如上图

  • 最后将数组内的元素个数减1,并将最后一个元素置空,方便GC的回收。

移除指定数据

在ArrayList中还有一个移除元素的方法,可以传入元素的值将其移除

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;
}

传入元素的值将其移除的方法是通过对数组的循环来实现的。

它在循环之前先对传入的值进行了是否为空的判断。

问:为啥删除的这里要区分是否为空?

答:如果传入的是一个null的话,在调用equals方法时会出现空指针异常。

然后进入if的某一个分支后就开始对数组进行遍历操作,如果发现传入的值与数组遍历到的值相等,就会调用fastRemove方法将其删除,

fastRemove方法这里不在做过多的解释,因为这个方法的代码和上面讲到了*的移除指定位置元素代码一样的

替换指定位置的元素

大家在使用ArrayList的时候下面这个set方法也一定会用过吧,那就来看看这个方法是如何实现的。

 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

同ArrayList中好多的方法一样,上来先对传入的索引进行是否越界的判断

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

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

如果索引没有越界,那就会将旧的值保存下来。然后将新传入的值将其替换掉就OK了。

然后将旧的值返回即可。

这里只说了我们常用到的几个方法的实现原理,其它的方法如果有兴趣可以自行查看。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值