Java集合之ArrayList

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

目录

基本介绍

常用方法

源码解析

关键变量 

关键方法解析

从集合中删除元素

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

正确的方法

总结


基本介绍

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

常用方法

public ArrayList(int initialCapacity) 
public ArrayList() 
public ArrayList(Collection 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 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 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的元素全部删除,但让我看看结果 

Java集合之ArrayList_第1张图片

竟然还有一个1 ?

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

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

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

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

List 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) + " ");
}

Java集合之ArrayList_第2张图片

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

// 首先在循环开始时,会先创建一个iterator对象
public Iterator 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对象进行循环,删除操作

Java集合之ArrayList_第3张图片

 再来看看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

你可能感兴趣的:(#,集合,java,开发语言)