ArrayList源码深度解析

今天分析ArrayList源码:

简单介绍:

简介
ArrayList实现了List接口,继承了AbstractList,底层是数组实现的,一般我们把它认为是可以自增扩容的数组。它是非线程安全的,一般多用于单线程环境下(与Vector最大的区别就是,Vector是线程安全的,所以ArrayList 性能相对Vector 会好些),它实现了Serializable接口,因此它支持序列化,能够通过序列化传输(实际上java类库中的大部分类都是实现了这个接口的),实现了RandomAccess接口,支持快速随机访问(只是个标注接口,并没有实际的方法),这里主要表现为可以通过下标直接访问(底层是数组实现的,所以直接用数组下标来索引),实现了Cloneable接口,能被克隆。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{

继承图:

重要特点:

缺点:
容量受限时,需要进行数据扩容,进行元素拷贝会影响性能
2.频繁删除和往中间插入元素时,产生元素挪动,也会进行元素拷贝
3.不是线程安全的。
优点:
随机访问某个元素,性能很好;

我们从其重点的几个点分析:

一、重要参数和构造方法解析:

    private static final long serialVersionUID = 8683452581122892189L;


    private static final int DEFAULT_CAPACITY = 10;//首次添加元素,数组默认大小

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

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

    transient Object[] elementData;//底层定义的数组,同时 transient关键字修饰的字段是不能够被序列化的

    private int size;//list长度

    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE;//list的最大容量: 2^32-1

    //有参构造函数
    public ArrayList(int initialCapacity){
        if(initialCapacity > 0){
            this.elementData = new Object[initialCapacity];
        }else if(initialCapacity == 0){
            this.elementData = EMPTY_ELEMENTDATA;
        }else {
            throw new IllegalArgumentException("Ilegal capacity: "+initialCapacity);
        }
    }

    //无参构造函数,初始化未空
    public ArrayList(){
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

 

    //有参构造函数,初始化时就放一个集合
    public ArrayList(Collection<? extends E> c){
        elementData = c.toArray();//重点代码,详细分析
        if((size = elementData.length) != 0){
            if(elementData.getClass() != Object[].class){
                //核心代码,一会重点分析:意思是把数据通过操作系统正式赋值,很多方法都会用到
                elementData = Arrays.copyOf(elementData,size,Object[].class);
            }
        }else {
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

点击: c.toArray();方法:

public Object[] toArray() {
    return Arrays.copyOf(elementData, size);//点击进入
}

点击 Arrays.copyOf (elementData, size);方法:浅复制

public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}

点击: Arrays.copyOf(elementData,size,Object[].class);方法:

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    //获得者数组对象
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));//调用系统的方法增加数组容量,核心代码,详细分析
    return copy;
}

二、扩容机制

数组的扩容是新建一个大容量(原始数组大小+扩充容量)的数组,然后将原始数组数据拷贝到新数组,然后将新数组作为扩容之后的数组。数组扩容的操作代价很高,我们应该尽量减少这种操作。

我们从添加元素的接口中分析扩容机制:

1、假如初始化无参构造方法后,调用 add(E e)方法 ,首次添加元素:

public boolean add(E e) {
    //确保容量够用,点击进入
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;//此处直接赋值
    return true;
}

点击:ensureCapacityInternal(size + 1); 方法:

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        //首次添加时数组为空,容量初始化为 minCapacity = 10
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);//确保明确的容量,点击进入:minCapacity = 10
}

点击:ensureExplicitCapacity(minCapacity);方法:

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;//表示修改的次数,继承父类的参数,后面会详细分析

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)//10 - 0 > 0;进入判断
        grow(minCapacity);//核心代码,点击进入
}

点击:grow(minCapacity);进入核心扩容方法

/**
 * 很明显,oldCapacity的值为elementData数组的长度为0,newCapacity的值为1.5倍的oldCapacity,
 * 所以newCapacity值也为0,而minCapacity为10,所以第一个if条件成立,于是newCapacity = 10;
 * 最后还执行elementData = Arrays.copyOf(elementData, newCapacity); 这是grow方法的另一个核心步骤,数组的拷贝;
 * 从脉络上看,这里的Array.copyOf它实参是elementData和newCapacity=10。可以看到他的内部直接调用了一个重载方法,
 * 在重载方法里,首先是一个三元表达式和一个System.arraycopy方法调用。
 * @param minCapacity
 */
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // 扩容至原来的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 再判断一下新数组的容量够不够,够了就直接使用这个长度创建新数组,
    // 不够就将数组长度设置为需要的长度
    if(newCapacity - minCapacity < 0){
        newCapacity = minCapacity;
    }
    //若预设值大于默认的最大值检查是否溢出
    if(newCapacity - MAX_ARRAY_SIZE > 0){
        newCapacity = hugeCapacity(minCapacity);
    }

    // 调用Arrays.copyOf方法将elementData数组指向新的内存空间时newCapacity的连续空间
    // 并将elementData的数据复制到新的内存空间
    /**
     *
     * 点击进入发现:System.arraycopy(original, 0, copy,0,Math.min(original.length, newLength));
     * 这句代码应该是操作了original和copy的样子。
     * 仔细分析,elementData的class是Object[].class的类型,可以看出三元表达式会执行(T[]) new Object[newLength]。
     * 接着就会执行System.arraycopy这个方法,你可以查阅下JDK的API,它的主要作用是数组的拷贝。原来ArrayList的缺点原因在这里。
     */
    elementData = Arrays.copyOf(elementData,newCapacity);//这里上面已经分析
}

点击 hugeCapacity(minCapacity);方法:

/**
 * 然后还要再进行一步判断,即判断当前新容量是否超过最大的容量 if (newCapacity - MAX_ARRAY_SIZE > 0),
 * 如果超过,则调用hugeCapacity方法,传进去的是minCapacity,即新增元素后需要的最小容量:
 * @param minCapacity
 * @return
 */
private static int hugeCapacity(int minCapacity){
    if(minCapacity < 0){
        throw  new OutOfMemoryError();
    }else {
        //如果minCapacity大于MAX_ARRAY_SIZE,则返回Integer的最大值。否则返回MAX_ARRAY_SIZE。
        return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
    }

}

 如果创建ArrayList不指定大小,ArrayList第一次添加元素时,会指定默认的容量大小为10。

可以发现,当添加开始的时候,如果大小不够就会进入grow方法,会进行一次1.5倍的扩容,所以代码规范一般建议我们要制定ArrayList的大小,如果不指定,可能会造成频繁的扩容和拷贝,导致性能低下。业界测试结果如下:

1千需要分配 11次

1万一级需要分配17次

10万 需要分配23次

100万需要分配28次

创建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、从 add(int index,E e)方法开始分析:

public void add(int index,E e){
    rangCheckForAdd(index);
    ensureCapacityInternal(size + 1);
    /**
     *  
     * src:源数组;elementData
     * srcPos:源数组要复制的起始位置;index
     * dest:目的数组;elementData
     * destPos:目的数组放置的起始位置;index + 1
     * length:复制的长度。size - index
     * 注意:src and dest都必须是同类型或者可以进行转换类型的数组.
     */
    System.arraycopy(elementData,index,elementData,index + 1,size - index);
    elementData[index] = elementData;
    size ++;
}
2、下面进入native方法:

/**
 * public static void arraycopy(Object src,int srcPos,Object dest,int destPos,int length)
 *
 * src:源数组; 
 * srcPos:源数组要复制的起始位置; 
 * dest:目的数组;elementData
 * destPos:目的数组放置的起始位置; 
 * length:复制的长度。 
 * 注意:src and dest都必须是同类型或者可以进行转换类型的数组.
 */ 
public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);

3、详细解释:

如果难以理解这个方法的代码,可以举个例子:
假设有两个数组src和dest,他们都有5个元素0,1,2,3,4,即src[0,1,2,3,4] dest[0,1,2,3,4]
问题:执行System.arraycopy(src, 3, dest, 1, 2)后是什么意思?
答案:就是从src源数组3位置开始,移动两个元素到dest数组1开始覆盖,结果就是dest变成[0,3,4,3,4]
所以  System.arraycopy(elementData,index,elementData,index + 1,size - index);

就很好理解,就是从elementData位置index开始移动(size - index)个元素到elementData数组,从elementData数组,index + 1位置开始覆盖,返回了新创建的数组T[] copy ,这个数组的长度为。

或者  System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength));

也很好理解,就是从original位置0开始移动Math.min(original.length, newLength)(此时为0)个元素到copy数组,从copy数组0位置开始覆盖,等于什么也没有拷贝,直接返回了新创建的数组T[] copy ,这个数组的长度为10。
 

四、 深复制、浅复制区别:

 Arrays.copyOf 是浅复制

浅复制:只复制一个对象,但新对象和老对象同是一个地址值,

    深复制:复制一个对象,新老对象的地址值也变了.

    详情请看(要了解什么是浅复制):参考 深浅复制
 

五、删除方法

1、按照索引删除:

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; // clear to let GC do its work  置空末尾元素,便于GC回收内存

    return oldValue;
}

2、按照元素值来删除:

public boolean remove(Object o) {
   
    if (o == null) { // 值为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;
}
 

进入fastRemove(index);//正式删除方法:

private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)//数组拷贝操作
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}
 

3、无参 remove操作:

public void remove() {
    // 如果数组中没有元素
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification(); // 校验迭代过程中,数组是否有结构性修改

    try {
        
        ArrayList.this.remove(lastRet);// 调外部类remove,删除当前迭代元素
        cursor = lastRet;  // 因为是删除操作,迭代索引位置不变   
        lastRet = -1;// -1表示元素已经被删除      
        expectedModCount = modCount;// 修改期望modCount值
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}
 

六查询

1、获取索引位置元素

public E get(int index) {
    rangeCheck(index);// 边界校验,index大于等于size,抛出IndexOutOfBoundsException
    return elementData(index);
}

E elementData(int index) {
    return (E) elementData[index];
}
 

 

七、修改元素

1、更新指定索引位置元素

public E set(int index, E element) {
    
    rangeCheck(index); // 边界校验

    E oldValue = elementData(index);
    elementData[index] = element;//赋予新值
    return oldValue;
}
 

八、modCount的作用

源码查阅:

public Iterator<E> iterator() {
        return new Itr();
    }

/**
     * An optimized version of AbstractList.Itr
     */
    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
         // 这里将 modCount 赋值给了 expectedModCount
        int expectedModCount = modCount;
 
        Itr() {}
 
        public boolean hasNext() {
            return cursor != size;
        }
 
        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
 
        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
 
            try {
                this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
 
        final void checkForComodification() {
            // 若修改后 modCount会变化 会与 expectedModCount 数值不相等
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

看源码可以得出获取迭代器时会将 expectedModCount 赋值为 modCount, 若在使用迭代器迭代期间修改列表则会导致两者不相等,调用next()时会进行checkForComodification检查抛异常。说明迭代时不可以进行修改操作。

modCount主要目的就是用来限制用户在迭代时修改列表,造成数据错乱。

现在对modCount和expectedModCount的作用应该非常清楚了。在对一个集合对象进行跌代操作的同时,并不限制对集合对象的元素进行操 作,这些操作包括一些可能引起跌代错误的add()或remove()等危险操作。在AbstractList中,使用了一个简单的机制来规避这些风险。 这就是modCount和expectedModCount的作用所在
 

可参考文献:https://blog.csdn.net/zhangqiluGrubby/article/details/80337748

https://blog.csdn.net/lizhongping00/article/details/103790652

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寅灯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值