【集合】ArrayList

一、概述

ArrayList是一个用来顺序存储元素的集合,它是有序且可以随机访问的。正如它的名字所示,它的底层数据结构就是用数组实现的。那为什么不直接用数组存储元素呢?因为ArrayList提供了一系列人性化的API,迭代器以及自动扩容机制,使你不必关心数组元素的迁移变动。有几个关键的成员变量需要我们重点关注一下:

  • elementData:一个对象数组,ArrayList用于存储元素的关键数据结构
  • size:elementData数组中已经存储的元素数量
  • modCount:ArrayList的修改次数,详细分析可以移步第三小节

不过需要注意的是ArrayList不是线程安全的,如需要多线程操作可以使用CopyOnWriteArrayList,该类利用一个写时复制的机制,通过加锁的方式依次给每个线程拷贝一个数组副本,在线程修改完副本后,将主数组引用指向该副本。

还有一种方法是使用工具类Collections的synchronizedList方法,该方法用到了装饰器模式,维护了一个内部类SynchronizedList,该类所有的方法都用到了synchronized同步锁,从而改变了原list的行为。

二、常用API源码分析

1.初始化

先来看下ArrayList的成员变量和构造方法

// java.util.ArrayList

	// ArrayList数组默认初始化容量为10
    private static final int DEFAULT_CAPACITY = 10;
	// 空元素数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
	// 默认容量的空元素数组,默认无参构造方法会使用到
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
	// 关键数据结构,ArrayList用来存储元素的数组
    transient Object[] elementData; 
	// 数组中已存储元素的数量
    private int size;
    
	// 指定数组容量的构造方法
    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);
        }
    }
    // 默认容量的构造方法
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    // 利用另外一个集合构造ArrayList
    public ArrayList(Collection<? extends E> c) {
    	// 将c转为数组,elementData指向该数组
        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;
        }
    }

2.增加元素

增加元素主要有两种方式,一种是指定插入位置add(int index, E element),另一种是尾部插入add(E e)。首先来看下尾插法,该方法的实质是往数组已存储元素的后一个位置插入新元素,这个逻辑并不复杂,比较值得一看的是自动扩容部分(或者数组初始化)的代码。

    public boolean add(E e) {
    	// 保证数组长度能够容纳新增加的元素,如果不够会自动扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }   
    
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
    	// 数组未初始化的情况下,返回默认容量10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        // 数组已经初始化的情况下返回size+1
        return minCapacity;
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
    	// 修改计数器加一
        modCount++;

        // 当size+1超过数组长度时需要扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        // 在原来的基础上扩容50%
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 溢出以及int最大值的判断
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // Arrays.copyOf方法会根据传入的数组长度创出一个新数组,同时将原数组的元素复制进新数组中,元素下标不变,新数组空位是null
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

再来看下指定位置的插入方法add(int index, E element),扩容机制和上面代码相同就不再多说了,主要差别体现在:指定插入位置会使该位置之后的元素后移一格

    public void add(int index, E element) {
    	// 检查入参index是否越界或小于0
        rangeCheckForAdd(index);
		// 检查容量,这个方法和上面add中的相同,不再展开
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // arraycopy方法使index后的数组元素后移一格
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        // 将新元素插入index位置
        elementData[index] = element;
        size++;
    }

3.获取元素

获取元素的源码非常简单,就是先检查index是否合法,然后将index位置的元素返回回来。

public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

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

4.删除元素

删除元素也有两种方式,第一种是根据数组下标删除remove(int index),第二种是根据对象删除remove(Object o) 第一种方式的实质将index后的数组元素前移一格,并将末尾置为null

    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        // 找到该索引的数组元素,用于当做返回值
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        // index后的数组元素前移一格
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 由于数组元素前移了,所以末尾置为null                     
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

再来看下第二种删除方式remove(Object o)和第一种方式相比多了一步根据对象找寻索引的过程。

    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 {
        	// 遍历数组找寻第一个对象o的索引,用equals方法判断相等
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
    
    // 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
    }

三、迭代器和fail-fast

ArrayList提供了两种迭代器,一种是单向迭代的Itr还有一种是双向迭代的ListItr后者是前者的子类。使用iterator()方法能够构造出当前ArrayList的Itr迭代器;使用listIterator()能够构造出ListItr迭代器。我们常用的foreach语法其实是个语法糖,把class文件反编译后会发现其实它用的就是Itr迭代器。

由于ArrayList不是线程安全的,所以迭代器都实施了一种快速失败(fail-fast)的机制,这个机制会对比ArrayList的修改次数来快速抛出异常,而不是尽最大努力去遍历元素。这个修改次数是由modCount变量来统计的,正如上面分析的源码所示,增加或者删除元素时都会将modCount加一。

    private class Itr implements Iterator<E> {
        int cursor;       // 游标,指向当前指向的元素
        int lastRet = -1; // 上一个返回的数组元素的下标
        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 = ArrayList.this.elementData;
            // 游标超出数组长度
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            // 返回游标所指向的元素,并赋值lastRet 
            return (E) elementData[lastRet = i];
        }

        public void remove() {
        	// 没有调用过next()方法就remove或者连续remove两次
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();  // 检查迭代中是否有线程增删过元素

            try {
            	// 调用外部类ArrayList的remove方法删除元素,该方法会修改modCount
                ArrayList.this.remove(lastRet);  
                cursor = lastRet;  // 游标指向上一个返回的元素
                lastRet = -1;  
                expectedModCount = modCount;  // 同步modCount到expectedModCount
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            // 自旋的过程也要检查modCount
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }
		// 当expectedModCount和不符合modCount时,就说明在迭代过程中有线程增删过元素,抛出异常
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

从上述源码中我们可以获悉:使用foreach语法时不能使用ArrayList的remove方法。想要在迭代过程中删除元素必须使用迭代器的remove方法,因为它会同步modCount。下面是一段测试代码

    public void testForEach() {
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        for (String s : list) {
            // list.remove(s);  // 抛出ConcurrentModificationException异常
            // list.add("test");  // 抛出ConcurrentModificationException异常
            list.set(1, "22"); // ok
            System.out.println(s);
        }
        
		Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String s = iterator.next();
            // list.remove(s);  // 抛出ConcurrentModificationException异常
            iterator.remove();
        }
       System.out.println(list);
    }

四、参考

带你走进Java集合之ArrayList

猜你喜欢

转载自blog.csdn.net/hch814/article/details/107708375