深入理解ArrayList

138 阅读8分钟

什么是ArrayList?

ArrayList

的实现原理其实就是数组(动态数组),
ArrayList
的介绍及简单使用方法

动态数组与一般数组有什么区别?

Java
中的数组相比,
ArrayList
的容量能动态地增长

ArrayList

效率怎么样?

ArrayList

不是线程安全的,所以效率比较高 ,但是只能用于单线程的环境中,那多线程呢?别急,文末会讲到

ArrayList

主要继承哪些类实现了哪些接口?

ArrayList

主要继承了
AbstractList
类,实现了
List
RandomAccess
Cloneable
Serializable
接口

public class ArrayList<E> extends AbstractList<E>

implements List<E>, RandomAccess, Cloneable, java.io.Serializable

RandomAccess

的意思是其拥有快速访问的能力,
ArrayList
可以以
O(1)[^1]
的时间复杂度去根据下标访问元素。由于
ArrayList
底层结构是数组,所以它占据了一块连续的内存空间,其长度就是数组的大小,因此它也有数组的缺点,在空间效率不高,但是也有它的优点,就是查询速度快,时间效率较快

ArrayList

的常量与变量有哪些?

//

序列
ID

private static final long serialVersionUID = 8683452581122892189L;

// ArrayList

默认的初始容量大小

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

// list

大小

private int size;

当集合中的元素超出数组规定的长度时,数组就会进行扩容操作,扩容操作就是
ArrayList
存储操作缓慢的原因,尤其是当数据量较大的时候,每次扩容消耗的时间会越来越多

ArrayList

的构造方法有哪些?

一、
ArrayList(int initialCapacity)

所以当我们要使用
ArrayList
时,可以
new ArrayList(
大小
)
构造方法来指定集合的大小,以减少扩容的次数,提高写入效率,该构造函数的源码如下:

//

自定义初始容量的构造方法

public ArrayList(int initialCapacity) {

if (initialCapacity > 0) {

this.elementData = new Object[initialCapacity];

} else if (initialCapacity == 0) {

this.elementData = EMPTY_ELEMENTDATA;

} else {

//

如果初始容量小于
0
,则会出现
IllegalArgumentException
异常

throw new IllegalArgumentException("Illegal Capacity: "+

initialCapacity);

}

}

这个构造函数还是比较好理解的,因为涉及到的代码也不多,而且都是一些基础的代码,相信聪明的你肯定看得懂的

二、
ArrayList()

这个就更简单了,只有两行代码

//

默认的构造方法,构造一个初始容量为
10
的空列表

public ArrayList() {

// elementData

初始化为
DEFAULTCAPACITY_EMPTY_ELEMENTDATA

this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;

}

三、
ArrayList(Collection<? extends E> c)

//

构造一个包含指定元素的列表集合,按集合的返回顺序迭代器

//

传入参数为
Collection
对象

// c

要将其元素放入此列表的集合

public ArrayList(Collection<? extends E> c) {

//

调用
toArray()
方法将
Collection
对象转换为
Object[]

elementData = c.toArray();

//

判断
size
的大小,如果
size
值为
0
,则会抛出
NullPointerException
异常

//

如果
size > 0
,则执行以下代码

if ((size = elementData.length) != 0) {

// c.toArray might (incorrectly) not return Object[] (see 6260652)

if (elementData.getClass() != Object[].class)

//

执行
Arrays.copyOf
,把
Collection
对象的内容
copy
elementData

elementData = Arrays.copyOf(elementData, size, Object[].class);

} else {

// replace with empty array.

this.elementData = EMPTY_ELEMENTDATA;

}

}

ArrayList

的方法有哪些?

add()

单个
add()

//

添加单个元素
,
添加元素之前会先检查容量,如果容量不足则调用
grow
方法

public boolean add(E e) {

//

判断添加后的长度是否需要扩容

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

//

然后在数组末尾添加当前元素,并且修改
size
的大小

elementData[size++] = e;

//

返回布尔值
true

return true;

}

add()

方法中主要用到了一个新的方法——
ensureCapacityInternal
,来看下
ensureCapacityInternal
的源码:

//

判断是否需要扩容

private void ensureCapacityInternal(int minCapacity) {

//

执行
calculateCapacity

ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));

}

ensureCapacityInternal
主要调用的是
ensureExplicitCapacity
方法和
calculateCapacity
方法,我们先看下
calculateCapacity
方法

//

判断是否是第一次初始化数组

private static int calculateCapacity(Object[] elementData, int minCapacity) {

//

判断当前数组是否等于空的数组

//

注意:这里的
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
并不是
EMPTY_ELEMENTDATA
,不过并无太大差别,只是为了
区分何时需要扩容而已

if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {

//

取其中最大的值作为判断本次是否需要扩容的依据,由于第一次数组是空的,所以默认要使数组扩容到
10
的长度

return Math.max(DEFAULT_CAPACITY, minCapacity);

}

return minCapacity;

}

点击
ensureCapacityInternal
中的
ensureExplicitCapacity
,可以看到来到了
ensureExplicitCapacity
方法,而
ensureExplicitCapacity
主要调用的就是上面所说的
grow
方法,源码如下:

//

判断扩容的方法

private void ensureExplicitCapacity(int minCapacity) {

//

如果需要扩容
modCount
自增,这个参数是指当前列表的结构被修改的次数

modCount++;

// overflow-conscious code

//

判断当前数据量是否大于数组的长度,如果是,进行扩容

if (minCapacity - elementData.length > 0)

//

执行扩容操作

grow(minCapacity);

}

grow

方法源码如下:

// grow

扩容方法

private void grow(int minCapacity) {

// overflow-conscious code

//

记录扩容前的数组长度

int oldCapacity = elementData.length;

//

将原数组的长度扩大
1.5
倍作为扩容后数组的长度(如果扩容钱数组长度为
10
,那么经过扩容后的数组长度应该为
15

//

这里涉及到异或运算,不懂的朋友可以看下这篇文章
https://blog.csdn.net/Woo_home/article/details/103146845

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

}

grow

方法调用的
hugeCapacity
源码如下:

//

如果新数组长度超过当前数组定义的最大长度时

private static int hugeCapacity(int minCapacity) {

//

抛出
OOM
异常

if (minCapacity < 0) // overflow

throw new OutOfMemoryError();

//

将扩容长度设置为
Interger.MAX_VALUE,
也就是
int
的最大长度

return (minCapacity > MAX_ARRAY_SIZE) ?

Integer.MAX_VALUE :

MAX_ARRAY_SIZE;

}

public void add(int index, E element) {

//

判断下标是否越界,如果是则抛出
IndexOutOfBoundsException
异常

rangeCheckForAdd(index);

//

判断是否需要扩容,上面讲到过,这里不再解释

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

//

拷贝数组,将下标后面的元素全部向后移动一位

System.arraycopy(elementData, index, elementData, index + 1,

size - index);

//

将元素插入到当前下标的位置

elementData[index] = element;

size++;

}

rangeCheckForAdd

方法

//

判断下标是否越界,如果是则抛出
IndexOutOfBoundsException
异常

private void rangeCheckForAdd(int index) {

if (index > size || index < 0)

throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

}

添加多个元素
addAll()

//

添加多个元素

public boolean addAll(Collection<? extends E> c) {

return addAll(this.size, c);

}

//

添加多个元素到指定下标

public boolean addAll(int index, Collection<? extends E> c) {

//

判断下标是否越界,上面提到过

rangeCheckForAdd(index);

//

判断
c
的大小是否大于
0

int cSize = c.size();

//

如果等于
0
返回
false

if (cSize==0)

return false;

checkForComodification();

//

将元素插入到数组中

parent.addAll(parentOffset + index, c);

//

将修改次数赋值给
modCount

this.modCount = parent.modCount;

// size

大小加一

this.size += cSize;

return true;

}

private void checkForComodification() {

//

如果修改的次数不相等

if (ArrayList.this.modCount != this.modCount)

//

则抛出
ConcurrentModificationException
(并发修改)异常

throw new ConcurrentModificationException();

}

总结:

在进行
add
操作时先判断下标是否越界,是否需要扩容,如果需要扩容,就复制数组,然后设置对应的下标元素值

扩容:默认扩容一半,如果扩容一半不够的话,就用目标的
size
作为扩容后的容量

//

先判断下标索引

public E get(int index) {

//

调用
rangeCheck
判断是否超出了
Object
数组长度

rangeCheck(index);

//

调用
elementData
方法

return elementData(index);

}

private void rangeCheck(int index) {

//

如果超出了
Object
数组的长度

if (index >= size)

//

则抛出
IndexOutOfBoundsException
(数组下标越界)异常

throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

}

//

通过下标索引找到对应的元素值,返回指定元素

E elementData(int index) {

return (E) elementData[index];

}

public E set(int index, E element) {

//

调用
rangeCheck
判断是否超出范围,上面讲到过,不懂的同学往上翻翻

rangeCheck(index);

//

返回指定元素,上面也讲到过

E oldValue = elementData(index);

elementData[index] = element;

return oldValue;

}

//

删除元素

public E remove(int index) {

//

调用
rangeCheck
方法判断是否超出范围,上面讲到过

rangeCheck(index);

modCount++;

//

位置访问操作

E oldValue = elementData(index);

//

计算移除元素后需要移动的元素个数

int numMoved = size - index - 1;

if (numMoved > 0)

//

通过
System.arraycopy
方法将后面的元素往前移动一位

System.arraycopy(elementData, index+1, elementData, index,

numMoved);

//

最后一位赋值为
null

elementData[--size] = null; // clear to let GC do its work

//

返回移除后元素的值

return oldValue;

}

//

删除对象

public boolean remove(Object o) {

//

如果对象为
null

if (o == null) {

//

遍历整个
list
去匹配移除的值

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

源码如下:

/**

*

私有删除方法,跳过边界检查并且不返回删除的值。

*/

private void fastRemove(int index) {

modCount++;

//

位置访问操作

int numMoved = size - index - 1;

if (numMoved > 0)

//

通过
System.arraycopy
方法将后面的元素往前移动一位

System.arraycopy(elementData, index+1, elementData, index,

numMoved);

elementData[--size] = null; // clear to let GC do its work

}