【看看源码】尝试读ArrayList源码

文章详细分析了ArrayList的构造方法,包括无参数和初始化容量的构造。讨论了ArrayList如何根据传入的集合或指定容量来初始化内部数组。接着,重点讲解了添加元素的过程,特别是扩容机制,包括默认容量、1.5倍增长策略以及如何避免并发修改异常。此外,还涉及删除和修改元素的方法。
摘要由CSDN通过智能技术生成

List集合源码分析

再次接触List集合,已经有一定的能力可以看看List的源码,接下来便分析一下。

ArrayList源码分析

  1. 构造方法

    无参数构造方法:

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    transient Object[] elementData; // non-private to simplify nested class access
    
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    

    可以发现,ArrayList集合底层采用数组。当我们采用无参构造方法初始化的时候,默认会初始化一个空数组


    初始化容量构造方法:

    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    transient Object[] elementData; // non-private to simplify nested class access
    
    public ArrayList(int initialCapacity) {
        // 如果容量大于0,则创建一个容量为初始化值的数组,赋值给底层的数组
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            // 如果初始化数值为0,则赋值一个空对象
            this.elementData = EMPTY_ELEMENTDATA; 
        } else {
            // 小于零就直接抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
    

    当初始化时ArrayList数组时,如果在参数部分填上数值,则代表初始化容量:

    ArrayList<String> list = new ArrayList<>(18);
    

    通过添加另一个集合的方式来初始化

    // 官方注释: 数组列表的大小(它包含的元素数) 
    // The size of the ArrayList (the number of elements it contains).
    private int size;
    
    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    public ArrayList(Collection<? extends E> c) {
        // 将传入的集合转化成数组 方便后面赋值给底层的数组
        Object[] a = c.toArray();
        // 判断传入数组的容量是否不等于0 ,
        // 如果不等于0就把传入集合的元素数量传给ArrayList的计数变量 size
        if ((size = a.length) != 0) {
            // 判断传入的集合是否为ArrayList对象 如果时ArrayList直接将传入集合转化的数组赋值给底层数组
            // 这里再JDK8有提示:  
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            /*
            	因为看的是JDK17的源码 所以 ‘‘感觉’’下面判断好像貌似似乎可以不加 
            	用JDK17好像复现不了jdk8的错误了
            	有兴趣的取百度搜一下
            */
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                // 不是ArrayList对象则拷贝传输的集合转化的数组给底层的数组
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            // 如果传入的集合元素数量为0,则初始化底层的数组为空数组
            elementData = EMPTY_ELEMENTDATA;
        }
    }
    

    Collection继承图:

    image-20221104140841668

  2. 添加元素

    一个参数的添加元素

    /*
    	官方解释:此列表在<i>结构上被修改的次数<i>。结构修改是指更改列表大小或以其他方式干扰列表,使正在进行的迭代可能会产生不正确结果的修改。<p>此字段由 {@code 迭代器} 和 {@code listIterator} 方法返回的迭代器和列表迭代器实现使用。如果此字段的值意外更改,迭代器(或列表迭代器)将抛出 {@code ConcurrentModificationException} 以响应 {@code next}、{@code remove}、{@code previous}、{@code set} 或 {@code add} 操作。这提供了<i>快速故障行为<i>,而不是在迭代期间面对并发修改时的非确定性行为。<p><b>子类使用此字段是可选的。<b>如果子类希望提供快速失败迭代器(并列出迭代器),则只需在其 {@code add(int, E)} 和 {@code remove(int)} 方法(以及它覆盖的任何其他导致列表结构修改的方法)中递增此字段。对 {@code add(int, E)} 或 {@code remove(int)} 的单个调用必须向此字段添加不超过一个,否则迭代器(和列表迭代器)将抛出虚假的 {@code ConcurrentModificationExceptions}。如果实现不希望提供故障快速迭代器,则可以忽略此字段。
    	个人理解:避免在使用迭代器进行遍历的时候,有另外线程进行添加或者修改元素,从而出现不正确的结果
    	如果计算的结果modCount和预计的结果不相等就会出现并发修改异常。后面迭代遍历再说。
    */
    protected transient int modCount = 0;
    
    public boolean add(E e) {
        modCount++;
        // 调用添加元素的方法
        add(e, elementData, size);
        // 默认返回值 true 插入成功与否都会提示true
        return true;
    }
    // 被调用添加的方法
    private void add(E e, Object[] elementData, int s) {
        // 判断如果当前数组的实际容量如果和size大小一致 就调用grow()方法提升容量
        // elementData虽然是存放数据的地方,但真正控制集合大小 还是由size来决定。
        // 如果size 等于 底层数组的长度,就代表要进行扩容了
        if (s == elementData.length)
            elementData = grow();
        // 不需要扩容就进行赋值
        elementData[s] = e;
        // 添加一个元素 size要加1
        size = s + 1;
    }
    
    // ========== 添加容量的方法 ===============
    // 继续调用另一个方法
    private Object[] grow() {
        return grow(size + 1); // 这里加1 ,是当前数组存下刚加入的元素需要的最小容量
    }
    // 真正扩增容量的方法
    /*
    	官方解释:增加容量以确保它至少可以容纳最小容量参数指定的元素数。
    	@param 最小容量 所需的最小容量 @throws内存不足错误 最小容量小于零时出错。
    */
    
    private static final int DEFAULT_CAPACITY = 10;
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    private Object[] grow(int minCapacity) {
        	// 现在的容量 = 现在底层数组的容量长度
            int oldCapacity = elementData.length;
            if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                int newCapacity = ArraysSupport.newLength(oldCapacity,
                        minCapacity - oldCapacity, /* minimum growth */
                        oldCapacity >> 1           /* preferred growth */);
                return elementData = Arrays.copyOf(elementData, newCapacity);
            } else {
                // 如果当前底层数组的长度为0 或者数组还是一个{}
                // 就给数组初始化长度10,或者当前底层数组的容量
                return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
            }
    }
    
    /**
    *	所以,当ArrayList用无参方法初始化时,默认容量为0。
    	当第一次添加时
    		如果添加的元素的数量小于10,则默认数量为10.
    		如果元素数量等于10时,再次添加容量会扩充到1.5倍。
    		
    		通过构造方法添加元素时,容量变成构造方法的提供的容量/传入集合的数量,
    		下一次再添加时,容量扩充到1.5倍。
    *
    **/
    /*
    	oldLength: 当前底层数组的容量
    	minGrowth: 所需要的最小容量  ==== 1 / 添加集合的长度 (当前集合需要添加的最小容量)
    	prefGrowth: 首选增长量 (oldLength >> 1)
    	
    	并不是每次添加都会出发这个机制,只有当底层数组容量不够支持再添加下一个元素时,会触发这个机制
    */
    public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
        // preconditions not checked because of inlining
        // assert oldLength >= 0
        // assert minGrowth > 0
        int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
        if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
            return prefLength;
        } else {
            // put code cold in a separate method
            return hugeLength(oldLength, minGrowth);
        }
    }
    
    // 这里时避免集合数量达Math.max时 继扩容就是每次增长1 不进行1.5倍扩容
    private static int hugeLength(int oldLength, int minGrowth) {
        int minLength = oldLength + minGrowth;
        if (minLength < 0) { // overflow
            throw new OutOfMemoryError(
                "Required array length " + oldLength + " + " + minGrowth + " is too large");
        } else if (minLength <= SOFT_MAX_ARRAY_LENGTH) {
            return SOFT_MAX_ARRAY_LENGTH;
        } else {
            return minLength;
        }
    }
    
    // 对ArrayList集合进行反射 ,查看真实的容量
     private static int getArrayListLength(ArrayList<String> list) {
         Class<ArrayList> arrayListClass = ArrayList.class;
         try {
             //获取 elementData 字段
             Field field = arrayListClass.getDeclaredField("elementData");
             //开始访问权限
             field.setAccessible(true);
             //把示例传入get,获取实例字段elementData的值
             Object[] objects = (Object[]) field.get(list);
             //返回当前ArrayList实例的容量值
             return objects.length;
         } catch (Exception e) {
             e.printStackTrace();
             return -1;
         }
     }
    
    JDK17需要添加VM参数:
        --add-opens java.base/java.util=ALL-UNNAMED 
        --add-opens java.base/java.lang.reflect=ALL-UNNAMED
    

    image-20221231084622357


    多个参数添加元素

    搞懂上面的扩容机制,再往后看就简单很多了!

    根据索引位置插入元素:

    public void add(int index, E element) {
        // 检查所插入的索引是否合规
        rangeCheckForAdd(index);
        modCount++;
        final int s;
        Object[] elementData;
        // 这里是判断容量是否支持这次插入,如果不支持就进行扩容
        if ((s = size) == (elementData = this.elementData).length)
            elementData = grow();
        // 容量足够时,进行数组的拷贝
        System.arraycopy(elementData, index,
                         elementData, index + 1,
                         s - index);
        // 数组赋值完之后(将索引以及后面的元素往后复制一个后) 再对索引位置进行数据覆盖。
        // 这样就完成了数据按照索引的插入
        elementData[index] = element;
        // 元素插入后 数量加1 
        size = s + 1;
    }
    
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    // 这是一个本地方法,我们不能查看方法体,所以可以看一下官方的解释
    /*
    	将数组从指定的源阵列(从指定位置开始)复制到目标阵列的指定位置。
    	数组组件的子序列从 {@code src} 引用的源数组复制到 {@code dest} 引用的目标数组。
    	复制的组件数等于 {@code length} 参数。源数组中位置 {@code srcPos} 到 {@code srcPos+length-1} 的组件分别复制到目标数组的位置 {@code destPos} 到 {@code destPos+length-1} 中。
    	。。。。。。。。
    */
    // 个人理解就是将要插入索引后面的元素往后挪一位,将当前元素插入进来
    /*
    	参数说明:
    		src: 要被复制的元素
    		srcPos: 要开始复制的位置
    		src: 被赋值的元素
    		destPos: 复制到的位置
    		length: 要复制的长度
    	其实我们可以自己写一个数组的挪位置。回头有时间写写。
    */
    public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);
    
    

    把另一个集合的元素添加进来

    经过前面的分析 这里就很简单了。

    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        modCount++;
        int numNew = a.length;
        // 如果添加的集合元素为空,就不添加 返回false
        if (numNew == 0)
            return false;
        Object[] elementData;
        final int s;
        // 判断容量够不够,如果(当前容量减去控制大小的size 就是剩余没有用到的数组容量)小于新添加集合元素的数量,就进行扩容。
        if (numNew > (elementData = this.elementData).length - (s = size))
            elementData = grow(s + numNew);
        // 进行数组的拷贝 和上面一致
        // 就是把传入的数组全部复制到原来数组后面
        System.arraycopy(a, 0, elementData, s, numNew);
        // 复制完成 元素数量添加
        size = s + numNew;
        return true;
    }
    

    按照索引进行复制

    public boolean addAll(int index, Collection<? extends E> c) {
        // 对索引的正确性进行检查
        rangeCheckForAdd(index);
    	// 将添加的集合的元素转化为数组
        Object[] a = c.toArray();
        modCount++;
        int numNew = a.length;
        // 如果长度为0就不用添加了 直接返回false
        if (numNew == 0)
            return false;
        Object[] elementData;
        final int s;
        // 判断当前可用的容量够不够支持插入新的集合里面的元素
        if (numNew > (elementData = this.elementData).length - (s = size))
            // 不够的话就进行扩容
            elementData = grow(s + numNew);
    	// 对要移动元素的位置进行判断
        int numMoved = s - index;
        // 大于0,则代表在当前底层数组中进行插入
        if (numMoved > 0)
            // 进行数组元素的拷贝 ,简单来说就是把原数组挪出来可以容下新元素的位置
            System.arraycopy(elementData, index,
                             elementData, index + numNew,
                             numMoved);
        // 不大于0,代表在当前底层数组后面插入
        // 在这里进行新数组的数据覆盖
        System.arraycopy(a, 0, elementData, index, numNew);
        // 集合的大小扩大
        size = s + numNew;
        return true;
    }
    

  3. 删除元素

    根据索引删除元素

    public E remove(int index) {
        // 检查元素是否越界 这里很简单就不复制下面的代码了
        Objects.checkIndex(index, size);
        // 拿到当前数组的元素
        final Object[] es = elementData;
    	// 获取当前索引对应的元素
        @SuppressWarnings("unchecked") E oldValue = (E) es[index];
        // 这里是删除的核心代码,详情看下面
        fastRemove(es, index);
    	// 返回被删除的元素
        return oldValue;
    }
    
    /*
    	参数说明: 
    		param1: 源数组
    		param2: 要删除的索引位置
    */
    
    private void fastRemove(Object[] es, int i) {
        modCount++;
        // 删除一个元素后,剩余集合容量的大小
        final int newSize;
        // 如果是大于代表当前元素在底层数组的中间或者前面,否则在末尾
        if ((newSize = size - 1) > i)
            // 数组拷贝 和前面一样 不再分析
            System.arraycopy(es, i + 1, es, i, newSize - i);
        // 这里代表正好要删除的元素在末尾 直接赋值为null就行 等待GC的处理
        // 并且给size重新赋值大小
        es[size = newSize] = null;
    }
    

    根据元素删除

    // 这里其实也是调用了索引删除
    // 通过变量i拿到对应元素索引的位置,然后进行底层数组的覆盖(复制)
    /*
    	为什么这里写的那么复杂呢 因为ArrayList支持存储null,
    	当存储的元素为null的时候,寻找元素会空指针异常,所以分成了为null和不为null两个分流
    	这里也可以发现 这里的移除只会移除第一个符合的元素 后面的不会移除
    
    */
    public boolean remove(Object o) {
        final Object[] es = elementData;
        final int size = this.size;
        int i = 0;
        found: {
            if (o == null) {
                for (; i < size; i++)
                    if (es[i] == null)
                        break found;
            } else {
                for (; i < size; i++)
                    if (o.equals(es[i]))
                        break found;
            }
            return false;
        }
        fastRemove(es, i);
        return true;
    }
    
    /*
    	参数说明: 
    		param1: 源数组
    		param2: 要删除的索引位置
    */
    
    private void fastRemove(Object[] es, int i) {
        modCount++;
        // 删除一个元素后,剩余集合容量的大小
        final int newSize;
        // 如果是大于代表当前元素在底层数组的中间或者前面,否则在末尾
        if ((newSize = size - 1) > i)
            // 数组拷贝 和前面一样 不再分析
            System.arraycopy(es, i + 1, es, i, newSize - i);
        // 这里代表正好要删除的元素在末尾 直接赋值为null就行 等待GC的处理
        // 并且给size重新赋值大小
        es[size = newSize] = null;
    }
    
    

    image-20221231104832762


    按照条件删除

    // 说实话 并看不懂这个。。特别牵扯到位运算。。。。想不清楚为什么这么做。
    @Override
    public boolean removeIf(Predicate<? super E> filter) {
        return removeIf(filter, 0, size);
    }
    
    /**
         * Removes all elements satisfying the given predicate, from index
         * i (inclusive) to index end (exclusive).
         */
    boolean removeIf(Predicate<? super E> filter, int i, final int end) {
        Objects.requireNonNull(filter);
        int expectedModCount = modCount;
        final Object[] es = elementData;
        // Optimize for initial run of survivors
        for (; i < end && !filter.test(elementAt(es, i)); i++)
            ;
        // Tolerate predicates that reentrantly access the collection for
        // read (but writers still get CME), so traverse once to find
        // elements to delete, a second pass to physically expunge.
        if (i < end) {
            final int beg = i;
            final long[] deathRow = nBits(end - beg);
            deathRow[0] = 1L;   // set bit 0
            for (i = beg + 1; i < end; i++)
                if (filter.test(elementAt(es, i)))
                    setBit(deathRow, i - beg);
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            modCount++;
            int w = beg;
            for (i = beg; i < end; i++)
                if (isClear(deathRow, i - beg))
                    es[w++] = es[i];
            shiftTailOverGap(es, w, end);
            return true;
        } else {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            return false;
        }
    }
    
    

  4. 修改

    设计lambda表达式额真的不太好想,能力上还是差了太多。

    根据索引下标进行修改:

    public E set(int index, E element) {
        // 检查数组是否越界
        Objects.checkIndex(index, size);
        // 获取当前索引的元素
        E oldValue = elementData(index);
        // 将当前索引的值进行覆盖
        elementData[index] = element;
        return oldValue;
    }
    
    E elementData(int index) {
        return (E) elementData[index];
    }
    

  5. 查询元素

    // 这个很简单就不分析了
    public E get(int index) {
        Objects.checkIndex(index, size);
        return elementData(index);
    }
    
  6. 遍历元素

    // 使用增强for进行遍历的时候,会自动创建一个迭代器
    public Iterator<E> iterator() {
            return new Itr();
    }
    
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值