Java容器重点源码回顾(一)——ArrayList

1.ArrayList定义

ArrayList是基于数组进行实现的,数组的默认大小为10.

    /**
     * 默认初始化容量
     */
    private static final int DEFAULT_CAPACITY = 10;
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  1. AbstractList实现了List接口的部分方法,使得它的派生类可以减少工作量。
  2. RandomAccess: 是一个标志接口,本身没有方法。用来标识是否可以实现随机访问。常见的例子是ArrayList(实现RandomAccess)和LinkedList(没有实现RandomAccess)。实现了这个接口的会使用indexBinarySearch(二分查找法),没有实现的会使用iteratorBinarySearch(迭代器查找)。实不实现主要看效率问题,具体参见博文:RandomAccess详细介绍
    Java容器重点源码回顾(一)——ArrayList_第1张图片
  3. Cloneable:是一个标志接口,本身没有方法。用来表示可以进行克隆,如果要进行clone但是没有实现这个接口,就会报错。具体深拷贝和浅拷贝的知识可以参见博文:深拷贝和浅拷贝的区别
  4. java.io.Serializable: 是一个标志接口,本身没有方法。用来表示能够进行序列化和反序列化,如果要进行序列化但是没有实现这个接口,就会报错

2. 扩容操作

下面介绍下ArrayList的扩容机制,这里以在指定位置插入新的元素为样例,主要代码如下面几行所示:

	/**
     * 在指定位置插入元素
     */
    public void add(int index, E element) {
        rangeCheckForAdd(index);   // 检查范围

        ensureCapacityInternal(size + 1);  // Increments modCount!!   // 确保容量足够
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);   // index开始后面的元素全部向后移动一位
        elementData[index] = element;  // 插入元素
        size++;
    }
    // 确保容量足够的函数
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    // 计算容量的函数
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
	   if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {   // 如果没有定义长度的ArrayList
	       return Math.max(DEFAULT_CAPACITY, minCapacity);  // 如果容量minCapacity小于默认容量,就设置为默认容量
	   }
    	return minCapacity;
	}

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;  // 修改次数+1

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)  // 需要进行扩容的话
            grow(minCapacity);  // 进行扩容
    }
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;  // 原始数组长度
        int newCapacity = oldCapacity + (oldCapacity >> 1);  // 新的长度是原来的1.5倍
        if (newCapacity - minCapacity < 0)  // 如果新的长度小于最小扩容后长度
            newCapacity = minCapacity;  // 更新为最小最小扩容后长度
        if (newCapacity - MAX_ARRAY_SIZE > 0)  // 如果比最大数组长度还要大
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);   // 拷贝数组元素到扩容后的数组上
    }
  	
  	// 最大容量
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow   // 违规变量
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?   // 最大容量
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
	

扩容的主要步骤如下:

  1. 检查要插入的元素的位置是否合规
  2. 检查原始容量是否足够,容量不够则进行扩容。
    - 首先进入ensureCapacityInternal函数,接着是继续调用calculateCapacity函数。
    - 在calculateCapacity函数中,检查原始的数组是否是空的数组,如果是空的数组,则将默认容量DEFAULT_CAPACITY和扩容的最小容量minCapacity比较,选择大的进行返回
    - 接着进入到ensureExplicitCapacity函数,如果minCapacity小于原始数组的长度,那就调用grow函数进行扩容。
    - 在grow函数中,可以看到,首先是**利用原始容量oldCapacity*1.5来得到新的容量**,然后和minCapacity进行比较,选择较大的作为新的容量大小。如果容量特别大,还要调用hugeCapacity函数。
    - 最后,利用Arrays.copyOf完成元素迁移到新数组中。
  3. 数组中index之后的元素全部向后移动一位
  4. 在index位置中插入新的元素。

主要知识点总结:

  1. ArrayList底层是基于数组进行实现的,每次扩容都会new一个新的数组,然后将元素copy过去,这是比较消耗算力的。因此在进行初始创建的时候就应该选定一个差不多的容量。
  2. 因为每次扩容比较消耗时间,因此虽然每次可能只插入一个新的元素,但是ArrayList的扩容是每次扩容1.5倍。

3. 删除操作

下面介绍下删除操作,这里以移除指定位置的元素为样例,具体代码如下所示:

    /**
     * 移除指定位置的元素
     */
    public E remove(int index) {
        rangeCheck(index);    // 检查范围

        modCount++;  // 修改次数+1
        E oldValue = elementData(index);   // 旧的值

        int numMoved = size - index - 1;   // 需要移动元素的个数
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);  // index后的元素全部向前移动一位
        elementData[--size] = null; // clear to let GC do its work  最后一个元素设置为null

        return oldValue;
    }

主要步骤:

  1. 首先得到旧的值oldValue
  2. 将index之后的元素全部向前移动一位
  3. 将最后一个位置设置为null,方便gc。

4. Arrays.copyOf()和System.arraycopy()

我们根据具体的代码来看两者的区别:

	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;
    }
	
	public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

  1. Arrays.copyOf()他是会创建一个新的数组,然后进行拷贝迁移。内部实际上在创建新的数组之后,也是调用System.arraycopy()函数
  2. System.arraycopy()方法可以看到是一个native方法,底层不是用java实现的。他实现的是将数组中的元素迁移到指定位置,并不会创建新的数组

4. modCount的作用

modCount在ArrayList的代码里面经常出现,它**主要用来记录数据的修改次数,应用于多线程情况下使用迭代器。在使用迭代器的时候,线程1首先会设置expectModCount=modCount。当线程1查看了当前数据,然后挂起。线程2这时对于数据进行了修改,这时modCount+1.当线程1继续访问,便会发现expectModcount != modCount。这时就会直接报错,也就是我们常说的fail-fast机制**。

你可能感兴趣的:(Java基础,java,开发语言,容器)