Java集合容器——ArrayList

一,ArrayList特点:

  1. ArrayList具有List的有序性和可重复性
  2. ArrayList底层使用数组实现,因此支持随机访问(通过索引),根据索引查询快,但是根据内容查询慢(需要迭代),插入、删除很慢,因为需要移动和复制数组中的数据。
  3. ArrayList可以存放null
  4. ArrayList初始容量为10(在执行一次add方法后才变为10,不然是空)
  5. ArrayList扩张容量时的新容量默认为原始容量的1.5倍

二,ArrayList源码分析:

类定义:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
//继承了AbstractList抽象类,主要实现了List接口

成员变量:

private static final long serialVersionUID = 8683452581122892189L;
//序列化ID
private static final int DEFAULT_CAPACITY = 10;
//默认的List初始化长度为10

private static final Object[] EMPTY_ELEMENTDATA = {};
//默认的初始化空List是空的Object类型的数组

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//和上面的区别是当第一个元素被加入进来的时候它知道如何扩张,扩张多少

transient Object[] elementData; 
//数据实际存放在Object类型的数组,transient表示该变量不参与序列化

private int size;
//List中元素的个数,即数组长度

构造函数:

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);
    }
}

//指定List元素个数的初始化,如果元素个数大于0,则构建一个对应长度的Object数组,若等于0,则赋予空数组常量,若小于0,则报错
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//无参构造,若不指定元素个数,则赋予空数组常量,注意这时候数组长度为0,而不是10,需要调用一次add才能让数组长度变为10.
 
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;
    }
}
//若参数是一个集合,则将集合转化为数组赋予ElementData,并对转化后的数组是否为空进行判断,若不为空则对其返回的数组类型是否为Object进行判断,如果不是Object类型的数组,将其转换为Object数组;若返回的是空数组,则用空数组常量代替。

常用成员方法:

1. public boolean add(E e) 在列表中增加一个元素

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
//添加一个元素,传入新的数组长度size+1,进行新数组容量计算和更新(具体见下面的函数),判断后将新增的元素e存入数组,并返回true
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//计算内部数组需要的最小容量,再确定新数组的真正容量

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
//如果数组为空(注意这时候新元素还没有添加到数组里面),则比较一下默认容量10和添加元素后需要的最小容量,取更大的那个;若数组已经有元素了,数组容量至少也是10(因为已经经历过第一次初始化),因此返回最小容量。

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

    // 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);
    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);
}
//先定义新数组的容量是原数组的1.5倍(而不是一个一个增加),即增加一半;若数组需要的最小容量比预定义的容量还要大,那么新容量就设为最小容量,如果新容量超过了最大数组容量,则赋予一个尽量最大的容量;最后更新数组长度。
这里有个疑问,为什么传入hugecapacity的是mincapacity而不是newcapacity呢? 
这里假设原数组已经有非常多的元素了(接近array的最大容量),新数组的容量初始定义为原来的1.5倍,newcapacity已经超过MAX_ARRAY_SIZE了,如果这时候newcapacity比mincapacity还小,那么newcapacity此时应等于mincapacity,这时候传入hugecapacity两者均可;但是有一种情况就是newcapacity比mincapacity大,这时候newcapacity和mincapacity还是不同的,这时候传入hugecapacity的话两者就有不同了,如果传入的是newcapacity,那么hugecapacity一定返回Integer的最大值,这样就会浪费一些空间,毕竟实际需要的mincapacity可能还是小于MAX_ARRAY_SIZE的,用MAX_ARRAY_SIZE会节省一些空间,因此传入的最好是mincapacity,这样能得到Integer.MAX_VALUE和MAX_ARRAY_SIZE中较小的那个值;

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}
//如果最小需要容量小于零则报错,如果最小需要容量大于数组长度最大值,则返回Integer的最大值,否则返回数组长度最大值。 

2. public void add(int index, E element)在列表指定位置添加元素

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++;
}
//首先判断index的合法性(是否小于0,或大于数组长度);计算并更新数组容量;将原始数组从index开始的元素复制到新数组从index+1开始的位置,将数组index处空出;将新增元素插入到index处;将数组长度+1
附:arraycopy(Object src, int srcPos, Object dest,int destPos, int length)

3. public E remove(int index)删除指定位置的元素

public E remove(int index) {
    rangeCheck(index);
0,1,3,2,长度4,删除index=2,nummoved=1
    modCount++;
    E oldValue = elementData(index);

    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

    return oldValue;
}
//首先检查index的范围,注意remove中的rangecheck和add中的rangecheck有所不同,详情见下面的阐述;获取要被删除的元素,计算被移动的元素的数量,如果数量>0,说明有元素要被移动,就将要被删除的元素之后的元素向前移动一位,然后将数组长度-1的元素置为空;最后返回刚刚被删除的元素。

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//这两个方法分别为remove和add方法需要用到的索引进行范围检测,但是可以看到两者的范围不同:
1.为什么对于remove不需要检测index<0,而add需要?
答:我们再看一下add的代码
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1);
在rangecheck后对数组内部容量进行了重新确定,但是可以发现,确定新容量时并没有用到index,也就是说无论index是否合规,数组长度一定是增加的,如果不在rangecheck里对index进行负数判断,那么如果index是负数,在最后添加元素时虽然会报错,但是数组容量还是增加了,出现了没有添加元素但却增加了数组容量的逻辑错误;而remove中,紧接着rangecheck的就是对索引的使用,留到这里报错也是没有问题的。 
2.为什么remove的index不能等于size,而add可以?
很简单,这是因为add是指在指定索引处添加元素,举个例子,现在数组里有0-4共5个元素,现在在index=5的地方添加一个元素,按照add中的扩展数组的方法,此时数组容量一定大于5,0-4保持不变,然后在index=5的地方添加该元素即可,并不会存在越界的问题;而remove中,remove的index必须在数组范围内,但是问题是index>=size的报错仍然可以像负数报错一样留给后面引用索引时再抛出,为何这边又提前做了处理呢?我不知道该怎么解释

4. public boolean remove(Object o)移除指定值的元素

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}
//很简单,就是遍历数组,看有没有相同的元素,若相同就调用一个私有方法fastremove进行删除,删除成功则返回true,没有找到元素则返回false

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
}
//这个方法只为remove(object o)服务,是个私有方法,它和remove基本一样,只是少了范围检测(因为没有用到index),且不需要返回元素的值。

5. public void clear()清除所有元素

public void clear() {
    modCount++;

    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}
//遍历所有元素,并将其置为空,size置为0即可

其余包括get()、set()、addall()、removeall()、contains()等方法都比较易懂,在此不多做介绍了。

猜你喜欢

转载自blog.csdn.net/qq_41773240/article/details/87616011