Java 容器之 ArrayList

一、概述

ArrayList 在日常开发的过程中经常使用到,所以了解它的底层实现是比较有必要的。既然带有 Array 的字眼,那么它的内部无疑也就是用数组来存储值的,和普通的数组不同,它是一个动态数组,也就是说在往内部添加元素的时候我们可以动态地对数组大小进行扩容。

本篇文章基于源码层面,对 ArrayList 的常用方法进行一些源码的简要分析,了解其内部的工作原理,并将它和 LinkedListVector 等做一个简要的比较。

另外需要特别注意的是 本篇文章的源码基于 JDK1.8,可能会与前面的版本略有不同

二、ArrayList 的源码分析

在源码分析中笔者分为以下 4 部分内容进行展开:

  • 类的继承关系
  • 类的属性
  • 类的构造方法
  • 常用方法

1. 类的继承关系

ArrayList 在源码中的定义如下:


public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable 

ArrayList 类的定义中,可获得信息如下:

  • ArratList 继承 AbstractList 抽象类。
  • 实现了 List 接口,它规定了 List 的操作规范。
  • 实现了 RandomAccess 接口,提供了随机访问。
  • 实现了 Cloneable 接口,可拷贝。
  • 实现了 Serializable 接口,可序列化。

2. 类的属性

了解了 ArrayList 的继承关系后,接下来看到类内部维护的成员:

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
	// 版本号
	private static final long serialVersionUID = 8683452581122892189L;
    // 数组的默认初始化大小
    private static final int DEFAULT_CAPACITY = 10;
	// 空对象数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
	// 默认空对象数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
	// 元素数组
    transient Object[] elementData; // non-private to simplify nested class access
    // 实际元素大小
    private int size;
    // 最大数组容量
	private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    ......
}

这里需要特别关注的几个成员如下:

  • elementData:实际的元素数组,所有的元素都保存在这个数组里面进行维护。
  • size:表示实际元素的大小(个数),注意它指的不是数组的大小。
  • DEFAULT_CAPACITY:默认容量,大小为 10。

3. 类的构造方法

类的构造方法有 3 个,如下所示:

public ArrayList()   // 使用默认数组大小,默认为0
public ArrayList(int initialCapacity)  // 指定数组的大小
public ArrayList(Collection<? extends E> c)  // 通过其他的集合对象来构造数组对象

首先看到第一个无参构造方法的源码:

public ArrayList() {
	// 将数组设置为空,此时数组长度为0
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

接着是指定数组大小的有参构造方法:

public ArrayList(int initialCapacity) {
	// 当初始容量值大于0时初始化数组
    if (initialCapacity> 0) {
        this.elementData = new Object[initialCapacity];
    } else {
        if (initialCapacity!= 0) {
        	// 小于0的时候直接抛出异常
            throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
        }
		// 等于0的时候为空数组
        this.elementData = EMPTY_ELEMENTDATA;
    }

}

最后是通过其它容器来初始化 ArrayList 的构造方法:

public ArrayList(Collection<? extends E> c) {
	// 将c转化成数组
    this.elementData = c.toArray();  
    if ((this.size = this.elementData.length) != 0) { // 判断长度是否不为空
    	// 判断是否能转化成Object类型数组
        if (this.elementData.getClass() != Object[].class) {
        	// 不能转化为Object类型数组就将c中的元素复制到elementData数组中
            this.elementData = Arrays.copyOf(this.elementData, this.size, Object[].class);
        }
    } else {
    	// 如果c为空容器就创建一个空数组
        this.elementData = EMPTY_ELEMENTDATA;
    }

}

在介绍完 ArrayList 的 3 个构造方法之后,接下来看看 ArrayList 的常用方法。

4. 常用方法

ArrayList 的常用方法如下所示:

  • add:往 ArrayList 中添加元素,它有 2 个重载方法。
  • remove:从 ArrayList 中删除元素,它有 2 个重载方法。
  • get:获取 ArrayList 中的元素。
  • indexOf:获取 ArrayList 中指定元素的索引值。
  • set:更新指定位置的元素值。
  • contains:判断 ArrayList 中是否包含参数中的元素。
  • isEmpty:判断 ArrayList 中元素个数是否为空。

4.1 add

add(E e) 的实现如下:

public boolean add(E e) {
	// 判断是否需要对数组进行扩容
    this.ensureCapacityInternal(this.size + 1);
    this.elementData[this.size++] = e;  // 往最后一个元素的后一个位置添加 e
    return true;
}

示意图如下:
在这里插入图片描述
add(int index, E e) 的实现如下:

public void add(int index, E e) {
	 // 检查index是否小于0或者大于size,如果是的话就会抛出异常
    this.rangeCheckForAdd(index); 
    // 判断是否需要对数组进行扩容
    this.ensureCapacityInternal(this.size + 1);
    // 将索引值index到size-1的元素整体向右移动一个位置
    System.arraycopy(this.elementData, index, this.elementData, index+ 1, this.size - index);
    this.elementData[index] = e;  // 插入新添加的值e
    ++this.size;
}

示意图如下:

在这里插入图片描述
add 方法中每次添加元素之前调用 ensureCapacityInternal 方法都会检查数组是否需要扩容,它的实现如下所示:

private void ensureCapacityInternal(int minCapacity) {
	// 判断elementData是否是一个空数组
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    	// 如果是的话取DEFAULT_CAPACITY和minCapacity之间的最大值作为数组大小
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

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

    // 检测minCapacity是否大于原先数组的容量,如果是的话对数组进行扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    // oldCapacity即为原先数组的容量
    int oldCapacity = elementData.length;
    // 将容量扩大为原来的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
    	// 如果扩容1.5倍仍然小于minCapacity,那么将新的数组容量直接设置为minCapacity
        newCapacity = minCapacity; 
    if (newCapacity - MAX_ARRAY_SIZE > 0)
    	// 数组容量有一个最大值限制,超过了最大值则newCapacity为Integer.MAX_VALUE
        newCapacity = hugeCapacity(minCapacity);

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

从上面对 ensureCapacityInternal 方法的分析以及结合前面对构造方法的分析,可以得知以下信息:

  • ArrayList 调用无参构造方法初始化时,数组容量为 0。
  • ArrayList 调用 add、addAll 等添加元素的方法时,会先检查数组是否需要扩容,如果原先数组长度为 0,那么数组的容量就会设置为 10,后面每次扩容至少都会扩充 1.5 倍。
  • 在初始化 ArrayList 对象时,如果我们预先知道大概要存储多少元素,那么可以调用有参构造方法进行构造,指定合适的数组容量,这样可以减少一些数组扩容消耗的时间。

4.2 remove

remove(int index) 的实现如下:

public E remove(int index) {
    this.rangeCheck(index);  // 检测索引是否合法
    ++this.modCount;
    Object oldValue = this.elementData(index);
    // 计算需要移动的元素个数
    int numMoved = this.size - index- 1;
    if (numMoved > 0) {
    	// 将需要移动的元素整体向左移动一个位置
        System.arraycopy(this.elementData, index+ 1, this.elementData, index, numMoved);
    }
	// 将原先最后的元素赋值为空
    this.elementData[--this.size] = null;
    return oldValue ;
}

示意图如下:
在这里插入图片描述
remove(Object o) 的实现如下:

public boolean remove(Object o) {
    int index;
    if (o == null) {
    	// 当o为空时遍历整个数组寻找空元素
        for(index= 0; index< this.size; ++index) {
            if (this.elementData[index] == null) {
            	// 如果存在空元素的话讲该空元素删除
                this.fastRemove(index);
                return true;
            }
        }
    } else {
    	// 当o不为空的时候遍历数组寻找和o相匹配的子元素
        for(index = 0; index< this.size; ++index) {
            if (o.equals(this.elementData[index])) {
            	// 如果存在匹配的元素的话删除这个元素
                this.fastRemove(index);
                return true;
            }
        }
    }
	// 当o为空时如果数组中不存在空元素则为返回false
	// 当o不为空时如果数组中不存在该元素返回false
    return false;
}

其中 fastRemove 方法的源码如下:

private void fastRemove(int index) {
    ++this.modCount;
    // 计算需要移动的元素个数
    int numMoved = this.size - index - 1;
    if (numMoved > 0) {
    	// 将需要移动的元素整体向左移动一个位置
        System.arraycopy(this.elementData, index + 1, this.elementData, index, numMoved );
    }
	// 将原先最后的元素赋值为空,方便GC
    this.elementData[--this.size] = null;
}

remove 方法的实现可以得知,remove 方法不会对数组的容量做相应的裁减,所以有可能会造成内存的浪费,如果在 remove 方法调用过后需要调整下数组的容量的话,可以调用 trimToSize 方法,它的实现如下:

public void trimToSize() {
    ++this.modCount;
    // 如果数组容量大于元素个数的话就会将数组容量调整为size
    if (this.size < this.elementData.length) {
        this.elementData = this.size == 0 ? EMPTY_ELEMENTDATA : Arrays.copyOf(this.elementData, this.size);
    }

}

4.3 get

get(int index) 方法的实现如下:

public E get(int index) {
	// 检测索引值是否合法
    this.rangeCheck(index);
    // 直接返回数组索引值所对应的元素
    return this.elementData(index);
}

其中 rangeCheck 方法的实现如下:

private void rangeCheck(int index) {
	// 当索引值大于等于数组大小的时候就会抛出异常
    if (index >= this.size) {
        throw new IndexOutOfBoundsException(this.outOfBoundsMsg(var1));
    }
}

4.4 indexOf

indexOf(Object o) 的代码实现如下所示:

public int indexOf(Object o) {
    if (o == null) {
    	// 如果o为空找到空元素所在索引值返回
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
    	// 否则的话在数组中寻找和o相匹配的元素,返回它的索引值
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;  // 如果数组中不存在该元素返回-1
}

4.5 set

set(int index, E element) 的实现如下:

public E set(int index, E element) {
    rangeCheck(index);  // 索引值检测
	// 从数组中取出旧的元素值作为返回值
    E oldValue = elementData(index);
    elementData[index] = element;  // 更新元素
    return oldValue;
}

E elementData(int index) {
    return this.elementData[index];
}

4.6 contains & isEmpty

contains(Object o) 的实现如下:

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

可以看到 contains 方法调用了 indexOf 方法进行判断,indexOf 方法在数组不存在该元素时就会返回 -1。

isEmpty() 的实现如下:

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

isEmpty 方法直接判断 size 的值是否为 0,时刻记住 size 指的是元素的大小(个数),而不是指的数组的大小

三、遍历 ArrayList

因为 ArrayList 实现了 RandomAccess 接口,所以它是支持随机访问的,并且在内部它也实现了 iterator() 方法用以支持迭代器遍历,所以 ArrayList 总共有以下 3 种遍历方式:

  • for 循环遍历
for(Integer number : list){
    result = number;
}
  • 迭代器遍历
Iterator iterator = list.iterator();
while (iterator.hasNext()){
    result = (int) iterator.next();
}
  • 随机访问,通过索引值遍历
for (int i = 0; i < list.size(); i++){
    result = list.get(i);
}

下面通过一个简单的例子来测试一下这 3 种方式各自的效率:

public class Test {
    public static void main(String[] args) throws InterruptedException {
        long startTime;
        int result;
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 0; i < 100000; i++){
            list.add(i);
        }

        startTime = System.currentTimeMillis();
        for(Integer number : list){
            result = number;
        }
        System.out.println("foreach cost " + (System.currentTimeMillis() - startTime) + " ms");

        startTime = System.currentTimeMillis();
        Iterator iterator = list.iterator();
        while (iterator.hasNext()){
            result = (int) iterator.next();
        }
        System.out.println("iterator cost " + (System.currentTimeMillis() - startTime) + " ms");

        startTime = System.currentTimeMillis();
        for (int i = 0; i < list.size(); i++){
            result = list.get(i);
        }
        System.out.println("random access cost " + (System.currentTimeMillis() - startTime) + " ms");
    }
}

输出结果如下:

foreach cost 3 ms
iterator cost 2 ms
random access cost 1 ms

可以看到采用随机访问的遍历效率更高,所以在访问的时候更加推荐使用调用 get 方法来访问数组中的元素。

四、和其他容器的比较

Vector 的比较

从上面的源码分析可以得知 ArrayList 是线程不安全的。所以 ArrayList 更加推荐在单线程的情况下进行使用,如果需要在多线程的情况下进行使用则可以考虑使用 VectorVector 内部使用了同步锁机制,所以是线程安全的,但是在没有线程安全担忧的情况下应当优先使用 ArrayList,因为 Vector 加了同步锁机制无可避免地会导致开销变大,访问变慢。

LinkedList 的比较

ArrayList 的内部维护的是一个动态数组,而 LinkedList 内部维护的则是一个双向链表。对于随机访问 getsetArrayList 的速度会比 LinkedList 快,因为 LinkedList 需要移动指针去寻找元素。而在进行添加(add)和删除(remove)操作的时候,LinkedList 的速度则比 ArrayList 快,因为 ArrayList 经常要涉及到数组元素的移动,而链表结构的 LinkedList 在这方面更具优势。

参考

本篇文章属于笔者的学习笔记,参考自以下博客:

https://www.jianshu.com/p/2cd7be850540
https://www.cnblogs.com/leesf456/p/5308358.html

如果对于本篇博客有疑惑的可以在下方评论区给我留言,希望这篇博客对您有所帮助~

猜你喜欢

转载自blog.csdn.net/qq_38182125/article/details/88543054