Java集合——ArrayList

1.ArrayList的基本介绍

官方API:java.util.ArrayList

官方解释永远是最权威的解释

List接口的大小可变数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。(此类大致上等同于 Vector 类,除了此类是不同步的。)
sizeisEmptygetsetiteratorlistIterator 操作都以固定时间运行。add 操作以分摊的固定时间 运行,也就是说,添加 n 个元素需要 O(n) 时间。其他所有操作都以线性时间运行(大体上讲)。与用于 LinkedList 实现的常数因子相比,此实现的常数因子较低。
每个 ArrayList 实例都有一个容量。该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向 ArrayList 中不断添加元素,其容量也自动增长。并未指定增长策略的细节,因为这不只是添加元素会带来分摊固定时间开销那样简单。
在添加大量元素前,应用程序可以使用 ensureCapacity 操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。
注意,此实现不是同步的。如果多个线程同时访问一个 ArrayList 实例,而其中至少一个线程从结构上修改了列表,那么它必须 保持外部同步。(结构上的修改是指任何添加或删除一个或多个元素的操作,或者显式调整底层数组的大小;仅仅设置元素的值不是结构上的修改。)这一般通过对自然封装该列表的对象进行同步操作来完成。
此类的 iteratorlistIterator 方法返回的迭代器会 fail-fast :在创建迭代器之后,除非通过迭代器自身的 removeadd 方法从结构上对列表进行修改,否则在任何时间以任何方式对列表进行修改,迭代器都会抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
注意,迭代器的fail-fast行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的fail-fast行为应该仅用于检测 bug。

ArrayList继承结构:


image.png

ArrayList的底层是使用数组实现的:

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

ArrayList的默认容量为10:

    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

ArrayList特点

  • 随机访问:访问快(随机访问)、增删慢(会移动数组元素)
  • 排列有序,可重复
  • 线程不安全
  • 容量不够时,增长率为0.5
  • contain()与remove()方法底层依赖的是equals()方法;若要比较引用类型数据,则需要重写equals方法;
  • ArrayList遍历(以及是否支持删除元素):
    • 普通for循环,删除后需要将索引减一;效率最高
    • 迭代器,使用迭代器的删除方法
    • 增强for循环(foreach):不能删除元素

2.ArrayList的构造方法

构造方法 描述
ArrayList(int initialCapacity) 构造一个 指定容量 的ArrayList
ArrayList() 构造一个 默认容量 的ArrayList
ArrayList(Collection c) 通过集合构造一个新的 ArrayList ,这个 ArrayList 包含指定集合中的所有元素
    /**
     * Constructs an empty list with the specified initial capacity.
     *
     * @param  initialCapacity  the initial capacity of the list
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
    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);
        }
    }

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
     */
    public ArrayList(Collection 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;
        }
    }

3 ArrayList的容量

与容量有关的属性
注:常量代表static final

属性 类型 说明
DEFAULT_CAPACITY 常量 int 10 默认容量,构造ArrayList时没有指定大小时ArrayList的容量
EMPTY_ELEMENTDATA 常量 Object[] {} 用于空实例的共享空数组实例
DEFAULTCAPACITY_EMPTY_ELEMENTDATA 常量 Object[] {} 用于默认大小的空实例的共享空数组实例。将其与 EMPTY_ELEMENTDATA 区分开来,以知道在添加第一个元素时要扩张多少
elementData Object[] ArrayList底层存储ArrayList元素的数组缓冲区
size int ArrayList中实际元素的个数,并有:size <= elementData.length

ArrayList的容量就是elementData.length,是这个ArrayList能够存放多少个元素,这个elementData相当于容器。而size指的是这个容器中已经存放的元素数量。一般对于使用者来说,关心的是ArrayList中存放的元素数量,即size,因为ArrayList可以自动扩容;但是要了解ArrayList的实现原理,就需要了解容量的相关知识。


通过上面的构造方法可以知道,构造ArrayList时可以不指定大小,使用默认大小(10) ;也可以指定ArrayList的容量大小;甚至可以传入一个集合来构造ArrayList,集合的大小就是ArrayList的容量。

Tips:

  • EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的区别:前者表示的是一个空数组,后者表示的也是一个空数组,但是不同在于后者是可以扩容的,当往进添加首个元素的时候就会触发扩容机制,容量会扩容到10个长度。
  • elementData字段是保存元素的缓冲数组,被transient修饰表示它不会被序列化,这意味着集合对象保存的元素不会被自动序列化,所以后面添加了writeObject和readObject方法,用来序列化和反序列化数组中的元素。

3.1 使用默认容量(空参构造)

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

通过代码可以看见,空参构造方法给elementData初始化为DEFAULTCAPACITY_EMPTY_ELEMENTDATA;而DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空数组。怎么会说把它初始化为10呢?
为了验证他的容量是0还是10,我们写段代码验证一下:

        ArrayList list = new ArrayList();
        System.out.println(getArrayListCapacity(list));    // 反射获取elementData.length,输出 0
   /**
    * 通过反射机制获取ArrayList 的容量
    * @param arrayList
    * @return
    */
   public static int getArrayListCapacity(ArrayList arrayList) {
       Class arrayListClass = ArrayList.class;
       try {
           Field field = arrayListClass.getDeclaredField("elementData");
           field.setAccessible(true);
           Object[] objects = (Object[])field.get(arrayList);
           return objects.length;
       } catch (NoSuchFieldException e) {
           e.printStackTrace();
           return -1;
       } catch (IllegalAccessException e) {
           e.printStackTrace();
           return -1;
       }
   }

这里输出的确实是0,那为什么我们又说默认容量为10呢?
初始化ArrayList之后,肯定会添加元素,所以,我们先来粗略的看一下add()方法:

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return true (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

ArrayList的add()有很多重载方法,我们先不细究,但是每个add()都有一行类似下面的代码

    ensureCapacityInternal(size + 1);  // Increments modCount!!

所以我们看一下这个ensureCapacityInternal(int)

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

发现他又调用了一个ensureExplicitCapacity(int)方法,传入的参数是calculateCapacity(elementData, minCapacity):

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

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

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

下面我们来简要分析一下这几个方法:
顾名思义(这就是命名规范的好处之一),

方法 参数 说明
ensureCapacityInternal() 期望的最小容量值(size+添加个数) 确保内部容量(满足需求)的方法
ensureExplicitCapacity() 计算后期望的容量 确保明确(指定)的容量:是否需要调用grow()扩容
calculateCapacity() Object数组elementData 和 期望的最小容量值(size+添加个数) 计算容量:通过Object[] 和 期望的最小容量计算出明确的的期望容量值

tips:
calculateCapacity()其实就是为了默认构造方法中给elementData赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA后设计的扩容机制:初始化默认容量为ArrayList提供的默认容量 或 添加元素的个数(add添加一个,addAll可以添加多个);这里需要说明一下,默认容量相当于一个阈值,是为了避免添加一个元素后,容量不至于太小,再次添加元素需要重新扩容(并且扩容的空间也很小),调用grow()中的Arrays.copyOf()创建新数组,效率低;但是如果使用addAll()添加的元素个数大于默认容量了,ArrayList提供的默认容量就不够使用,满足需求的最小容量就应该是添加的元素个数。

这属于ArrayList的扩容机制,先不深究,但是眼睛毒辣的朋友肯定发现了一句解答上面问题的关键代码:


image.png

DEFAULTCAPACITY_EMPTY_ELEMENTDATA不就是空参构造方法里给elementData赋值的常量吗?如果elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,空参构造后就满足这个条件,之后返回DEFAULT_CAPACITY与minCapacity(这里传入的是size+1,size为0)的最大值,就是DEFAULT_CAPACITY(上面提到的默认容量:10)。
是不是已经接近答案了?我们继续深入:当添加一个元素时,将这个DEFAULT_CAPACITY传入ensureExplicitCapacity(),通过判断这个容量与elementData.length(当前容量)的大小,当前容量不足则扩容,调用grow()方法:

    /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);//这里是向下取整,比如15扩容后是22
        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);
    }

这里就是ArrayList的扩容机制:

扩容机制 条件
扩容50% 不满足下面两个扩容机制条件的
扩容为传入容量 扩容为1.5倍后小于传入的容量
扩容为最大容量 扩容1.5倍后超过了ArrayList提供的最大容量

刚初始化时容量为0,扩容后还是0,小于传入的minCapacity,如果传入的是DEFAULT_CAPACITY,则扩容为DEFAULT_CAPACITY。
由于add()添加一个的使用频率比较高,所以传入的size+1为1,需要给grow传入DEFAULT_CAPACITY,所以我们通常说ArrayList的默认容量为10.

3.2 使用指定容量(参数为int型的容量)

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

这段代码也比较简单,就是判断一下容量与0的大小

  • 如果大于0,直接构造一个此容量的数组
  • 如果等于0,将ArrayList的共享空实例常量EMPTY_ELEMENTDATA赋值给elementData(这里就是EMPTY_ELEMENTDATA与DEFAULTCAPACITY_EMPTY_ELEMENTDATA区分的地方,传入0作为初始化容量,ArrayList不会把默认容量拿来初始化,后面有代码验证)
  • 如果小于0,抛出IllegalArgumentException异常提示参数非法

3.3 使用集合

public ArrayList(Collection 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;
        }
    }

使用集合构造一个ArrayList与上面的区别不是很大,做了集合元素是否为空的判断:先让elementData初始化为集合转换的数组:

  • 如果数组长度不为空,就判断elementData记录的数组类型是否为Object [] ,如果不是,就需要将其转换为Object []类型
  • 如果数组长度为空,就将elementData初始化为共享的空实例EMPTY_ELEMENTDATA

3.4 代码验证扩容机制

由于elementData外部无法访问,所以写了一段代码用反射机制获取集合的容量elementData.length:

    /**
     * 通过反射机制获取ArrayList 的容量
     * @param arrayList
     * @return
     */
    public static int getArrayListCapacity(ArrayList arrayList) {
        Class arrayListClass = ArrayList.class;
        try {
            Field field = arrayListClass.getDeclaredField("elementData");
            field.setAccessible(true);
            Object[] objects = (Object[])field.get(arrayList);
            return objects.length;
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
            return -1;
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            return -1;
        }
    }

然后基于这个动态获取容量的方法,编写了一段代码验证以上理论:

/**
 * ClassName: MyCollectionCapacityTest
 * Description:
 * date: 2019/11/23 22:48
 *
 * @author seaxll
 * @since JDK 1.8
 */
public class MyCollectionCapacityTest {
    public static void main(String[] args) {
        // 分别用不同参数初始化ArrayList:空参(默认容量)、0、5
        ArrayList listWithDefaultCapacity = new ArrayList<>();
        ArrayList listWithInitCapacityZero = new ArrayList<>(0);
        ArrayList listWithInitCapacity = new ArrayList<>(5);

        // 打印初始化信息:初始容量、初始size
        System.out.println("create a ArrayList with no parameter default capacity:");
        System.out.println( "init:  elementData.length:\t" + getArrayListCapacity(listWithDefaultCapacity) +
                "\tsize:\t" + listWithDefaultCapacity.size());
        System.out.println("----------------------------------------I am is a Dividing line --------------------------------");
        System.out.println("create a ArrayList used capacity 0:");
        System.out.println( "init:  elementData.length:\t" + getArrayListCapacity(listWithInitCapacityZero) +
                "\tsize:\t" + listWithInitCapacityZero.size());
        System.out.println("----------------------------------------I am is a Dividing line --------------------------------");
        System.out.println("create a ArrayList used capacity 5:");
        System.out.println( "init:  elementData.length:\t" + getArrayListCapacity(listWithInitCapacity) +
                "\tsize:\t" + listWithInitCapacity.size());
        System.out.println("----------------------------------------I am is a Dividing line --------------------------------");

        // 扩容并打印信息
        System.out.println("\t\t\t" + "no parameter" + "\tinitCapacity = 0"  + "\tinitCapacity = 5");
        System.out.println("init :\t\t" + getArrayListCapacity(listWithDefaultCapacity) +
                "\t\t\t\t" + getArrayListCapacity(listWithInitCapacityZero) +
                "\t\t\t\t\t" + getArrayListCapacity(listWithInitCapacity));
        // 添加50个验证扩容机制,这里扩容1.5倍,向下取整,下面代码可以验证。
        for (int i = 0; i < 50; ) {
            listWithDefaultCapacity.add(i);
            listWithInitCapacityZero.add(i);
            listWithInitCapacity.add(i);
            System.out.println("size = " + (++i) +
                    "\t" + getArrayListCapacity(listWithDefaultCapacity) +
                    "\t\t\t\t" + getArrayListCapacity(listWithInitCapacityZero) +
                    "\t\t\t\t\t" + getArrayListCapacity(listWithInitCapacity));
        }

        // arrayListCapacityTest();
        // test();
    }

输出:

create a ArrayList with no parameter default capacity:
init:  elementData.length:  0   size:   0
----------------------------------------I am is a Dividing line --------------------------------
create a ArrayList used capacity 0:
init:  elementData.length:  0   size:   0
----------------------------------------I am is a Dividing line --------------------------------
create a ArrayList used capacity 5:
init:  elementData.length:  5   size:   0
----------------------------------------I am is a Dividing line --------------------------------
            no parameter    initCapacity = 0    initCapacity = 5
init :      0               0                   5
size = 1    10              1                   5
size = 2    10              2                   5
size = 3    10              3                   5
size = 4    10              4                   5
size = 5    10              6                   5
size = 6    10              6                   7
size = 7    10              9                   7
size = 8    10              9                   10
size = 9    10              9                   10
size = 10   10              13                  10
size = 11   15              13                  15
size = 12   15              13                  15
size = 13   15              13                  15
size = 14   15              19                  15
size = 15   15              19                  15
size = 16   22              19                  22
size = 17   22              19                  22
size = 18   22              19                  22
size = 19   22              19                  22
size = 20   22              28                  22
size = 21   22              28                  22
size = 22   22              28                  22
size = 23   33              28                  33
size = 24   33              28                  33
size = 25   33              28                  33
size = 26   33              28                  33
size = 27   33              28                  33
size = 28   33              28                  33
size = 29   33              42                  33
size = 30   33              42                  33
size = 31   33              42                  33
size = 32   33              42                  33
size = 33   33              42                  33
size = 34   49              42                  49
size = 35   49              42                  49
size = 36   49              42                  49
size = 37   49              42                  49
size = 38   49              42                  49
size = 39   49              42                  49
size = 40   49              42                  49
size = 41   49              42                  49
size = 42   49              42                  49
size = 43   49              63                  49
size = 44   49              63                  49
size = 45   49              63                  49
size = 46   49              63                  49
size = 47   49              63                  49
size = 48   49              63                  49
size = 49   49              63                  49
size = 50   73              63                  73

Process finished with exit code 0

4 ArrayList中的一些方法(部分API源代码,逻辑比较简单,可以忽略此内容)

4.1添加

4.1.1 添加指定元素

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

ensureCapacityInternal方法主要用于校验当前List的容量是否已经达到极限,如果达到极限需要进行扩容。具体参照上面扩容解析。

剩下的就是添加新元素的逻辑,简单至极,直接将新元素到添加到底层数组elementData的下一下标位size++即可。

4.1.2 添加指定元素到指定位置

除了在末尾添加元素之外,在指定位置添加元素的时候,都依赖于 System.arraycopy()方法将指定位置的元素后的元素后移。

    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,index必须小于size的值,并大于等于0。

然后同样校验容量是否达到极限,达到极限需要扩容。

之后执行一个本地方法,System.arraycopy方法用于将指定位置及其后面的所有元素整个通过复制迁移到从index+1开始的位置,即整体后移一位,将index位空出来用于保存新元素。

最后将新元素添加到空出的index位置。

4.1.3 将指定集合中的元素添加到List末尾

    public boolean addAll(Collection c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

首先将给定的集合转换为数组Object[]。

然后以目标List的size+给定集合转化的数组的容量为总容量进行容量校验,若容量不足,执行扩容操作。

再然后通过本地方法执行数组复制操作将给定集合转换数组的元素复制到目标List的底层数组的尾部。

最后不要忘记将size增加。

4.1.4 将指定集合中的元素添加到List指定位置

    public boolean addAll(int index, Collection c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

首先校验下标index,index必须小于size,大于等于0。

然后将给定集合转换为数组Object[],再执行容量校验,扩容操作。

通过本地方法数组复制操作将给定位置开始的所有元素整体后移一定的距离,具体的距离为给定集合转换后数组的容量大小,这样就能空出容量大小的空位来存放给定的集合元素。

最后再次通过本地数组复制方法将给定的集合转换的数组元素整体复制到上一步空出来的位置上。

4.2 修改

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

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

修改指定位置的元素为新元素,首先需要校验给定index的值,index必须大于等于0,小于size,然后将新元素保存到index位置,并将旧元素返回。

4.3 获取

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

        return elementData(index);
    }

获取指定下标位置的元素值,首先需要校验给定的下标index,index必须大于等于0,小于size。

4.4 根据元素查询索引

    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;
    }
    public int lastIndexOf(Object o) {
        if (o == null) {
            for (int i = size-1; i >= 0; i--)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = size-1; i >= 0; i--)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

indexOf是通过正序遍历的方式搜索给定的元素的下标,lastIndexOf是通过逆序遍历的方式搜索给定元素的下标,这两个方法找到的下标都是正序或者逆序该元素首次出现的位置下标。如果o为null,那么将会搜索第一个null值元素的下标。

4.5 移除

4.5.1 移除指定下标的元素

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

首先校验给定的下标值index,index必须小于size,这里的校验和添加元素的下标校验有点不同:

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

前者是此处的index校验,后者是添加元素的index校验。

那为何后者比前者要多一个index<0的校验呢,那是因为在add(int,E)方法中,校验完成后紧接着就是调用本地方法进行数组复制操作,如果index小于0,那么出错位置在C代码中,无法在Java代码中得以体现,所以提前进行校验,保证调用本地C代码之前参数的准确性。前者校验完成之后,紧接着的是Java代码获取指定下标的元素,如果下标小于0,也会出错但是JVM会抛出异常,不会无声无息,所以没有必要校验是否小于0。

index校验完成后,通过本地方法数组复制将index+1及其之后的元素整体复制到index位置。

最后将原来的最后一个位置元素置空。

4.5.2 移除指定元素

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

首先通过循环操作找到首个指定的元素,然后将针对找到的元素执行删除操作。

删除操作还是依靠本地的数组复制操作完成的。

4.5.3 清空元素

    public void clear() {
        modCount++;

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

        size = 0;
    }

至于清空元素,就是通过循环将List中的每个元素都删除,将整个List置空。

4.5.4 移除当前List中所有(不)包含在给定集合中的元素

    // 删除当前List中所有包含在给定集合中的元素
    public boolean removeAll(Collection c) {
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    }
    // 删除当前List中所有不包含在给定集合中的元素
    public boolean retainAll(Collection c) {
        Objects.requireNonNull(c);
        return batchRemove(c, true);
    }
    private boolean batchRemove(Collection c, boolean complement) {
        final Object[] elementData = this.elementData;
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }

4.6 遍历

ArrayList的遍历方式有很多:

4.6.1 ListIterator

ListIterator是继承自Iterator的,在其基础上添加了反向遍历的功能方法。

    // 截取从执行下标开始的元素组成迭代器实例,进行遍历
    public ListIterator listIterator(int index) {
        if (index < 0 || index > size)
            throw new IndexOutOfBoundsException("Index: "+index);
        return new ListItr(index);
    }
    // 将集合中所有元素组成迭代器实例,进行遍历
    public ListIterator listIterator() {
        return new ListItr(0);
    }

源码中的ListItr是ListIterator的实现类。

4.6.2 Iterator

Iterator拥有正向遍历的功能。

    public Iterator iterator() {
        return new Itr();
    }

4.6.3 Spliterator

Spliterator是分割迭代器,是JDK1.8的新功能。几乎每个集合都实现了它,它是实现并行遍历的基础。使用这个迭代器,可以实现多线程遍历集合的功能,其中每个线程遍历集合中的一段,因此没有线程安全问题。

    public Spliterator spliterator() {
        return new ArrayListSpliterator<>(this, 0, -1, 0);
    }
public interface Spliterator {
    // 针对单个元素执行某个行为
    // 接收一个行为(Lambda表达式或方法引用)
    boolean tryAdvance(Consumer action);
    // 针对所有元素执行某个行为,通过tryAdvance实现
    // 接收一个行为(Lambda表达式或方法引用)
    default void forEachRemaining(Consumer action) {
        do { } while (tryAdvance(action));
    }
    // 分割集合,返回一个新的Spliterator迭代器,
    // 一旦元素被返回的迭代器携带,则从当前迭代器消失
    // 一般分割是截取一半
    Spliterator trySplit();
    // 获取剩余遍历元素的数量的估计值
    long estimateSize();
    // 当前迭代器拥有SIZED特征值时,返回剩余元素个数,否则返回-1
    default long getExactSizeIfKnown() {
        return (characteristics() & SIZED) == 0 ? -1L : estimateSize();
    }
    // 返回当前迭代器的特征值
    int characteristics();
    // 校验当前迭代器是否拥有指定的特征值
    default boolean hasCharacteristics(int characteristics) {
        return (characteristics() & characteristics) == characteristics;
    }
    // 返回比较器
    default Comparator getComparator() {
        throw new IllegalStateException();
    }
}

4.6.4 forEach

forEach方式是java 1.8中新增的方式,接受一个行为作为参数,即接收一个方法引用或者Lambda表达式。

    // action代表接受的行为,是一个函数式接口类型Consumer,表示消费之意,消费就是将资源处理掉,所以有一个入参,无返回值。
    public void forEach(Consumer action) {
        Objects.requireNonNull(action);
        final int expectedModCount = modCount;
        @SuppressWarnings("unchecked")
        final E[] elementData = (E[]) this.elementData;
        final int size = this.size;
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            action.accept(elementData[i]);// 执行函数式接口行为
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

实例:

public class ArrayListTest {
    public static void main(String[] args) {
        List list = new ArrayList<>();
        list.addAll(Arrays.asList("123","444444","2123"));
        ListIterator listIterator = list.listIterator();// 第一种
        listIterator.forEachRemaining(System.out::println);
        System.out.println("-------------");
        ListIterator listIterator1 = list.listIterator(1);// 第二种
        listIterator1.forEachRemaining(System.out::println);
        System.out.println("-------------");
        Iterator iterator = list.iterator();// 第三种
        iterator.forEachRemaining(System.out::println);
        System.out.println("-------------");
        Spliterator spliterator = list.spliterator();// 第四种
        spliterator.forEachRemaining(System.out::println);
        System.out.println("-------------");
        list.forEach(System.out::println);// 第五种
    }
}

4.7 校验

4.7.1 是否为空

    public boolean isEmpty() {
        return size == 0;
    }

size表示的就是List中包含的元素的个数。

4.7.2 是否包含某元素

    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

该校验通过indexOf()方法来实现,如果能找到元素的下标,则存在,否则不存在。

4.8 排序

Java中排序可以通过两种方式实现:

  • 实现Comparable接口
  • 使用Comparator比较器

这里很明显ArrayList的继承体系中并无Comparable接口,那么只能通过Comparator来实现,这就涉及到了ArrayList中的sort方法:

    public void sort(Comparator c) {
        final int expectedModCount = modCount;
        Arrays.sort((E[]) elementData, 0, size, c);
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }

使用这种方式来排序需要传递一个Comparator比较器作为参数,最简单的方式就是匿名内部类方式,在Java 1.8之后直接使用Lambda来实现。

public class ComparatorTest {
    public static void main(String[] args) {
        List list = new ArrayList<>();
        list.addAll(Arrays.asList("123","45612","7839"));
        list.sort((o1, o2) -> o1.length()-o2.length());
        list.forEach(System.out::println);
    }
}

执行结果为:

123
7839
45612

4.9 克隆

因为ArrayList实现了Cloneable接口,重写了clone方法,便拥有了对象克隆的功能。

    public Object clone() {
        try {
            ArrayList v = (ArrayList) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }

这是一个浅拷贝的实现。

4.10 序列化/反序列化

由于ArrayList中使用transient修饰了elementData,它代表的是底层的元素数组,序列化的主要内容就是它,或者说是它里面的内容,而它又无法被序列化,因此我们只能通过自定义writeObject方法来手动序列化,定义readObject方法来手动反序列化。

    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();

        // Write out size as capacity for behavioural compatibility with clone()
        s.writeInt(size);

        // Write out all elements in the proper order.
        for (int i=0; i

4.11 其他方法

4.11.1 trimToSize()方法:

    /**
     * 将此ArrayList实例的容量修剪为列表的当前大小。
     * 程序可以使用此操作最小化ArrayList实例的存储。
     *
     * 注意:这里只是根据size的大小去除elementData中的空元素,而不是通过判断 == null 去掉null的元素:
     *  1) 通过remove(int index)方法(删除index位置,并将index后的元素前移),在后面移动空出的位置会被 剪切 掉
     *  2) 如果通过set()方法将某个索引的位置(不论是在中间还是末尾)为null,其size不变,并不会被移出
     *  3) 此外,trimToSize还会剪切扩容后(或初始化ArrayList容量时)多出的空余位置。
     */
    public void trimToSize() {
        // modCount:继承自 AbstractList 中的属性,记录着 ArrayList 被修改的次数
        // modCount 与 fail-fast 机制有关 (concurrentModficationException)
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
                    ? EMPTY_ELEMENTDATA
                    : Arrays.copyOf(elementData, size);
        }
    }

验证代码:

    public void testTrimToSize(){
        ArrayList arrayList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            arrayList.add(String.valueOf(i));
        }
        // arrayList.remove(4); // 9    [0, 1, 2, 3, 5, 6, 7, 8, 9]
        arrayList.set(4,null);  // 10   [0, 1, 2, 3, null, 5, 6, 7, 8, 9]
        arrayList.trimToSize();
        System.out.println(arrayList.size() + " \t" + arrayList);
    }

5 其他

5.1 modCount

该字段表示list结构上被修改的次数。结构上的修改指的是那些改变了list的长度大小或者使得遍历过程中产生不正确的结果的其它方式。
该字段被Iterator以及ListIterator的实现类所使用,如果该值被意外更改,Iterator或者ListIterator 将抛出ConcurrentModificationException异常。
这是jdk在面对迭代遍历的时候为了避免不确定性而采取的fast-fail原则。
子类对此字段的使用是可选的,如果子类希望支持快速失败,只需要覆盖该字段相关的所有方法即可。单线程调用不能添加删除terator正在遍历的对象,否则将可能抛出ConcurrentModificationException异常,如果子类不希望支持fast-fail,该字段可以直接忽略。


/**
 * ClassName: ArrayListTest
 * Description:
 * date: 2019/11/24 10:24
 *
 * @author seaxll
 * @since JDK 1.8
 */
public class ArrayListTest {
    public static void main(String[] args) {
        removeTest();
    }
    public static void removeTest(){
        List list = new ArrayList();
        for (int i = 0; i < 5; i++) {
            list.add(i);
        }

        Iterator it = list.iterator();
        int i = 0;
        while(it.hasNext()){
            if(i==2){
                it.remove();// 如果用list.remove(it.next());会报异常
            }
            System.out.println("第"+i+"个元素"+it.next());
            i++ ;
        }
        System.out.println("----------------");
        Iterator it2 = list.iterator();
        while(it2.hasNext()){
            System.out.println(it2.next());
        }
    }
}

另:注意it.remove()删除的是最近的一次it.next()获取的元素,而不是当前iterator中游标指向的元素!!
因此,本例中i==2时,删除的其实是1,而不是2,这很容易被忽视或者误解。如果想删掉2,正确操作是先调用it.next()获取到具体元素,再判断;而且由于调用了it.next(),此时游标已经指向我们期望删除的值了。想直接数数字进行删除,在这里会容易出错误。可以理解为此处的下标从1开始

其实我们可以查看Iterator的源码来验证:

public Iterator iterator() {
    return new Itr();
}

返回的是一个Itr对象,这个Itr是ArrayList的内部类

private class Itr implements Iterator {
    int cursor;       // index of next element to return   //下一个元素的游标
    int lastRet = -1; // index of last element returned; -1 if no such  //上一个元素的
    int expectedModCount = modCount;  //修改计数器期望值
 
    public boolean hasNext() {
        return cursor != size;
    }
    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        int i = cursor; //此时的游标,指向的是本次要遍历的对象,因为上一次已经++了,初始值为0,没有++的情况下是第一个元素
        if (i >= size)   //越界了
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;  //游标指向了下一个元素, 但 i 的值没有变
        return (E) elementData[lastRet = i];   //将 i 赋值给lastRet,取的值是方法开始时int i=cursor;中的cursor指向的值,而且最终这个游标的数值赋值给了lastRet
    }
    public void remove() {
        if (lastRet < 0)  // 如果没有next()操作就直接remove的话,lastRet=-1,会抛异常
            throw new IllegalStateException();
        checkForComodification();
        try {
            ArrayList.this.remove(lastRet); // remove之前,cursor、lastRet的值没有修改,都是上次next之后的值,因此此处的lastRet指向上次next获取的元素
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;  // 手动将ArrayList.remove()后modCount的值赋给expectedModCount,避免引起不一致
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer consumer) {
        Objects.requireNonNull(consumer);
        final int size = ArrayList.this.size;
        int i = cursor;
        if (i >= size) {
            return;
        }
        final Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) {
            throw new ConcurrentModificationException();
        }
        while (i != size && modCount == expectedModCount) {
            consumer.accept((E) elementData[i++]);
        }
        // update once at end of iteration to reduce heap write traffic
        cursor = i;
        lastRet = i - 1;
        checkForComodification();
    }
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

以上代码告诉我们,iterator.remove()实际是remove了上次next返回的元素,并且为了防止ConcurrentModificationException异常,手动修复了修改计数的期望值,而且如果没有经过next操作就直接remove的话,会因为初始的lastRet=-1而抛出IllegalStateException异常。

6.总结

  • ArrayList底层采用数组实现,拥有快速随机访问能力,但是非线程安全的集合。
  • ArrayList默认容量为10,扩容规则为当要保存的新元素所需的容量不足时触发,基本规则为扩容1.5倍。
  • 如果在遍历的时候发生结构性变化,会触发ConcurrentModificationException异常。
  • ArrayList支持序列化功能,支持克隆(浅拷贝)功能,排序功能等。

以上只是我——一个java新手学习时的一点思考与笔记,如果有朋友刷到这篇笔记,并且赏脸看了这篇笔记,如果发现了有不对的地方,还请不吝赐教。

你可能感兴趣的:(Java集合——ArrayList)