一、简介
ArrayList 是可以动态增长和缩减的索引序列,它是基于数组实现的 List 类,它实现了 List 接口,List 接口继承自 Collection 接口,Collection 是所有集合类的父类,所以 ArrayList 间接实现了 Collection 接口。我们之前学过一门叫做数据结构的课程,线性表是我们学过的第一个数据结构,它有两种存储方式,一种是线性的,另一种是链式的。分析一个集合类的时候,数据结构往往是它的灵魂所在,理解底层的数据结构其实就理解了该类的实现思路,ArrayList 就是 Java 语言为我们封装好的线性表顺序存储结构的实现。
二、继承结构
分析:
- 为什么要先继承 AbstractList,而让 AbstractList 先实现 List?而不是让 ArrayList 直接实现 List?
这里是有一个思想,接口中全都是抽象的方法,而抽象类中可以有抽象方法,还可以有具体的实现方法,正是利用了这一点,让 AbstractList 是实现接口中一些通用的方法,而具体的类,如 ArrayList 就继承这个 AbstractList 类,拿到一些通用的方法,然后自己在实现一些自己特有的方法,这样一来,让代码更简洁,就继承结构最底层的类中通用的方法都抽取出来,先一起实现了,减少重复代码。所以一般看到一个类上面还有一个抽象类,应该就是这个作用。
- ArrayList 实现了哪些接口?
List 接口:我们会出现这样一个疑问,在查看了 ArrayList 的父类 AbstractList 也实现了 List 接口,那为什么子类 ArrayList 还是去实现一遍呢?
开发这个 collection 的作者 Josh 说:这其实是一个 mistake,因为他写这代码的时候觉得这个会有用处,但是其实并没什么用,但因为没什么影响,就一直留到了现在。
RandomAccess 接口:这个是一个标记性接口,通过查看 API 文档,它的作用就是用来快速随机存取,有关效率的问题,在实现了该接口的话,那么使用普通的 for 循环来遍历,性能更高,例如 ArrayList。
而没有实现该接口的话,使用 Iterator 来迭代,这样性能更高,例如 LinkedList。所以这个标记性只是为了让我们知道我们用什么样的方式去获取数据性能更好。
Cloneable 接口:实现了该接口,就可以使用 Object.Clone() 方法了。
Serializable 接口:实现该序列化接口,表明该类可以被序列化。
三、属性
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.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;
}
对于 ArrayList 的属性,我有下面几点需要强调:
- ArrayList 默认的初始化容量为 10,最大容量为 Integer.MAX_VALUE - 8
- ArrayList 使用一个 size 变量来记录当前的容量
- ArrayList 的底层是一个 Object[] 类型的数组,用 elementData 变量来存储
- 比较有意思的一点是 EMPTY_ELEMENTDATA 和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 这两个除了名字完全相同的变量,我们在下文讲解构造方法时讲解
四、构造方法
ArrayList 有三种构造方法,我们常用的有两种,分别是无参构造方法和给定初始容量的有参构造方法,我们先看给定初始容量的有参构造方法:
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,直接创建一个 initialCapacity 大小的数组,如果等于 0,就将实例变量 EMPTY_ELEMENTDATA 赋值给 elementData,否则的话就抛出异常。
再来看无参构造方法:
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
无参构造方法是采用的默认值 10 作为容量大小,直接将 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 赋值给 elementData。
到这里,就可以揭晓上文卖下的关子了,原来开发 Java 的工程师将 ArrayList 的初始化分成三种情况,elementData 引用的数据对应不同的情况也不同,使用默认值容量就引用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,使用 0 就引用 EMPTY_ELEMENTDATA,否则引用新创建的数组。
这里引出一个问题值得我们进行深入的思考,为什么这里当初始容量为 0 时创建的空的数组使用的是EMPTY_ELEMENTDATA 而不使用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA?
JDK 源码的注释给了我们解答
Shared empty array instance used for default sized empty instances. We
distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
first element is added.
这句话的大概意思是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 用于默认大小的空实例,我们将其与 EMPTY_ELEMENTDATA 区分开来,以了解当添加第一个元素时需要膨胀多少。
public ArrayList(Collection<? extends E> 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;
}
}
第三种构造方法简单了解一下就好,开发中用的不多。
五、常用方法
0x01、新增操作
1.add(E e)
public boolean add(E e) {
// 确定内部容量是否够了,size 是数组中数据的个数,因为要添加一个元素,所以 size + 1
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
这个方法用于在集合的尾部添加一个元素。
我们再一次的来看看 ensureCapacityInternal(size + 1) 方法:
private void ensureCapacityInternal(int minCapacity) {
// 如果 elementData 指向的是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的地址
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 设置默认大小为 DEFAULT_CAPACITY,也就是 10
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 确定实际容量,上面只是将 minCapacity 赋值为 10,这个方法就是真正的判断 elementData 是否够用
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果超出了容量,进行扩展
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
// 将扩充前的 elementData 大小给 oldCapacity
int oldCapacity = elementData.length;
// 右移运算符等价于除以 2,如果第一次是 10,扩容之后的大小是 15
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果 newCapacity 超过了最大的容量限制,就调用 hugeCapacity,也就是将能给的最大值给 newCapacity
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 新的容量大小已经确定好了,就 copy 数组,改变容量大小
elementData = Arrays.copyOf(elementData, newCapacity);
}
结合默认构造器或其他构造器中,如果默认数组为空,则会在 ensureCapacityInternal() 方法调用的时候进行数组初始化。这就是为什么默认构造器调用的时候,我们创建的是一个空数组,但是在注释里却介绍为长度为 10 的数组。
第二种就是已经存在元素了,当添加第 11 个元素的时候,minCapacity 为 11,此时的数组的长度为 10,所以 minCapacity - elementData.length > 0,需要进行扩容,int newCapacity = oldCapacity + (oldCapacity >> 1)。如果第一次是 10,扩容之后的大小是 15,然后在再把之前的元素进行复制到新的数组中去。
第一次扩容后,如果容量还是小于 minCapacity(需要的最小的容量),就将容量扩充为 minCapacity。
2.add(int index, E element)
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++;
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
这个方法首先检查要放置的下标是否合法,然后将集合 index 及其之后的元素拷贝到 index + 1 及之后中,最后将 element 插入到 index 位置上。
3.addAll(Collection<? extends E> c)
public boolean addAll(Collection<? extends E> 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;
}
addAll() 方法,通过将 collection 中的数据转换成 Array[] 然后添加到 elementData 数组,从而完成整个集合数据的添加。在整体上没有什么特别之处,这里的 collection 可能会抛出控制异常 NullPointerException 需要注意一下。
4.addAll(int index, Collection<? extends E> c)
public boolean addAll(int index, Collection<? extends E> 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;
}
与上述方法相比,这里主要多了两个步骤,判断添加数据的位置是不是在末尾,如果在中间,则需要先将数据向后移动 collection 长度的位置。
0x02、删除操作
1.remove(int index)
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;
}
删除数据并不会更改数组的长度,只会将数据从数组中移除,如果目标没有其他有效引用,则在 GC 时会进行回收。
2.remove(Object o)
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
}
这种方式,会在内部进行 AccessRandom 方式遍历数组,当匹配到数据跟 Object 相等,则调用 fastRemove() 进行删除。这个方法可以看出来,ArrayList 是可以存放 null 值的。
3.removeAll(Collection<?> c)
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
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.removeRange(int fromIndex, int toIndex)
protected void removeRange(int fromIndex, int toIndex) {
modCount++;
int numMoved = size - toIndex;
System.arraycopy(elementData, toIndex, elementData, fromIndex,
numMoved);
// clear to let GC do its work
int newSize = size - (toIndex-fromIndex);
for (int i = newSize; i < size; i++) {
elementData[i] = null;
}
size = newSize;
}
该方法主要删除了在范围内的数据,通过 System.arraycopy() 对整部分的数据进行覆盖即可。
5.clear()
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
该方法通过将所有元素置为 null,等待垃圾回收将这个给回收掉,并且将 size 设置为 0 来实现对集合的清除。
0x03、其它常用方法
1.indexOf(Object o)
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;
}
该方法通过遍历查找元素第一次出现的位置。
2.lastIndexOf(Object o)
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;
}
该方法通过从后往前遍历查找元素第一次出现的位置,也就是最后一次出现的位置。
3.contains(Object o)
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
通过判断调用 indexOf() 方法的返回值来判断集合中是否包含该元素。
4.get(int index)
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
先检查下标是否合法,再直接返回该下标对应的值。
5.set(int index, E element)
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
先检查下标是否合法,再将旧值去除,将新值赋上,最后返回旧值。
6.trimToSize()
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
删除元素时不会减少容量,若希望减少容量则调用 trimToSize()。
六、线程不安全
先来回顾一下上面讲的 ArrayList 添加元素的操作:
public boolean add(E e) {
/**
* 添加一个元素时,做了如下两步操作
* 1.判断列表的 capacity 容量是否足够,是否需要扩容
* 2.真正将元素放在列表的元素数组里面
*/
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
这样也就出现了第一个导致线程不安全的隐患,在多个线程进行 add 操作时可能会导致 elementData 数组越界。
具体逻辑如下:
- 列表大小为 9,即 size = 9
- 线程 A 开始进入 add 方法,这时它获取到 size 的值为 9,调用 ensureCapacityInternal 方法进行容量判断。
- 线程 B 此时也进入 add 方法,它获取到 size 的值也为 9,也开始调用 ensureCapacityInternal 方法。
- 线程 A 发现需求大小为 10,而 elementData 的大小就为 10,可以容纳。于是它不再扩容,返回。
- 线程 B 也发现需求大小为 10,也可以容纳,返回。
- 线程 A 开始进行设置值操作,elementData[size++] = e 操作。此时 size 变为 10。
- 线程 B 也开始进行设置值操作,它尝试设置 elementData[10] = e,而 elementData 没有进行过扩容,它的下标最大为 9。于是此时会报出一个数组越界的异常 ArrayIndexOutOfBoundsException。
另外第二步 elementData[size++] = e 设置值的操作同样会导致线程不安全。从这儿可以看出,这步操作也不是一个原子操作,它由如下两步操作构成:
elementData[size] = e;
size = size + 1;
- 列表大小为 0,即 size = 0
- 线程 A 开始添加一个元素,值为 A。此时它执行第一条操作,将 A 放在了 elementData 下标为 0 的位置上。
- 接着线程 B 刚好也要开始添加一个值为 B 的元素,且走到了第一步操作。此时线程 B 获取到 size 的值依然为 0,于是它将 B 也放在了 elementData 下标为 0 的位置上。
- 线程 A 开始将 size 的值增加为 1
- 线程 B 开始将 size 的值增加为 2
这样线程 AB 执行完毕后,理想中情况为 size 为 2,elementData 下标 0 的位置为 A,下标 1 的位置为 B。而实际情况变成了 size 为 2,elementData 下标为 0 的位置变成了 B,下标 1 的位置上什么都没有。并且后续除非使用 set 方法修改此位置的值,否则将一直为 null,因为 size 为 2,添加元素时会从下标为 2 的位置上开始。
七、总结
- ArrayList 可以存放 null。
- ArrayList 本质上就是一个 elementData 数组。
- ArrayList 区别于数组的地方在于能够自动扩展大小,其中关键的方法就是 gorw() 方法。
- ArrayList 中 removeAll(collection c) 和 clear() 的区别就是 removeAll() 可以删除批量指定的元素,而 clear() 是全是删除集合中的元素。
- ArrayList 由于本质是数组,所以它在数据的查询方面会很快,而在插入删除这些方面,性能下降很多,要移动很多数据才能达到应有的效果。
- ArrayList 实现了 RandomAccess,所以在遍历它的时候推荐使用 for 循环。
参考
https://www.cnblogs.com/zhangyinhua/p/7687377.html
https://blog.csdn.net/weixin_40304387/article/details/80790177