ArrayList

概述

  1. ArrayList是可以动态增长和缩减的索引序列,它是基于数组实现的List类

  2. 该类封装了一个动态可在分配的Object[]数组,每个类对象都有一个capacity表示他们所封装的Object[]数组的长度,当向ArrayList中添加元素时,该属性值会自动增加。

  3. ArrayList的用法和Vector类似,但Vector是一个较老的集合,具有很多缺点,不建议使用,区别在于ArrayList是线程不安全的,而Vector则是线程安全的。

  4. ArrayList和Collection的关系图:

    image.png

ArrayList的数据结构

image.png

底层的数据结构就是数组,数组元素类型为Object类型,即可以存放所有类型数据。

ArrayList源码分析

继承结构和层次关系

image.png

image.png

从图中可以得知ArrayList先继承了AbstractList抽象类和实现了List接口。但是AbstractList它也实现了List接口,那为什么ArrayList不直接实现List接口呢?

这里是有一个思想,接口中全都是抽象的方法,而抽象类中可以有抽象方法,还可以有具体的实现方法,正是利用了这一点,让AbstractList实现接口中一些通用的方法,而具体的类,如ArrayList就继承这个AbstractList类,拿到一些通用的方法,然后自己在实现一些自己特有的方法,这样一来,让代码更简洁,就继承结构最底层的类中通用的方法都抽取出来,先一起实现了,减少重复代码。所以一般看到一个类上面还有一个抽象类,应该就是这个作用。

类中的属性

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // 版本号
    private static final long serialVersionUID = 8683452581122892189L;
    // 缺省容量 
    // 不是在我们调用无参构造创建list的时候它的底层数组就已经设置好长度了 而是当我们add第一个元素的 这个长度才来初始化
    private static final int DEFAULT_CAPACITY = 10;
    // 空对象数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // 缺省空对象数组  
    // 用于默认大小的空实例的共享空数组实例。
    // 我们将其与 EMPTY_ELEMENTDATA 区分开来,以了解添加第一个元素时要膨胀多少。
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    // 元素数组
    // ArrayList 的元素存储在其中的数组缓冲区。
    // ArrayList 的容量就是这个数组缓冲区的长度。任何带有 
    // elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空 ArrayList
    // 添加第一个元素时将扩展为 DEFAULT_CAPACITY。
    transient Object[] elementData;
    // 实际元素大小,默认为0
    private int size;
    // 最大数组容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}
复制代码

构造方法

ArrayList有三个构造方法

image.png

1)无参构造方法

/**
    * Constructs an empty list with an initial capacity of ten.  这里就说明了默认会给10的大小,所以说一开始arrayList的容量是10.
    */
//ArrayList中储存数据的其实就是一个数组,这个数组就是elementData,在123行定义的 private transient Object[] elementData;
public ArrayList() {  
    super();        //调用父类中的无参构造方法,父类中的是个空的构造方法
    this.elementData = EMPTY_ELEMENTDATA;//EMPTY_ELEMENTDATA:是个空的Object[], 将elementData初始化,elementData也是个Object[]类型。空的Object[]会给默认大小10,等会会解释什么时候赋值的。
}
复制代码

2)有参构造一

    /**
     * 构造一个具有指定初始容量的空列表
     *
     * @param  initialCapacity  列表的初始容量
     * @throws 如果指定的初始容量为负数,则抛出 IllegalArgumentException
     */
    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);
        }
    }
复制代码

小结:arrayList的构造方法就做一件事情,就是初始化一下储存数据的容器,其实本质上就是一个数组,在其中就叫elementData。

核心方法:

1、add方法

image.png

1)boolean add(E);//默认直接在末尾添加元素

/**
* 把元素添加到集合的末尾
*/
public boolean add(E e) {
    
    //确定数组容量是否足够,size是数据个数,所以+1. 
    ensureCapacityInternal(size + 1); 
    
    //在数组中正确的位置上添加元素e,并size++
    elementData[size++] = e;
    
    return true;
}
复制代码

ensureCapacityInternal(xxx); 确定内部容量的方法 

//用于确定数组容量
private void ensureCapacityInternal(int minCapacity) {
    
    //判断初始化elementdata是否为一个空数组
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        
        //如果是,相当于是空数组没有长度,则获取“默认的容量”和“传入参数”两者之间的最大值
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);

    }
    //确认实际的容量,判断是否需要进行扩容操作
    ensureExplicitCapacity(minCapacity);
}
复制代码

ensureExplicitCapacity(xxx);

//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
    //记录修改次数
    modCount++;

    //如果最小大小 减 数组长度 大于0 -> 进行数组扩容
    if (minCapacity - elementData.length > 0)
        //实际进入扩容机制
        grow(minCapacity);
}
复制代码

grow(xxx); arrayList核心的方法,能扩展数组大小的真正秘密。

/**
* ArrayList扩容的核心方法。
*/
private void grow(int minCapacity) {
     // oldCapacity为旧容量,newCapacity为新容量
    
    //将扩充前的elementData大小给oldCapacity
    int oldCapacity = elementData.length;
	
    //将oldCapacity 右移一位,其效果相当于oldCapacity/2
    //newCapacity就是1.5倍的oldCapacity
    int newCapacity = oldCapacity + (oldCapacity >> 1);

     //检查新容量是否大于最小需要容量,若小于最小需要容量,那么就把最小需要容量当作数组的新容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

    /**
     * 检查新容量是否超出了ArrayList所定义的最大容量,
     * 若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
     * 如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
    */
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        //比较minCapacity和 MAX_ARRAY_SIZE
        newCapacity = hugeCapacity(minCapacity);
    
    //新的容量大小已经确定好了,就copy数组,改变容量大小
    elementData = Arrays.copyOf(elementData, newCapacity);
}

复制代码

hugeCapacity();

//这个就是上面用到的方法,很简单,就是用来赋最大值。
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    //如果minCapacity都大于MAX_ARRAY_SIZE,那么就Integer.MAX_VALUE返回,反之将MAX_ARRAY_SIZE返回
    return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
复制代码

2)void add(int,E);在特定位置添加元素,也就是插入元素


/**
* 在此列表中的指定位置插入指定的元素。 
*先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大;
*再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。
*/
public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    
    //arraycopy()这个实现数组之间复制的方法一定要看一下,下面就用到了arraycopy()方法实现数组自己复制自己
    System.arraycopy(elementData, index, elementData, index + 1,size - index);
    elementData[index] = element;
    size++;
}
复制代码

rangeCheckForAdd(index) 

    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)   //插入的位置肯定不能大于size 和小于0
        //如果是,就报这个越界异常
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
复制代码

总结:

正常情况下会扩容1.5倍,特殊情况下(新扩展数组大小已经达到了最大值)则只取最大值。
当我们调用add方法时,实际上的函数调用如下:

image.png

说明:程序调用add,实际上还会进行一系列调用,可能会调用到grow,grow 会调用hugeCapacity。

remove()方法

image.png

其实这几个删除方法都是类似的。我们选择几个讲,其中fastRemove(int)方法是private的,是提供给remove(Object)这个方法用的。

remove(int) 删除指定位置上的元素

public E remove(int index) {
    rangeCheck(index);//检查index的合理性

    modCount++;//增加修改次数
    E oldValue = elementData(index);//通过索引直接找到该元素

    int numMoved = size - index - 1;//计算要移动的位数。
    if (numMoved > 0)
        //这个方法也已经解释过了,就是用来移动元素的。
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    //将--size上的位置赋值为null,让gc(垃圾回收机制)更快的回收它。
    elementData[--size] = null; // clear to let GC do its work
    //返回删除的元素。
    return oldValue;
}
复制代码

remove(Object)

这个方法可以看出来,arrayList是可以存放null值得。

//通过元素来删除该元素,就依次遍历,如果有这个元素,就将该元素的索引传给fastRemobe(index),使用这个方法来删除该元素,
//fastRemove(index)方法的内部跟remove(index)的实现几乎一样,这里最主要是知道arrayList可以存储null值
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;
}
复制代码

image.png

clear()

将elementData中每个元素都赋值为null,等待垃圾回收将这个给回收掉,所以叫clear

public void clear() {
    modCount++;

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

    size = 0;
}
复制代码

总结:

remove函数用户移除指定下标的元素,此时会把指定下标到数组末尾的元素向前移动一个单位,并且会把数组最后一个元素设置为null,这样是为了方便之后将整个数组不被使用时,会被GC,可以作为小的技巧使用。

set()方法

设定指定下标索引的元素值

public E set(int index, E element) {
    // 检验索引是否合法
    rangeCheck(index);
    // 旧值
    E oldValue = elementData(index);
    // 赋新值
    elementData[index] = element;
    // 返回旧值
    return oldValue;
}
复制代码

indexOf()方法

从头开始查找与指定元素相等的元素,注意,是可以查找null元素的,意味着ArrayList中可以存放null元素的。与此函数对应的lastIndexOf,表示从尾部开始查找。

// 从首开始查找数组里面是否存在指定元素
public int indexOf(Object o) {
    if (o == null) { // 查找的元素为空
        for (int i = 0; i < size; i++) // 遍历数组,找到第一个为空的元素,返回下标
            if (elementData[i]==null)
                return i;
    } else { // 查找的元素不为空
        for (int i = 0; i < size; i++) // 遍历数组,找到第一个和指定元素相等的元素,返回下标
            if (o.equals(elementData[i]))
                return i;
    } 
    // 没有找到,返回空
    return -1;
}
复制代码

get()方法

public E get(int index) {
    // 检验索引是否合法
    rangeCheck(index);
    return elementData(index);
}
复制代码

get函数会检查索引值是否合法(只检查是否大于size,而没有检查是否小于0),值得注意的是,在get函数中存在element函数,element函数用于返回具体的元素,具体函数如下:

E elementData(int index) {
    return (E) elementData[index];
}
复制代码

返回的值都经过了向下转型(Object -> E),这些是对我们应用程序屏蔽的小细节。

总结

  1. ArrayList可以存放null
  2. ArrayList本质上是一个elementData数组
  3. ArrayList区别于数组的地方在于能够自动扩展大小,其中关键的方法就是grow()方法
  4. arrayList中removeAll(collection c)和clear()的区别就是removeAll可以删除批量指定的元素,而clear是全删除集合中的元素。
  5. arrayList由于本质是数组,所以它在数据的查询方面会很快,而在插入删除这些方面,性能下降很多,有移动很多数据才能达到应有的效果
  6. arrayList实现了RandomAccess,所以在遍历它的时候推荐使用for循环。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享