ArrayList集合源码浅分析【一篇到底】

一、定义

        ArrayList是Java中的一种常见的数据结构,它实现了List接口,是线程不安全的动态数组,也是我们常用的集合,它允许任何元素的插入,甚至包括null元素。

二、属性  

 //默认初始容量
private static final int DEFAULT_CAPACITY = 10;

 //空数组
private static final Object[] EMPTY_ELEMENTDATA = {};

 //默认容量的空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

 //集合真正存储数组元素的数组    transient:被修饰的变量不参与序列化和反序列化。
transient Object[] elementData;

 //集合的大小
private int size;

三、构造器分析

 1、无参构造器

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

        将一个默认容量的空数组赋值给elementData数组。当数组长度发生变化(add添加)时,该数组默认变成长度为10的数组。

 2、指定数组长度的有参构造器

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

        创建一个指定长度的ArrayList集合
        当 initialCapacity > 0 时,创建一个指定长度的Object数组并且将它赋值给elementData 。
        当 initialCapacity = 0 时,将空数组赋值给elementData 。
        当 initialCapacity < 0 时,抛出IllegalArgumentException(非法参数)异常。 

3、指定集合元素的有参构造器

//首先看toArray方法:将集合转数组的方法
public Object[] toArray() {
    //调用数组工具类方法进行拷贝
    return Arrays.copyOf(elementData, size);
}


public ArrayList(Collection<? extends E> c) {
    // 将集合构造中的集合对象转成数组,且将数组的地址赋值给elementData
    elementData = c.toArray();
    // 将elementData的长度赋值给 集合长度size,且判断是否不等于 0
    if ((size = elementData.length) != 0) {
        // 判断elementData 和 Object[] 是否为不一样的类型
        if (elementData.getClass() != Object[].class)
            //如果不一样,使用Arrays的copyOf方法进行元素的拷贝
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
      // 用空数组代替
      this.elementData = EMPTY_ELEMENTDATA;
    }
}

        将集合作为参数传入,首先对集合进行数组转化,转化成数组,然后判断是否长度大于0。

        如果 (size = elementData.length) == 0 ,则用空数组代替。

        如果 (size = elementData.length) != 0 ,进入到下一步判断。

        如果elementData与Object的类型不同,使用copyOf进行元素拷贝。

关于copyOf 与深拷贝、浅拷贝的问题,详见另一篇博客:

四、常用方法分析

1、添加方法

        1.1、add(E e) 方法

//将添加的数据传入给 e
public boolean add(E e) {
    //调用方法对内部容量进行校验 【扩容机制】
    ensureCapacityInternal(size + 1);
    //数组长度加1,并且把e赋值上去
    elementData[size++] = e;
    //返回添加成功
    return true;
}

        ArrayList的add方法其实很简单,只是在内部容量校验通过后对数组长度加1,并且将数据赋值到数组上就可以,但是难点在于容量校验与扩容机制上。

        1.2、ArrayList的扩容机制剖析

 详见另一篇博客:ArrayList 的扩容机制_再吃一口就不吃了~的博客-CSDN博客

        1.3、add(int index,E element) 方法

    //将元素添加到指定位置
    public void add(int index, E element) {
        //对参数进行判断,如果输入错误报错:数组下标越界异常
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        //内部容量校验,判断是否需要扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //将元素向后移动1位
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        //将元素添加到指定下标位置上
        elementData[index] = element;
        //最后对数组长度加1
        size++;
    }

        该方法在于指定位置添加元素,该位置之后的元素向后移动一位的操作。主要在于System.arraycopy() 方法的理解上。

        对于System.arraycopy 方法与Arrays.copyOf 方法单独出一个博客:

        1.4、addAll(Collection<? extends E> c) 方法

public boolean addAll(Collection<? extends E> c) {
    //把集合的元素转存到Object类型的数组中
    Object[] a = c.toArray();
    //记录数组的长度
    int numNew = a.length;
    //调用方法检验是否要扩容,且让增量++
    ensureCapacityInternal(size + numNew);
    //调用方法将a数组的元素拷贝到elementData数组中
    System.arraycopy(a, 0, elementData, size, numNew);
    //集合的长度+=a数组的长度
    size += numNew;
    //只要a数组的长度不等于0,即说明添加成功
    return numNew != 0;
}

        addAll()方法与上面两个有些区别在于:传入的参数不再是单个元素,而是一个包含0至多个元素的集合,首先要将集合转换成Object类型的数组,通过length获取数组的长度,再进行扩容校验,最关键的一步:将数组中的元素拷贝到elementData数组的后面,最后扩展elementData数组长度。返回值为boolean类型,【只要传入的集合不是null就返回true】。

        1.5、addAll(int index, Collection<? extends E> c) 方法

public boolean addAll(int index, Collection<? extends E> c) {
    //校验索引
    if (index > size || index < 0)
       throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    //将数据源转成数组
    Object[] a = c.toArray();
    //记录数据源的长度 3
    int numNew = a.length;
    //目的就是为了给集合存储数据的数组进行扩容
    ensureCapacityInternal(size + numNew);
    //numMoved:代表要移动元素的个数 --> 1个
    //numMoved: 数据目的(集合list1)的长度-调用addAll的第一个参数 (索引1)
    int numMoved = size - index;
    //判断需要移动的个数是否大于0
    if (numMoved > 0)
        //使用System中的方法arraycopy进行移动
        System.arraycopy(elementData, index, elementData, index + numNew,numMoved);
    //才是真正将数据源(list)中的所有数据添加到数据目的(lsit1)
    System.arraycopy(a, 0, elementData, index, numNew);
    size += numNew;
    return numNew != 0;
}

        与1.4的区别在是否是在指定位置添加集合元素。需要注意的是:当 size == index 的时候,两个 addAll() 方法没有任何区别。只有当 size - index > 0 的时候,才会移动原数组的元素。

2、删除方法

        2.1、remove(int index) 方法

public E remove(int index) {
    //范围校验
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    //增量++
    modCount++;
    //将index对应的元素赋值给 oldValue
    E oldValue = (E)elementData(index);
    //计算集合需要移动元素个数
    int numMoved = size - index - 1;
    //如果需要移动元素个数大于0,就使用arrayCopy方法进行拷贝
    //注意:数据源和数据目的就是elementData
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
        numMoved);
    //将源集合最后一个元素置为null,尽早让垃圾回收机制对其进行回收
    elementData[--size] = null;
    //返回被删除的元素
    return oldValue;
}

        remove(int index)方法:删除指定索引下标的元素。

        调用通过数组下标删除元素的方法首先对下标进行判定,数组下标越界会报错,下标符合后对数组修改次数加1,开辟一个变量接收指定下标元素的值,随后计算要移动的元素的个数,当 numMoved > 0 时,说明后面还有元素要移动,接下来通过System.arraycopy() 方法将数组拷贝,变成新的数组,最后将原集合最后一个元素设置为null,目的是为了尽早可以被垃圾回收机制回收。最后返回 oldValue【被删除的元素】。

        2.2、remove(Object o) 方法

public boolean remove(Object o) {
    //判断要删除的元素是否为null
    if (o == null) {
        //遍历集合
        for (int index = 0; index < size; index++)
            //判断集合的元素是否为null
            if (elementData[index] == null) {
                //如果相等,调用fastRemove方法快速删除
                fastRemove(index);
                return true;
            }
     } else {
       //遍历集合
       for (int index = 0; index < size; index++)
           //用o对象的equals方法和集合每一个元素进行比较
           if (o.equals(elementData[index])) {
                //如果相等,调用fastRemove方法快速删除
                fastRemove(index);
                return true;
           }
    }
    //如果集合没有o该元素,那么就会返回false
    return false;
}

        remove(Object o) 方法 :删除指定内容的第一个元素。

    当 o == null 时,遍历集合,当某一集合元素 == null 时,调用 fastRemove() 方法,返回 true 。

    当 o 为其他元素时,遍历集合,通过 equals 方法判断每个元素的值是否与 o 相等,如果相等就会调用 fastRemove() 方法并返回true 。

        当集合不存在这个元素的时候,便返回 false 。

        2.3、fastRemove(int index) 方法

private void fastRemove(int index) {
    //增量++
    modCount++;
    //计算集合需要移动元素的个数
    int numMoved = size - index - 1;
    //如果需要移动的个数大于0,调用arrayCopy方法进行拷贝
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
        numMoved);
    //将集合最后一个元素置为null,尽早被释放
    elementData[--size] = null;
}

        fastRemove(int index) 方法:删除元素方法,只是在2.2中会调用,它的作用在于:获取将要删除的指定的元素下标,通过下标再进行删除。是 remove(Object o)方法的核心功能 。

        2.4、removeRange(int fromIndex, int toIndex) 方法

//删除指定起始位置元素的方法
protected void removeRange(int fromIndex, int toIndex) {
    //校验开始位置是否小于结束位置 有问题报错
    if (toIndex < fromIndex) {
       throw new IndexOutOfBoundsException("toIndex < fromIndex");
    }

    //对集合的修改操作次数加1
    modCount++;
    //将elementData从toIndex位置开始的元素向前移动到fromIndex
    int numMoved = size - toIndex;
    System.arraycopy(elementData, toIndex, elementData, fromIndex,
                 numMoved);

    //将toIndex位置之后的元素全部置空,等待GC回收
    int newSize = size - (toIndex-fromIndex);
    for (int i = newSize; i < size; i++) {
        elementData[i] = null;
    }
    //修改size
    size = newSize;
}

        removeRange(int fromIndex, int toIndex) 方法:在 [ fromIndex - toIndex )范围内,所有元素都将被删除。

        它的执行过程是将elementData从toIndex位置开始的元素向前移动到fromIndex,然后将toIndex位置之后的元素全部置空顺便修改size。

注意:这个方法的权限是 protected ,受保护的方法,具体原因我还没搞懂。创建的对象没办法调用 removeRange 方法,但是我找到了它的平替

     对象.subList(fromIndex,toIndex).clear();     

如果我学会了出一篇博客,链接在这:

3、修改方法

        set(int index, E element) 方法

public E set(int index, E element) {
    //范围校验
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    //先取出index对应的元素,且赋值给oldValue
    E oldValue = elementData(index);
    //将element直接覆盖index对应的元素
    elementData[index] = element;
    //返回被覆盖的元素
    return oldValue;
}

        set(int index, E element) 方法:修改指定元素的元素值。

首先对 index 进行校验,通过后将该位置上原来的值赋值给 oldValue ,然后 把新值赋给elementData[index] 上,最后返回 oldValue 。

4、获取方法

        get(int index) 方法

public E get(int index){
    //首先对index进行校验
    if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    // 直接返回指定下标的数组的值
    return elementData[index];
}

        get(int index) 方法:源码很简单,直接 return elementData数组指定下标的值即可。

5、其他方法

        5.1、trimToSize() 方法

    public void trimToSize() {
        //修改数组次数加1
        modCount++;
        //判断数组元素个数是否小于数组长度
        if (size < elementData.length) {
            //三元运算符:如果没有元素,则设置成空数组,
            //反之,拷贝elementData成size大小的新数组,数组内容保持不变
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

        trimToSize() 方法:剔除未被利用的容量,使数组长度与元素个数相同。

        由于扩容机制,elementData的长度会被拓展,size标记的是其中包含的元素的个数。在这种情况下,内部数组中或许会有一些未分配的空间。trimToSize将返回一个新的数组给elementData,元素内容保持不变,length很size相同,节省空间。

        5.2、size() 方法

public int size() {
    // 直接返回size
    return size;
}

        size() 方法:返回elementData数组的实际长度。

        5.3、isEmpty() 方法

public boolean isEmpty() {
        // 直接返回 size == 0 的结果,没有元素返回true
        return size == 0;
    }

        isEmpty() 方法:判断elementData数组是否为空。

        5.4、indexOf(Object o) 方法

    public int indexOf(Object o) {
        //判断传入的元素是否为null
        if (o == null) {
            //遍历集合
            for (int i = 0; i < size; i++)
                //判断下标为 i 的元素是否为null
                if (elementData[i]==null)
                    //如果是,返回该元素的下标
                    return i;
        //传入的数据不为null
        } else {
            // 遍历集合
            for (int i = 0; i < size; i++)
                // 通过 equals 方法判断元素的值是否与传入的值相等
                if (o.equals(elementData[i]))
                    // 相等返回该元素的下标
                    return i;
        }
        // 如果集合中不存在该数据则返回 -1
        return -1;
    }

        indexOf(Object o) 方法:获取指定元素在此集合中第一次出现的下标。

        注意:在这个方法中比较 null 的时候,采用的是 == 比较,比较非 null 值的时候,采用的是 o1.equals(o2) 的方法进行比较,但是,在比较未重写 equals 方法的对象的时候,仍然比较的是引用值是否相等,并不是具体的值,之所以比较 String ,Array 等引用数据类型的时候会比较内容,是因为它们已经重写了 equals 方法,所以在比较自定义对象的时候,记得重写 equals 方法。

        5.5、lastIndexOf(Object o) 方法

    public int lastIndexOf(Object o) {
        if (o == null) {
            // 在这需要注意,从后往前数,起始值应当是size-1
            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;
    }

      lastIndexOf(Object o) 方法:获取指定元素在此集合中最后一次出现的下标。需要注意,从后往前数,起始值应当是size-1。

        5.6、contains(Object o) 方法

public boolean contains(Object o) {
    // 返回indexOf的结果,存在该元素返回true,不存在返回false
        return indexOf(o) >= 0;
    }

       contains(Object o) 方法:查询集合中是否存在该元素。返回 indexOf 的结果,存在该元素返回 true ,不存在返回 false 。

        5.7、clear() 方法

    public void clear() {
        //修改集合次数加1
        modCount++;

        // 遍历集合,将每个元素赋值为null,等待GC机制回收
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        //最后设置集合长度为0
        size = 0;
    }

         clear() 方法 :清除集合中的所有元素。

        5.8、subList(int fromIndex, int toIndex) 方法

    public List<E> subList(int fromIndex, int toIndex) {
        //进行参数校验
        subListRangeCheck(fromIndex, toIndex, size);
        // 返回截取内容
        return new SubList(this, 0, fromIndex, toIndex);
    }

    //参数校验方法
    static void subListRangeCheck(int fromIndex, int toIndex, int size) {
        if (fromIndex < 0)
            throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
        if (toIndex > size)
            throw new IndexOutOfBoundsException("toIndex = " + toIndex);
        if (fromIndex > toIndex)
            throw new IllegalArgumentException("fromIndex(" + fromIndex +
                                               ") > toIndex(" + toIndex + ")");
    }

        subList(int fromIndex, int toIndex) 方法:截取并返回 [ fromIndex , toIndex )的元素 。

        5.9、clone() 方法

ArrayList类中的 clone 方法 主要继承于 Object 类
    public Object clone() {
        try {
            //调用父类的clone方法返回一个对象的副本
            ArrayList<?> v = (ArrayList<?>) super.clone();
            //将返回对象的elementData数组的内容赋值为原对象elementData数组的内容
            v.elementData = Arrays.copyOf(elementData, size);
            //将副本的modCount设置为0
            v.modCount = 0;
            //返回副本
            return v;
        } catch (CloneNotSupportedException e) {
            // 报错
            throw new InternalError(e);
        }
    }

private native Object internalClone();
Object类中的 clone 方法

    protected Object clone() throws CloneNotSupportedException {
        //判断该对象是否是支持克隆的,是否实现了克隆的接口
        if (!(this instanceof Cloneable)) {
            throw new CloneNotSupportedException("Class " + getClass().getName() +
                                                 " doesn't implement Cloneable");
        }

        //返回内部克隆的参数
        return internalClone();
    }

        clone()方法:用来生成一个当前ArrayList的克隆对象,它与原始对象具有相同的元素和容量,并且是独立的对象。

五、需要注意的细节和常见问题汇总

       1、ArrayList 每次扩容的容量?

       第一次扩容:10,以后的每次扩容:原容量的1.5倍。

       2、ArrayList 是线程安全的么?

         ArrayList 本身不是线程安全的,比如 add 方法执行时首先会执行容量校验,必要时还需要扩容,在多个线程进行 add 操作时可能会导致 elementData 数组越界。

    解决线程不安全的方法有三种:

1、使用 Vector

   写法:ArrayList<String> list = newVector<>();

        Vector大部分方法和ArrayList都是相同的,只是加上了synchronized关键字,这种方式严重影响效率,因此,不再推荐使用Vector了 。

2、使用 Collections 里面的 synchronizedList

   写法:ArrayList<String> list =Collections.synchronizedList(new ArrayList<>());

        这个方法可以实现线程安全,但是迭代器未加锁,需要手动实现同步。

3、使用 CopyOnWriteArrayList

   写法:ArrayList<String> list = new CopyOnWriteArrayList();

        3、如何复制某个 ArrayList 到另一个 ArrayList 中?

        1、使用 clone() 方法

        2、使用 ArrayList 构造方法

        3、使用 addAll 方法

           4、ArrayList 和 LinkList 区别?

ArrayList
        基于动态数组的数据结构对于随机访问的get和set,ArrayList要优于LinkedList 。对于随机操作的add和remove,ArrayList不一定比LinkedList慢 (ArrayList底层由于是动态数组,因此,并不是每次add和remove的时候都需要创建新数组)

LinkedList
        基于链表的数据结构
        对于顺序操作,LinkedList不一定比ArrayList慢
        对于随机操作,LinkedList效率明显较低

            5、为什么ArrayList中的clone方法不是继承AbstractList类,而是继承Object类的clone呢?

        ArrayList继承于AbstractList类,但是AbstractList类中不存在clone方法,所有类的总类是Object类,所以ArrayList继承的clone方法是Object类中的clone方法。

            6、Arrays.copyOf,这个代码有什么作用?

        Arrays.copyof是用于数组进行复制时常使用的方法,将原数组的内容复制到一个新数组中,新数组后期的改动对原数组不会产生任何影响。但是从源码上看其实质是调用了System.arraycopy方法。

            7、浅拷贝的局限性是什么?

        浅拷贝对于基本数据类型拷贝是深拷贝,也就是两个数组之间是毫无影响的。但当新数组拷贝原数组引用类型的元素时,引用数据类型只是复制了引用值,当原数组或者新数组更改的时候,另一个相应的引用元素也会随之更改。

            8、为什么 LinkedList 随机访问比顺序访问要慢这么多?

        因为 ArrayList 实现了 RandomAccess 接口,而 LinkedList 没有实现 RandomAccess 接口。

通过源码分析:
    由于随机访问的时候底层每次都需要进行折半的动作,再判断是从头还是从尾部一个个寻找,比较耗时。
    而顺序访问只会在获取迭代器的时候进行一次折半的动作,以后每次都是在上一次的基础上获取下一个元素。
    因此顺序访问要比随机访问快得多。

             9、在遍历集合取出结果集之前面临一个问题,使用普通for遍历好 还是使用迭代器(增强for)?

数据量特别大的时候一定要考虑
对返回的集合进行判断,如果返回的集合实现了 RandomAccess 就使用 普通for
否则使用迭代器(增强for)

//判断是否实现了 RandomAccess 接口
if(list instanceof RandomAccess){
    //如果实现了就用普通 for 循环
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}else {    
    // 如果没实现就使用 增强 for 循环
    for (Stutb stutb : list) {
        System.out.println(stutb);
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值