java基础详解-ArrayList

一、适用场景

   ArrayList就是数组列表,对于基本数据类型byte、short、int、long、float、double、char、boolean,存储他们对应的包装类Byte、Short、Integer 、Long、Float、Double、Character、Boolean,主要底层实现为Object[] elementData.
   与LinkedList相比,查询效率高,增删效率低,线程不安全(更多在LinkedList介绍)
   与Array相比,容量能动态改变,拥有更丰富的操作方法 :
   存储内容比较
   Array数组可以包含基本类型和对象类型,ArrayList却只能包含对象类型。
但是需要注意的是:Array数组在存放的时候一定是同种类型的元素。ArrayList就不一定了,因为ArrayList可以存储Object。
   空间大小比较
Array它的空间大小是固定的,空间不够时也不能再次申请,所以需要事前确定合适的空间大小。
ArrayList的空间是动态增长的,如果空间不够,它会创建一个空间比原空间大约0.5倍的新数组,然后将所有元素复制到新数组中,接着抛弃旧数组。而且,每次添加新的元素的时候都会检查内部数组的空间是否足够。
   方法上的比较
ArrayList作为Array的增强版,当然是在方法上比Array更多样化,比如添加全部addAll()、删除全部removeAll()、返回迭代器iterator()等。
   适用场景
   如果想要保存一些在整个程序运行期间都会存在而且不变的数据,我们可以将它们放进一个全局数组里
   如果我们单纯只是想要以数组的形式保存数据,而不对数据进行增加等操作,只是方便我们进行查找的话,那么,我们就选择ArrayList。
   而且还有一个地方是必须知道的,就是如果我们需要对元素进行频繁的移动或删除,或者是处理的是超大量的数据,那么,使用ArrayList不是一个好的选择,因为它的效率很低,使用数组进行这样的动作就很麻烦,那么,我们可以考虑选择LinkedList。如果需要线程安全,可使用Vector或者List list = Collections.synchronizedList(new ArrayList(...))包装List为一个线程安全的数组容器;

二、构造方法

1、public ArrayList(int initialCapacity) {} 构建指定参数大小的ArrayList,内部数组分配为固定大小,this.elementData = new Object[initialCapacity];
2、public ArrayList() {} 构建一个默认ArrayList,内部数组分配为空,this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA,默认创建一个空数组,第一次add时容量会变为10
3、public ArrayList(Collection<? extends E> c) {} 构造一个包含指定集合元素的列表

三、常规方法

1、增加操作
ArrayList的添加元素操作,有指定index新增和直接新增两类
a:指定index新增

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

b:直接新增,直接将e赋值到指定位置,然后size+1

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

添加过程中间涉及扩容操作,放在四
指定index新增主要涉及arraycopy,数组的复制操作

Params:
    src – the source array 源数组
    srcPos – starting position in the source array 源数组要复制起始位置
    dest – the destination array 目标数组
    destPos – starting position in the destination data 目标数组起始位置
    length – the number of array elements to be copied 复制长度

public static native void arraycopy(Object src,  int  srcPos,
                               Object dest, int destPos,int length);

所以index新增是将源数组从index位置到末尾开始复制,然后放到index+1的位置,这样index的位置就空余了出来,可以插入对应元素(参考敖丙)
比如在index为4的位置新增元素A

将index为4位置之后的元素复制,放在了index+1=5的位置,这样index等于4的位置空了出来,elementData[index] = element 就实现了目标的插入

综上,其实arraylist顺序插入的时候速度并不会很慢,指定位置插入时,涉及到数组的复制,如果数组较大,再加上扩容等操作,插入速度就会受到影响,这也是常说的ArrayList插入效率慢的原因,当然,这也取决删除和新增的元素离末端有多远

2、删除操作
删除操作也有按指定元素和指定位置删除两种,本质上也是数组的复制,与增加类似,在此不多细述

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);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

按指定元素删除

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

3、修改操作
修改操作set实现比较简单,就是替换index位置的指为指定的element

public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

4、查询操作
查询操作get也不复杂,校验索引边界之后返回index所在元素

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

    return elementData(index);
}

四、扩容原理

做添加操作时,两类方法都会执行ensureCapacityInternal(int minCapacity)方法,查看方法执行过程,如果判断当前列表长度不足,即当需要的长度大于自身最大容量时,需要进行扩容,方法最终执行到grow(int minCapacity)

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 判断是否需要扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

扩容流程,可以看到扩容原理,一般情况下(指不发生越界,或者hugeCapacity返回长度为Integer.MAX_VALUE或Integer.MAX_VALUE-8),将容量扩充为原来的1.5倍(oldCapacity + (oldCapacity >> 1) 右移一位相当于除以2)

private void grow(int minCapacity) {
    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);
}

大小越界或者超过允许最大值判断

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

五、其他疑问

1、内部Object[] elementData为什么用transient修饰
   由于 ArrayList 是基于动态数组实现的,所以并不是所有的空间都被使用。因此使用了 transient 修饰,可以防止被自动序列化。ArrayList实现了writeObject和readObject方法,ArrayList在序列化的时候会调用writeObject,直接将size和element写入ObjectOutputStream,反序列化时调用readObject,从ObjectInputStream获取size和element,再恢复到elementData。
   为什么不直接用elementData来序列化,而采用上诉的方式来实现序列化呢?原因在elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间

2、ArrayList会不会初始化数组大小
   会初始化数组大小,但list大小没变,因为list大小是返回size(),即打印list大小依然是0,set下标值时会报错,数组下标越限

3、实现implements RandomAccess, Cloneable, java.io.Serializable这些接口的作用
   这些接口叫做标记接口
   RandomAccess :RandomAccess接口, 用来表明其支持快速(通常是固定时间)随机访问,如果容器支持快速随机访问,则会使用基于索引的二分查找,否则使用基于迭代的二分查找(实现RandomAccess接口的List可以通过for循环来遍历数据比使用iterator遍历数据更高效,未实现RandomAccess接口的List可以通过iterator遍历数据比使用for循环来遍历数据更高效)
   Cloneable 标记接口代表我们的对象可以被拷贝,ArrayList实现了clone方法
   Serializable 序列化接口,表明ArrayList支持序列化和反序列化,ArrayList实现了writeObject和readObject方法

posted @ 2021-03-20 17:59  潇视  阅读(264)  评论(0)    收藏  举报