ArrayList源码分析

在分析动态数组ArrayList的源码之前先对其底层数据结构——数组进行分析

数组

数组是一种最基本的数据结构,采用了一组连续的内存空间按顺序存储对象或基本数据类型。其访问的时间复杂度为O(1),插入以及删除由于涉及到元素的移动时间复杂度为O(n),在Java中声明一个数组需要事先指定数组的大小,那么就可能会造成空间浪费以及扩容问题,为了解决这些问题,动态数组ArrayList就诞生了。

ArrayList

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

(1)实现了RandomAccess接口,标识其具备随机访问功能
(2)实现了List,提供了List基础的增,删,改,查等操作
(3)实现了Cloneable,可以被克隆
(4)实现了Serializable,可以被序列化

属性

    /**
     * 默认的容量大小为10
     * Lazy-load: 只有在ArrayList真正添加元素的时候才会通过DEFAULT_CAPACITY创建数组, 
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 当指定ArrayList容量为0的时候, 底层使用的空数组
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 调用默认构造函数ArrayList()时,使用这个空数组作为elementData
     * 与EMPTY_ELEMENTDATA的区别在于: DEFAULTCAPACITY_EMPTY_ELEMENTDATA在第一次添加元素时会变为长度为DEFAULT_CAPACITY的数组
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 真正存储元素的数组
     * 标记了transient,此字段不会被序列化
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * 容器中元素的个数
     */
    private int size;

构造函数

	/**
	* 1. 指定容器大小初始化ArrayList
	*/
	public ArrayList(int initialCapacity) {
	  //若传入初始Capacity > 0, 那么就根据Capacity创建对应长度的Object数组
	  if (initialCapacity > 0) {
	      this.elementData = new Object[initialCapacity];
	  //若传入初始Capacity = 0, 那么就将EMPTY_ELEMENTDATA这个空数组赋给elementData
	  } else if (initialCapacity == 0) {
	      this.elementData = EMPTY_ELEMENTDATA;
	  //若传入初始Capacity < 0, throw Exception
	  } else {
	      throw new IllegalArgumentException("Illegal Capacity: "+
	                                         initialCapacity);
	  }
	}
	
	/**
	* 2. 默认构造函数
	*/
	public ArrayList() {
	  //将DEFAULTCAPACITY_EMPTY_ELEMENTDATA这个空数组赋给elementData
	  this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
	}
	
	/**
	* 3. 通过传入一个集合初始化ArrayList
	*/
	public ArrayList(Collection<? extends E> c) {
	  //把传入的集合元素转换为数组拷贝给elementData
	  elementData = c.toArray();
	  //检查拷贝之后, elementData的长度是否为0
	  if ((size = elementData.length) != 0) {
	      // c.toArray或许不能正确地转换为Object数组
	      if (elementData.getClass() != Object[].class)
	      	  //若没有正确转换为Object[], 重新拷贝成Object[].class类型再赋给elementData
	          elementData = Arrays.copyOf(elementData, size, Object[].class);
	  } else {
	      // 若拷贝之后elementData长度为0, 那么直接将EMPTY_ELEMENTDATA赋给elementData
	      this.elementData = EMPTY_ELEMENTDATA;
	  }
	}

针对c.toArray不能正确转换为Object数组,可以参考如下实例:

	String[] str = new String[]{"wang", "han", "lin"};
    List<String> asListTemp = Arrays.asList("wang", "han", "lin");
    //asListTemp.remove(0);     // UnsupportedOperationException
    //str[0] = "wang1998";      // 那么temp.get(0)也会随之被修改
    List<String> temp = new ArrayList<>(asListTemp);

这是因为asListTemp仍然是Arrays的内部类对象,asList()并没有实现对集合的修改,这里只是转换了接口,但后台数据仍然是String数组。因此在遇到if (elementData.getClass() != Object[].class)这段代码时必须要通过Arrays.copyOf进行赋值。
顺便一提,使用Arrays.asList()转换的集合并不能对其进行修改相关的操作,这类操作会抛出UnsupportedOperationException异常,但是在原数组之上进行修改是可行的。这里体现的是适配器模式,只是转换接口,后台数据不变。

add(E e) 添加元素到数组末尾

(1)明确添加元素后数组所需要的最小长度minCapacity
(2)如果minCapacity > elementData.length,需要进行数组扩容
(3)在末尾处添加元素
(4)添加成功return true

	/**
     * 添加元素到数组末尾, 时间复杂度为O(1)
     */
    public boolean add(E e) {
    	//确保ArrayList有足够的Capacity添加新的元素
        ensureCapacityInternal(size + 1);
        //把元素插入到elementData的末尾元素之后
        elementData[size++] = e;
        return true;
    }
	
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
	
	private static int calculateCapacity(Object[] elementData, int minCapacity) {
		//如果当前ArrayList处于刚初始化的状态, 就返回默认长度10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //如果ArrayList已经处于正在使用的状态(已经进行了添加操作), 直接返回传入的minCapacity
        return minCapacity;
    }
	
	
	private void ensureExplicitCapacity(int minCapacity) {
        modCount++;//modCount用于记录操作ArrayList的次数
        
        // 如果elementData的长度小于添加元素所需要的minCapacity, 需要对原数组进行扩容操作
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
	
	private void grow(int minCapacity) {
        //原数组长度
        int oldCapacity = elementData.length;
        //新数组的长度 = 原数组长度的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果新数组的长度依然小于minCapacity, 那么以minCapacity为准进行数组扩容
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //若新数组长度已经超过ArrayList规定的最大长度, 则使用最大长度
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //底层是根据newCapacity创建了一个新的数组, 然后在这个新数组之上添加原数组的元素
 		//将新数组赋值给elementData
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

add(int index, E element) 在指定位置添加元素

(1)检查index是否越界
(2)检查是否需要扩容
(3)右移index以及后续所有元素
(4)index位置插入新元素
(5)size++

	/**
     * 在指定位置插入元素, 并将原本在该位置的元素以及右边的所有元素向右移动一位
     */
    public void add(int index, E element) {
    	//检查index是否越界
        rangeCheckForAdd(index);
		//确保ArrayList有足够的Capacity添加新的元素
        ensureCapacityInternal(size + 1);
        //移动原本在该位置的元素以及右边的所有元素
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        //在index位置插入元素
        elementData[index] = element;
        size++;
    }
	
	/**
     * 确保index合法
     */
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

addAll(Collection c) 在末尾追加集合c中的所有元素

(1)将集合c转换为Object数组
(2)检查是否需要扩容
(3)将集合c中的元素全部拷贝到原数组末尾

	public boolean addAll(Collection<? extends E> c) {
		//将集合c转换为Object[]
        Object[] a = c.toArray();
        int numNew = a.length;
        //检查是否需要扩容
        ensureCapacityInternal(size + numNew);  // Increments modCount
        //将集合c中的元素全部拷贝到原数组末尾
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        //若c不为空return true, else return false
        return numNew != 0;
    }

addAll(int index, Collection c) 指定位置添加集合c的所有元素

(1)检查index是否越界
(2)将集合c转换为Object数组
(3)检查是否需要扩容
(4)移动index以及右边的所有元素,挪出空间存放集合c的所有元素
(5)index位置添加集合c的所有元素

	public boolean addAll(int index, Collection<? extends E> c) {
		//检查index是否合法
        rangeCheckForAdd(index);
		//这里的几个步骤都和addAll(Collection c)相同
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
		//需要移动的元素个数
        int numMoved = size - index;
        //若需要移动的元素个数大于0, 那么需要进行元素移动操作
        if (numMoved > 0)
        	//右移index以及右边的所有元素, 挪出空间用于存放集合c的元素
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);
		//在index位置将集合c的所有元素拷贝至elementData
        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

get(int index) 获取指定位置上的元素

(1)检查index是否越界
(2)返回index位置上的元素

	public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }
    
	/**
     * 这里只检查是否越上界, 如果越下界会抛出ArrayIndexOutOfBoundsException
     */
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
	
	E elementData(int index) {
        return (E) elementData[index];
    }

remove(int index) 删除指定位置上的元素并返回

(1)检查index是否越界
(2)获取index位置上的元素
(3)若index不指向末尾元素,index后面的元素前移一位
(4)末尾元素置为null,便于GC回收
(5)返回删除元素

    public E remove(int index) {
    	//检查index是否越界
        rangeCheck(index);

        modCount++;
        //获取index位置上的元素
        E oldValue = elementData(index);
		//如果index并不指向最后一个元素, 那么就前移index后面的元素
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
       	//elementData最后一位置为null, 消除引用便于GC回收
        elementData[--size] = null;
        return oldValue;
    }

remove(Object o) 删除指定元素值

(1)遍历整个数组找到指定元素
(2)fastRemove

	/**
     	删除数组中第一次出现的指定元素, 删除成功return true, else return false
     */
    public boolean remove(Object o) {
    	//遍历整个数组, 删除第一个出现的null元素 (这里使用"=="进行比较)
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {//遍历整个数组, 删除第一个出现的指定元素 (这里使用"equals()"进行比较)
            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
    }

retainAll(Collection c) 求与集合c的交集部分

(1)集合c检查是否为空
(2)遍历element,保留与集合c相交的部分,删除不相交的部分(写指针之后的所有元素置为空)
(3)若成功批量删除return true,else return false

    public boolean retainAll(Collection<?> c) {
        //要求集合c不为空
        Objects.requireNonNull(c);
        //调用批量删除方法, 此时boolean complement = true, 表示删除不包含在c中的元素
        return batchRemove(c, true);
    }
	
	/**
	 *	批量删除元素
	 *	complement = true: 删除c中不包含的元素
	 *	complement = false: 删除c中包含的元素	
	*/
    private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData;	
        //初始化读指针r, 写指针w
        int r = 0, w = 0;
        //是否成功修改的返回值, 默认false
        boolean modified = false;
        try {
        	//遍历整个数组, 如果集合c中包含读指针指向的元素, 且complement = ture, 就将读指针指向的元素放在写指针指向的位置, 这样就留下了集合c与ArrayList相交的部分, 删除了不相交的元素
        	//如果集合c中不包含读指针指向的元素, 且complement = false, 就将读指针指向的元素放在写指针指向的位置, 这样就留下了集合c与ArrayList不相交的部分, 删除了相交的元素
        	//整个操作都在原数组上完成, 不需要额外空间
        	//这个操作有点类似LeetCode上移动零那道题, 比较典型的双指针解法
            for (; r < size; r++)
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            //正常情况下, 读指针最后一定是指向size的, 除非c.contains()抛出了异常
            if (r != size) {
            	//若c.contains抛出异常, 则把未读元素都拷贝到写指针之后
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                //写指针指向相应的位置
                w += size - r;
            }
            //若写指针不指向末尾元素, 那么写指针后序的元素都需要置为空
            if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                //此时删除后的size等于写指针所指的位置
                size = w;
                //标识完成了整个批量删除
                modified = true;
            }
        }
        return modified;
    }

removeAll(Collection c) 求与集合c的差集

	/**
     * 保留当前集合中不与c相交的元素
     */
    public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        //同样调用了批量删除方法, complement = false意味着删除包含在c中的元素
        return batchRemove(c, false);
    }

总结

(1)ArrayList底层采用Object数组进行数据存储,每次添加时都会检查是否需要扩容,若容量不足则通过创建一个新的长度为原长度1.5倍的数组,并将原数组拷贝至新数组并返回。
(2)ArrayList随机访问时间复杂度为O(1)
(3)ArrayList添加元素到末尾的时间复杂度为O(1),添加元素到中间位置的时间复杂度为O(n)
(4)ArrayList删除末尾元素的时间复杂度为O(1),删除中间位置元素的时间复杂度为O(n)
(5)ArrayList支持求并集,调用addAll(Collection c)
(6)ArrayList支持求交集,调用retainAll(Collection c)
(7)ArrayList支持求单向差集,调用removeAll(Collection c)

一个比较重要的点写在末尾:不难发现ArrayList如果一开始通过默认构造函数创建,那么过多的添加操作就会导致过多的扩容操作,并且每一次扩容操作都会进行一次arraycopy,这个操作是非常耗时的,因此为了尽可能的避免扩容带来的性能开销,可以在事先就指定好ArrayList的容量进行创建。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值