ArrayList深入源码

Java容器专栏: Java容器源码详细解析(面试知识点)

这里仅作ArrayList的源码解析,不进行与LinkedList的查、插、删等常见的性能对比。也不对List接口的有序可重复允许null值等做过多的说明。既然看源码了,想必这些都懂了,我就不必再赘述了。

详细说明都在源码里,耐下心看吧,其实挺简单的。

(一)ArrayList底层数据结构

可调整大小的动态数组。

 

(二)ArrayList继承关系

除了实现List接口外,还实现了下面三个接口:

1、Serializable标记性接口:可序列化

2、Cloneable标记性接口:可克隆

3、RandomAccess标记性接口:可快速随机访问

      主要目的是允许通过算法更改其行为,以便在应用于随机访问列表或顺序访问列表时提供良好的性能。简单体现在:

对于ArrayList的遍历
//随机遍历
for(int i=0, n = list.size(); i < n; i++){
    list.get(i);
}

上面的随机遍历,效率要比下面的迭代器遍历效率要高!
//顺序遍历
for(Iterator iterator = list.iterator(); i.hasNext(); ){
    i.next();
}

ArrayList基于数组实现,带下标,随机访问复杂度为O(1)

LinkedList基于链表实现,随机访问需要依靠遍历实现,复杂度为O(n)

当一个List拥有快速访问功能时,其遍历方法采用for循环最快速。而没有快速访问功能的List,遍历的时候采用Iterator迭代器最快速

ArrayList使用for循环遍历快,而LinkedList使用迭代器快。因为ArrayList实现了RandomAccess接口,而LinkedList没有。

当我们不明确获取到的是Arraylist,还是LinkedList的时候,我们可以通过RandomAccess来判断其是否支持快速随机访问

if(list instanceof RandomAccess){
    //随机访问 
     for(int i=0, n = list.size(); i < n; i++){
        list.get(i);
     }
}else{
    //顺序访问
    for(Iterator iterator = list.iterator(); i.hasNext(); ){
        i.next();
    }
}

ArrayList除了实现上面三个接口外,还继承了抽象类AbstractList:该类实现了List接口的骨架实现。

 

(三)ArrayList源码分析

1、几个重要的成员变量

private static final int DEFAULT_CAPACITY = 10; // 默认容量为10
 
private static final Object[] EMPTY_ELEMENTDATA = {};   // 空实例数组
 
// 默认大小的空实例数组,最初和上面的空实例数组是一样的
//区别:若是默认的,在第一次调用add方法才会扩容至DEFAULT_CAPACITY(10) 
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};   
 
transient Object[] elementData; // 实际存放元素的数组
 
private int size;   // 数组当前实际的元素个数

上面的 size 是指 elementData 中实际有多少个元素,

elementData.length 为List的容量,表示最多可以容纳多少个元素

//可以通过反射获取到elementData(即ArrayList)的实际容量
try{
    Field field = list.getClass().getDeclaredField("elementData");
    field.setAccessible(true);
    
    Object[] obj =(Object[]) field.get(list);
    System.out.println(obj.length);
}catch(Exception e){
    e.printStackTrace();
}

 

2、三个构造方法

// 根据创建具有指定初始容量的ArrayList
public ArrayList(int initialCapacity){
    if (initialCapacity > 0) {
        //如果指定容量 > 0,则直接为elementData创建相应大小的Object数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        //如果指定容量为0,则elementData赋值为成员变量中的空实例数组(长度为0)
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
// 创建一个默认的ArrayList
public ArrayList(){
    //将elementData赋值为默认大小的空实例数组(长度为10)
    //注意刚开始也只是空数组,只有在第一次调用add是,判断若为默认的创建方式
    //才会扩容(grow)成长度为10的数组
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 根据其他集合来创建ArrayList
public ArrayList(Collection<? extends E> c){
    //将其他容器转换成数组赋值给elementData
    elementData = c.toArray();
    
    //如果长度不为0
    if ((size = elementData.length) != 0) {
        // c.toArray可能不会返回Object[]类型,这里保险处理,保证
        if (elementData.getClass() != Object[].class)
            //拷贝size大小的elementData到elementData中,类型为Object[]
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        //如果长度为0,则赋值为实例空数组(长度为0)
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

 

3、添加元素操作

//添加一个元素
public boolean add(E e) {
    //每次添加元素到集合中时都会先确认下集合容量大小
    ensureCapacityInternal(size + 1);  
    //然后将 size 自增 1并赋值
    elementData[size++] = e;
    return true;
}

//确保内部容量足够(充足或扩容)
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

//计算容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    //在这里判断!!若为默认创建的,则返回DEFAULT_CAPACITY(10)作为容量
    //这里最多进去一次,其他的就都是下面直接返回传入进来的 “size+1”
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
    //对list的修改次数(每次add和remove都会增加,是AbstractList抽象类的成员变量)
    modCount++;
    // 如果容量不足,则
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

//扩容方法   minCapacity:最小满足容量
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    //默认将扩容至原来容量的 1.5 倍
    //  >>位运算,右移一位代表oldCapacity / 2,位运算效率更高
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //若扩容1.5倍后仍不够,就直接将容量设置为minCapacity (普通的add方法传入的是size+1)
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    
    //如果扩容后,大于数组的最大长度
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        //调用此方法设置合适的容量大小
            //若最小满足容量小于MAX_ARRAY_SIZE,则扩容到MAX_ARRAY_SIZE(值为Integer.MAX_VALUE - 8)
            //若最小满足容量已经大于MAX_ARRAY_SIZE了,则扩容到Integer.MAX_VALUE(值为0x7fffffff)
        newCapacity = hugeCapacity(minCapacity);
   
     // 将原数组中的数据复制到大小为 newCapacity 的新数组中,并将新数组赋值给 elementData。
    elementData = Arrays.copyOf(elementData, newCapacity);
}

还有其他添加元素的方法

//在指定下标出设置元素,下标原本及其后面的元素向后移动。(不覆盖)
//注意:index不能超过size,即最多可以插入到最后一个元素的下一个位置中
public void add(int index, E element) {
    //检查index是否合理(不能超过size,当然也不能小于0)
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  
    //index之后的元素向后移一格
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

//故名思义,将传入的容器添加到list中
public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew); 
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

……

 

4、删除元素操作

//根据下标删除元素,返回被删除的对象
public E remove(int index) {
    //判断下标(只过滤了超过size的情况)
    /*
    //    为什么不判断index为负数的情况?
    //        因为在下面elementData(index)进行了负数判断了。
    */
    rangeCheck(index);

    //增加修改次数
    modCount++;
    //获取到指定元素(返回值)
    E oldValue = elementData(index);

    //计算需要移动的元素个数
    int numMoved = size - index - 1;
    //如果存在需要移动的元素(因为如果删除的是最后一个元素,就不需要移动了)
    if (numMoved > 0)
        /*参数含义:
            第一个参数:原数组
            第二个参数:从原数组的此下标开始
            第三个参数:目标数组
            第四个参数:从目标数组的此下标开始
            第五个参数:copy的长度
        */
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    
    //最后一个元素位置设置为null,可以被GC回收                     
    elementData[--size] = null; 

    //返回被删除的元素
    return oldValue;
}
//移除指定的对象,返回boolean表示是否移出成功
//可以移除null
//且每次移除都是按照下标从小到大遍历,遇到的第一个“相同”对象就移除,也仅移除一个
//也就是如果list中有多个“hello”字符串,调用此方法移除,仅移除第一个“hello”
public boolean remove(Object o) {
    //null和非null对象的判断是否相同的方法不一致
    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;
}


//是第一个remove方法简化版,不需要判断下标,也不需要返回值
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
}

 

5、获取元素操作

//没啥好所的,底层是数组,就直接数组+下标获取(数组随机访问)
public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

 

6、其他较重要方法

//将list的容量缩减到和size相同。(数组填满)
public void trimToSize() {
    //同样增加修改次数
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA    //如果size为0,则赋值为EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);    //否则直接复制size大小
    }
}
//set区分于add(int index, E element)
//set是覆盖,add是插入(可能需要后移)
public E set(int index, E element) {
    rangeCheck(index);
    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

 

(四)线程安全性

ArrayList不是线程安全的。

举例:

比如在add方法中,需要执行ensureCapacityInternal(size + 1)判断容量是否足够。以容量为10,已存9个元素(size==9)为例,那么如果是多线程同时add,有可能线程A、B同时获取到size=9,都判断不需要扩容,就直接存进去,结果可想而知啦,第二个存的线程就报数组越界异常ArrayIndexOutOfBoundsException了。

 

(五)fail-fast机制

在上面的源码中我们可以看到经常出现modCount++这样的代码。其实,modCount是用来实现fail-fast机制的。modCount 用来记录 ArrayList 结构发生变化(可变操作)的次数,包括增加、删除元素以及数组容量的压缩、扩容。

无论是多线程操作共享ArrayList还是单线程中,只要在进行序列化或者使用迭代器进行迭代遍历等操作时,每次需要比较操作前后 modCount 是否改变,如果改变了需要抛出异常ConcurrentModificationException。

//抛异常(经过测试,若移除的是倒数第二个元素,则不会抛出异常,我也不懂,希望大家能够指教!)
public class Test{
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");

        Iterator<String> iterator = list.listIterator();
        while (iterator.hasNext()) {
            String str = iterator.next();
            if (str.equals("a")) {
                list.remove(str);
            }
        }
    }
}    

如何避免?

调用Iterator的remove方法,而不是调用Arraylist的remove方法。(Iterator中的remove方法移除后会把modCount重写赋值给expectedModCount,下一个循环时expectedModCount与modCount相等。)

 

(六)ArrayList序列化机制

上面我们说到ArrayList实现了序列化接口Serializable,说明是可以被序列化的。但是同时我们在ArrayList源码中的成员变量中发现,存放实际元素的数组elementData是被transient修饰的,说明它不被序列化。

但是实际测试结果证明,ArrayList能够被序列化,同时elementData也能够经过反序列化后获取到。为什么明明用transient修饰还是能够被序列化?加上transient又是为了什么?

其实默认的序列化,调用的是ObjectOutputStream 的 defaultWriteObject( )以及 ObjectInputStream 的 defaultReadObject( )。但是ArrayList类中定义了writeObject()readObject ()。序列化过程中,虚拟机允许类自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程。

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // 上面提到的fail-fast机制
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // 序列化时不是直接写入按照容量大小,而是实际元素个数
    s.writeInt(size);

    // 循环写入元素
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    //fail-fast检错机制
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}


private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    //先把elementData赋值为空数组
    elementData = EMPTY_ELEMENTDATA;
    //读出所有数据
    s.defaultReadObject();

    //这个ignored了
    s.readInt(); 

    if (size > 0) {
        // 类似于克隆,size大小
        int capacity = calculateCapacity(elementData, size);
        SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
        ensureCapacityInternal(size);

        Object[] a = elementData;
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}

通过查看源码,很明显ArrayList不会走序列化的默认方法,而是走自定义的方法。

因为elementData经过扩容后,有可能后面有很大的空间都没有存放元素,若按照默认的序列化方式,则这些空间也需要同时进行序列化保存,浪费空间。

所以elementData修饰为transient,不能进行序列化,然后通过自定义的序列化方法,类似于trimToSize()方法的效果,序列化的数组长度等于实际元素个数

 

(七)ArrayList的clone机制

浅拷贝,同样的是根据实际元素个数进行拷贝,不是根据容量。

public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        //复制elementData实际元素个数size
        v.elementData = Arrays.copyOf(elementData, size);
        //默认修改次数为0
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // 这个不可能发生,因为实现了Cloneable
        throw new InternalError(e);
    }
}

 

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值