Java集合之ArrayList

  • Post author:
  • Post category:java


List包含ArrayList, Vector 和 LinkedList3 个常用子类,如果要使用List接口进行操作,就必须依靠其子类,今天我们就来一起学习它最常用的一个子类ArrayList。


目录


基本介绍


常用方法


源码解析


关键变量


关键方法解析


从集合中删除元素


for循环删除元素有哪些坑?


正确的方法


总结


基本介绍

ArrayList的底层实际上也是由数组实现的,在开发中我们如果不确定数据量大小的时候,一般选用集合而非数组,其原因主要是集合是支持动态扩容的,而数组不支持。

常用方法

public ArrayList(int initialCapacity) 
public ArrayList() 
public ArrayList(Collection<? extends E> c)
public int size()  返回集合的大小
public boolean isEmpty()  判断集合是否为空
public boolean contains(Object o)  判断包含
public int indexOf(Object o)  获得某个元素的第一个位置
public int lastIndexOf(Object o)  获得某个元素的最后一个位置
public E get(int index)  根据索引获取元素
public E set(int index, E element)  替换某个位置的元素
public boolean add(E e)  添加元素
public void add(int index, E element)  在某个位置添加元素
public E remove(int index)  根据索引删除元素
public boolean remove(Object o)   根据元素对象删除元素
public boolean addAll(Collection<? extends E> c)  将另一个集合添加到当前集合中

源码解析

关键变量

// ArrayList的默认容量
private static final int DEFAULT_CAPACITY = 10;

// 空数组元素,在实例化新集合时如果传入初始容量为0,则会生成这个空数组对象
private static final Object[] EMPTY_ELEMENTDATA = {};

// 默认大小的空集合对象,在实例化新集合时如果使用默认无参构造,则会生成这个数组对象
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 集合元素缓冲区,这个缓冲区的大小等于集合的容量
// 添加第一个元素时,elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA的任何空ArrayList都将扩展为DEFAULT_CAPACITY。
transient Object[] elementData;

// 集合的容量
private int size;

// 集合的修改次数
// 当前集合发生意料之外的修改时,程序会触发fail-fast机制抛出ConcurrentModificationException异常
// 当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
protected transient int modCount = 0;

// 集合的最大容量 2的31次方 - 1 - 8
// 一些虚拟机会预留数组头部的大小,所以还要在减8
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

关键方法解析

1. 初始化大小的构造函数 ArrayList(int initialCapacity)

// 初始化大小的构造函数
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            // 如果initialCapacity > 0,则将elementData实例化为初始容量为initialCapacity的Object数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            // 如果initialCapacity == 0,则初始化一个空数组EMPTY_ELEMENTDATA
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            // 如果initialCapacity < 0,则抛出IllegalArgumentException异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

2. 根据元素获取该元素所在的第一个位置  indexOf(Object o)

public int indexOf(Object o) {
        // 如果传入的元素为null
        if (o == null) {
            for (int i = 0; i < size; i++)
                // 从0开始循环,判断哪个元素为null,找到则返回该元素的下标
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                // 同上,因为这边o不为null,所以使用equals
                if (o.equals(elementData[i]))
                    return i;
        }
        // 找不到对应的元素,则返回-1
        return -1;
    }

3. 根据集合索引下标获取元素 get(int index)

public E get(int index) {
        // 校验下标参数
        rangeCheck(index);
           
        // 如果参数合法,则从数组的对应下标中取出对应的元素
        return elementData(index);
}


private void rangeCheck(int index) {
        // 判断参数是否>=集合的容量,如果是则抛出索引越界的异常
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

4. 添加元素  add(E e)

public boolean add(E e) {
        // 确保集合的容量,其实就是该扩容的时候要扩容
        ensureCapacityInternal(size + 1);
        // 在集合的最后一位添加元素
        elementData[size++] = e;
        return true;
    }


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

/**
 * 计算容量大小
 *
 * @Param elementData 当前数组
 * @Param minCapacity 最小容量
 */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 如果当前elementData是默认大小的数组
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // 返回默认大小(10)或minCapacity的最大值
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }


private void ensureExplicitCapacity(int minCapacity) {
        // 操作数+1
        modCount++;

        // overflow-conscious code
        // 如果当前数组所需的最小容量 - 当前容量 > 0 说明当前容量不够本次新增的数组所需的容量
        if (minCapacity - elementData.length > 0)
            // 扩容
            grow(minCapacity);
    }


private void grow(int minCapacity) {
        // overflow-conscious code
        // 当前数组容量
        int oldCapacity = elementData.length;

        // 预计扩容后的容量,为当前容量的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);

        // 如果newCapacity还是比所需容量小,则直接将所需容量赋值给newCapacity
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;

        // 如果newCapacity大于集合所能承载的最大容量
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            
            则考虑将Integer的最大值赋给newCapacity
            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;
    }

5. 在指定索引处插入一个元素  add(int index, E element)

public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);
    // 上面的逻辑都是一样的,区别就在这个地方,会将需要插入的索引之后的数组往后复制一位,然后在当前位置插入一个新的元素
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    elementData[index] = element;
    size++;
}

6. 根据指定索引移除一个元素 remove(int index)

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

    modCount++;
    
    // 获取当前位置的元素,用于后面返回
    E oldValue = elementData(index);

    // 计算数组需要移动的索引位置
    int numMoved = size - index - 1;
    if (numMoved > 0)
        // 复制数组
        System.arraycopy(elementData, index+1, elementData, index, numMoved);

    // 将当前位置置为null,后期会被GC
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

从集合中删除元素

for循环删除元素有哪些坑?

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 1, 1, 4, 5, 6));
    for (int i = 0; i < list.size(); i++) {
        if (list.get(i) == 1) {
            list.remove(i);
        }
    }
    for (int i = 0; i < list.size(); i++) {
        System.out.print(list.get(i) + " ");
}

按照上述代码中的逻辑 ,我们应该把list中值等于1的元素全部删除,但让我看看结果

竟然还有一个1 ?

这种方式的问题在于:删除某个元素后,list的大小发生了变化,而你的索引也在变化,所以会导致你在遍历的时候漏掉某些元素。

比如当你删除第1个元素后,后面的元素都往前移动了一位;继续根据索引访问第2个元素时,实际访问的是原来的第3个元素。

因此,这种方式可以用在删除特定的一个元素时使用,但不适合循环删除多个元素时使用。

如果通过增强for循环来删除元素呢?

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 1, 1, 4, 5, 6));
    for (Integer i : list) {
        if (list.get(i) == 1) {
            list.remove(i);
        }
    }
    for (int i = 0; i < list.size(); i++) {
        System.out.print(list.get(i) + " ");
}

竟然直接报错了? 通过debug我发现,原来增强for循环实际上就是创建一个Iterator对象,让我们一起来看看源码

// 首先在循环开始时,会先创建一个iterator对象
public Iterator<E> iterator() {
    return new Itr();
}

// 然后会调用hasNext方法,这里其实就是比较当前索引是否不等于容器容量
public boolean hasNext() {
    // cursor标记了当前循环的索引
    return cursor != size;
}

// 然后调用next方法
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;
    return (E) elementData[lastRet = i];
}


final void checkForComodification() {

    // 在ArrayList中实现了一个iterator对象
    // 在其中有expectedModCount这个属性
    // 在循环之初初始化Itr对象时,会将ArrayList的modCount赋值给expectedModCount
    // int expectedModCount = modCount;
    // 此处如果modCount != expectedModCount 就会抛出异常
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

那么在这为什么expectedModCount 和 modCount会不相等呢,我们可以回到上面看看ArrayList的remove方法就能知道,方法中只对modCount做了操作,但是并没有对expectedModCount做任何的操作。说到这是不是就清晰了,大家也明白为什么会这边会抛出ConcurrentModificationException异常了吧~

正确的方法

1. 使用ArrayList内置的iterator对象进行循环,删除操作

再来看看ArrayList中Itr对象的remove方法

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        // 实际上这个方法也是调用list中的remove方法
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;

        // 区别在这,itr中的remove方法会将modCount 赋值给expectedModCount
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

2. 使用ArrayList的removeIf方法删除元素

这个方法是将要删除的元素的下标先放到BitSet中,然后再进行删除操作,这个操作不会修改modCount的值。

总结

1. ArrayList是底层是由Object数组实现的

2. ArrayList支持动态扩容,每次扩容1.5倍

3. ArrayList支持按照索引下标随机访问,因此查询快

4. 如果初始化ArrayList时不指定容量,则会生成默认容量的空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA,在第一次add时才会初始化容量

5. ArrayList删除元素时要使用Iterator或removeIf



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