• 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了。

    然后将旧的值返回即可。

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

  • 相关阅读:
    MyCLI :一个支持自动补全和语法高亮的 MySQL/MariaDB 客户端
    pathlib:优雅的路径处理库
    MySQL索引连环18问
    Mysql 百万级数据迁移实战笔记
    强大的Json解析工具 Jsonpath 实战教程
    JavaScript 中的 Var,Let 和 Const 有什么区别
    【前端安全】从需求分析开始,详解前端加密与验签实践
    vue3开发企业级生鲜系统项目
    mysql随笔
    shiro相关Filter
  • 原文地址:https://www.cnblogs.com/sdayup/p/16100241.html
Copyright © 2020-2023  润新知