Java集合-ArrayList深入学习

  • Post author:
  • Post category:java


深入理解学习ArrayList



概述


  1. ArrayList

    使用动态数组来存储元素。用MSDN(面向开发人员和技术专业人员的 Microsoft 文档和学习主页)中的说法,就是Array的复杂版本,它提供了动态的增加和减少元素,实现了Collection和List接口,灵活的设置数组的大小等好处。

动态数组是指在声明时没有确定数组大小的数组,即忽略圆括号中的下标;当要用它时,可随时用ReDim(为数组变量重新分配存储空间)语句重新指出数组的大小。使用动态数组的优点是可以根据用户需要,有效利用存储空间


  1. 继承关系图


    在这里插入图片描述
    1>.ArrayList继承了AbstractList,实现了List,提供了添加、修改、删除、遍历等功能。

    2>.ArrayList实现了RandomAccess接口,提供了随机访问的功能。在ArrayList中,通过元素下标快速获取到元素。实际RandomAccess是没有任何代码的,但ArrayList底层是数组,所以快速随机访问是肯定的。

    3>.ArrayList实现了Cloneable接口,即覆盖了clone()方法,可以被克隆(浅克隆)。

    4>.ArrayList实现了Serializable接口,即支持序列化(将对象的状态信息转换为可以存储或传输的形式的过程)。

    5>.ArrayList是非线程安全的,根据场景需要可以使用线程安全的替换ArrayList,例如Vector或CopyOnWriteArrayList。



扩容原理

  1. 了解ArrayList的属性
	// 默认数组长度
    private static final int DEFAULT_CAPACITY = 10;
	// 空数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // 空数组,不同于EMPTY_ELEMENTDATA,该空数组主要用于无参创建ArrayList时扩容
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    // 存储元素的数组,transient修饰不存于序列化
    transient Object[] elementData; 
    // 数组长度,实际数组里元素的数量
    private int size;
  1. 了解ArrayList的构造方法,不同的构造方法影响着扩容机制的判断
    // 无参构造,elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

	// 指定初始容量构造,当initialCapacity为0,elementData = EMPTY_ELEMENTDATA
    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);
        }
    }

	// 包含指定集合有参构造,指定集合的length为0时,elementData = EMPTY_ELEMENTDATA
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
  1. 扩容,发生在add添加元素方法中,只有添加才会涉及容量不足而产生扩容。后续分析扩容以无参构造创建的ArrayList,且第一次添加元素作为主要分析依据。看下两个主要添加方法的具体逻辑:
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    public void add(int index, E element) {
        // 确保指定下表不越界
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }	

可以看到两个方法都调用了ensureCapacityInternal(size + 1)方法,把数组长度加1以确保能存下下一个数据。

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

此方法会先调用calculateCapacity方法,参数有elementData、minCapacity。无参构造时elementData为DEFAULTCAPACITY_EMPTY_ELEMENTDATA;minCapacity为size + 1,size为0,即minCapacity为1;

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

此方法会判断当前数组是否为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,刚才提到无参构造时才会返回这个数组。所以,若创建ArrayList时调用的是无参构造,此方法会返回DEFAULT_CAPACITY(值为10)和 minCapacity的之间最大值,因此,最终会返回固定值10;若创建ArrayList时调用了有参构造,则第一次调此方法会返回1,注意这个minCapacity变量只是第一次调用add方法时值为1,此后的调用需要根据实际的数组长度size + 1。

再调用ensureExplicitCapacity方法

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

modCount++用到了快速失败机制,此处先不做讨论;

继续向下走,如果minCapacity大于elementData.length,则会调用grow方法,注意这个elementData.length返回的是当前数组的容量,而不是数组实际的长度size。如果调用了有参构造,例如传入的容量为5,则此时elementData.length值即为5,而此时第一次调用add时,size值为0,因此minCapacity为1,不满足条件,此情况不需要扩容调用grow方法;如果调用了无参构造返回数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA,注意这个数组只是一个空数组,因此此时elementData.length为0,满足条件,需要扩容调用grow方法。

通俗来讲,就是如果ArrayList给定了特定初始容量,则此处需要根据实际情况确定是否调用grow方法,即有可能不需要扩容。如果没有指定初始容量,第一次调用add则此处一定需要调用grow方法。

	private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
	
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        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);
    }

int newCapacity = oldCapacity + (oldCapacity >> 1)此行代码即为扩容的核心,oldCapacity为原来的容量,右移一位,即除以2,因此这句的意思就是新的容量newCapacity = oldCapacity+oldCapacity / 2,即原来的1.5倍。

然后判断newCapacity如果小于传入的minCapacity,则直接让newCapacity等于minCapacity(当无参构造时,elementData.length为0,所以oldCapacity也为0,minCapacity为10,因此最终newCapacity为10)。

然后判断newCapacity是否大于设定的MAX_ARRAY_SIZE,此处

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE – 8;

如果大于,则调用hugeCapacity方法

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

如果minCapacity大于MAX_ARRAY_SIZE,则返回Integer的最大值,否则返回MAX_ARRAY_SIZE。最后,通过Arrays.copyOf方法把原数组的内容放到更大容量的数组里面。



面试常问

  1. ArrayList 是什么?常用来做什么?底层是什么实现的?

    ArrayList是有序动态数组,顾名思义底层是数组实现的,实现了List接口,提供了基本的增、删、改、查等功能。常用来装载数据的。
  2. ArrayList 和 LinkedList 有什么区别?各自优缺点?

    ArrayList 查找和访问速度较快(底层是数组),添加和删除相较慢些(例如指定中间位置添加元素,该下标后的元素全部后移一位,删除同理,该下标后的元素全部前移一位)

    LinkedList 查找和访问速度较慢(底层是链表),添加和删除相较快些。

    都是非线程安全的。

    当遇到访问元素比较频繁时,建议使用ArrayList;当遇到在某个特定索引中,删除或添加元素比较频繁时,建议使用LinkedList。
  3. ArrayList 是否是线程安全的?

    ArrayList是非线程安全的,可以使用线程安全的集合例如Vector或CopyOnWriteArrayList。
  4. ArrayList 动态扩容机制是什么?

    1.扩容是发生在调用add方法的时候

    2.在add方法中,会先 判断存储元素的数组elementData是否等于ArrayList一个特殊的空数组,只有无参构造才会等于,minCapacity才会为默认长度10

    有参构造不会等于,minCapacity为size + 1,size为当前数组的元素数量。再判断minCapacity是否大于elementData.length,大于则表明需要扩容。

    指定两个变量,oldCapacity = element.length,newCapacity = oldCapacity + oldCapacity / 2,即新数组长度为之前数组长度的1.5倍。判断

    newCapacity < minCapacity,小于则newCapacity = minCapacity;再判断newCapacity > MAX_ARRAY_SIZE,大于则最大赋值Integer.MAX_VALUE。

    最后通过Arrays.copyOf创建新长度的数组,包含原数组元素。

扩容参考文章:

https://blog.csdn.net/qq_26542493/article/details/88873168



版权声明:本文为qq_43646405原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。