ArrayList
ArrayList就是一个数组,源码中有几个重要概念
- index:表示数组下标
- elementData:表示数组本身
- DEFAULT_CAPACITY:表示初始数组的大小,默认是10!!!(无参构造器初始化是0,10 是在第一次 add 的时候扩容的数组值。)
- size:表示当前数组的大小,没有用volatile修饰,非线程安全
- modCount:统计当前数组被修改的次数,数组结构有变动,就会+1
一些重要注释
- ArrayList允许put null值
- size、isEmpty、get、set、add 等方法时间复杂度都是 O(1)
- 是非线程安全的,多线程情况下,推荐使用线程安全类
- 增强 for 循环,或者使用迭代器迭代过程中,如果数组大小被改变,会快速失败,抛出异常。
1、 初始化
有三种初始化办法:无参数直接初始化、指定大小初始化、指定初始数据初始化,源码如下:
// 无参初始化,数组大小为空
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 指定初始数据来初始化
public ArrayList(Collection<? extends E> c) {
// elementData是保存数组的容器,默认为null
elementData = c.toArray();
// 如果初始集合c有值
if ((size = elementData.length) != 0) {
// 如果集合元素不是Object类型,则转成Object类型
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 如果初始集合c没值,则默认空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
注意
- ArrayList 无参构造器初始化时,默认大小是空数组,并不是10,10 是在第一次 add 的时候扩容的数组值。
2、 新增与扩容
新增就是往数组中添加元素,主要分为两步。
- 首先看要不要扩容,如果需要就先扩容
- 直接赋值
新增源码如下
public boolean add(E e) { //确保数组大小是否足够,不够则直接扩容,size是当前数组的大小,+1就是增加后的大小 ensureCapacityInternal(size + 1); // Increments modCount!! //直接赋值,这是线程不安全的 elementData[size++] = e; return true; }
扩容(ensureCapacityInternal)源码
private void ensureCapacityInternal(int minCapacity) {
//确保容量足够
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
// 记录数组被修改的次数
modCount++;
// 如果我们需要的最小容量 大于 当前数组的长度,那就需要扩容了
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//扩容,把现有数据拷贝到新的数组中
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//新数组容量
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果扩容后的容量 < 期望的容量,那就让期望容量成为新容量,因为至少需要这么多的
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果扩容后的容量 > jvm能分配的最大值,那么就用 Integer 的最大值,上界
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//通过复制进行扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
- 扩容成原来大小的1.5倍
- ArrayList 中的数组的最大值是 Integer.MAX_VALUE,超过这个值,JVM 就不会给数组分配内存空间了。
- 新增时,并没有对值进行严格的校验,所以 ArrayList 是允许 null 值的。
扩容本质
通过代码
Arrays.copyOf(elementData, newCapacity);
来实现扩容,就是数组的拷贝,新建一个符合预期容量的新数组,然后把老数据拷贝过去。Arrays.copyOf
是通过System.arraycopy
来实现的,这个方法是native方法,源码如下。
3、 删除
ArrayList 删除元素有很多种方式,比如根据数组索引删除、根据值删除或批量删除等等,原理和思路都差不多,这里选取根据值删除方式来进行源码说明:
public boolean remove(Object o) {
//如果要删除的是null,找到第一个为null的删除
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
//调用根据索引位置来删除
fastRemove(index);
return true;
}
} else {
//如果要删除的值不为null,找到第一个和要删除的值相等的元素删除
for (int index = 0; index < size; index++)
//!!注意!!这里是根据equals来判断值是否相等,然后根据索引位置来删除
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
- 新增元素时可以增加null元素,所以删除时也是允许删除null元素的
- 找到值在数组中的索引位置,通过equals来判断相不相等等。
下面是fastRemove方法
private void fastRemove(int index) {
//记录修改次数
modCount++;
//numMoved表示删除index上的元素后,有多少个元素要移动到元素前面去(数据结构知识)
int numMoved = size - index - 1;
if (numMoved > 0)
//把index后面的元素拷贝过去
System.arraycopy(elementData, index+1, elementData, index,numMoved);
//数组最后一个元素赋值null,帮助GC
elementData[--size] = null; // clear to let GC do its work
}
4、 迭代器
如果要自己实现迭代器,实现 java.util.Iterator 类就好了,ArrayList 也是这样做的,它里面的Itr实现了迭代器接口。迭代器有三个重要参数,如下:
private class Itr implements Iterator<E> {
int cursor; // 迭代过程中下一个元素的位置,默认从0开始
int lastRet = -1; // add场景:表示上一次迭代过程中索引的位置,remove场景:-1
int expectedModCount = modCount; //迭代过程中期望的版本次数。
ArrayList迭代器的三个方法源码
private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
int expectedModCount = modCount;
Itr() {}
//有没有值可以迭代
public boolean hasNext() {
//如果下一个元素位置和大小相等,说明已经迭代完了,不等则还可以继续迭代
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
//迭代过程中判断版本号有没有被修改,如果被修改了,抛出ConcurrentModificationException异常
checkForComodification();
//下一个元素位置
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
//为下一次迭代做准备
cursor = i + 1;
//返回元素值
return (E) elementData[lastRet = i];
}
public void remove() {
//如果lastRet值为-1,说明数组已经被删完了
if (lastRet < 0)
throw new IllegalStateException();
//迭代过程中判断版本号有没有被修改
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
//-1表示元素已经被删除,写这一句是为了避免重复删除的操作
lastRet = -1;
//删除后modCount已经发生变化,要把它赋值给expectedModCount,下一次迭代两个值就一致了
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
//补上
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
只有当 ArrayList 作为共享变量时,才会有线程安全问题,当 ArrayList 是方法内的局部变量时,是没有线程安全的问题的。ArrayList 有线程安全问题的本质,是因为 ArrayList 自身的 elementData、size、modConut 在进行各种操作时,都没有加锁,而且这些变量的类型是不可见(volatile)的,所以如果多个线程对这些变量进行操作时,可能会有值被覆盖的情况。
类注释中推荐我们使用 Collections#synchronizedList 来保证线程安全,SynchronizedList 是通过在每个方法上面加上锁来实现,虽然实现了线程安全,但是性能大大降低。
5、 面试问题
(1)ArrayList 无参数构造器构造,现在 add 一个值进去,此时数组的大小是多少,下一次扩容前最大可用大小是多少?
答:此处数组的实际大小是 1,但下一次扩容前最大可用大小是 10,因为 ArrayList 第一次扩容时, 是有默认值的,默认值是 10,在第一次 add 一个值进去时,数组的可用大小被扩容到 10 了。
(2) 如果我连续往 list 里面新增值,增加到第 11 个的时候,数组的大小是多少?
答:这里的考查点就是扩容的公式,当增加到 11 的时候,此时我们希望数组的大小为 11,但 实际上数组的最大容量只有 10,不够了就需要扩容,扩容的公式是:oldCapacity + (oldCapacity>> 1),oldCapacity 表示数组现有大小,目前场景计算公式是:10 + 10 /2 = 15,然后我们发现 15 已经够用了,所以数组的大小会被扩容到 15。
(3)数组初始化,被加入一个值后,如果我使用 addAll 方法,再一下子加入 15 个值,那么最终数组的大小是多少?
答:第一题中我们已经计算出来数组在加入一个值后,实际大小是 1,最大可用大小是 10 ,现在需要一下子加入 15 个值,那我们期望数组的大小值就是 16,此时数组最大可用大小只有 10,明显不够,需要扩容,扩容后的大小是:10 + 10 /2 = 15,这时候发现扩容后的大小仍 然不到我们期望的值 16,这时候源码中有一种策略如下:
// 如果扩容后的值 < 我们的期望值,我们的期望值就等于本次扩容的大小 if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
所以最终数组扩容后的大小为 16。
(4)现在我有一个很大的数组需要拷贝,原数组大小是 5k,请问如何快速拷贝?
答:因为原数组比较大,如果新建新数组的时候,不指定数组大小的话,就会频繁扩容,频繁扩容就会有大量拷贝的工作,造成拷贝的性能低下,所以说新建数组时,指定新数组的大小为 5k 即可。
(5)有一个 ArrayList,数据是 2、3、3、3、4,中间有三个 3,现在我通过 for 循环的方式想把3删除,可以删除干净吗?最终结果是什么?为什么
答:不能删除干净,最终删除的结果是 2、3、4,有一个 3 删除不掉,原因我们看下图
每次删除一个元素后,该元素后面的元素就会往前移动,而此时循环的 i 在不断地增长,最终会使每次删除 3 的后一个 3 被遗漏,导致删除不掉。
(6)还是上面的 ArrayList 数组,我们通过增强 for 循环进行删除,可以么?
答:不可以,会报错。因为增强 for 循环调用的就是迭代器的 next () 方法,当你调用 remove () 方法进行删除时,modCount 的值会 +1,而这时候迭代器中的 expectedModCount 的值却没有变,导致在迭代器下次执行 next () 方法时, expectedModCount != modCount 就会报 ConcurrentModificationException 的错误。
(7)还是上面的数组,如果删除时使用list. Iterator 然后remove () 可以删除么,为什么?
答:可以的,因为 Iterator.remove () 方法在执行的过程中,会把最新的 modCount 赋值给 expectedModCount,这样在下次循环过程中,modCount 和 expectedModCount 两者就会相等。
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(3);
list.add(3);
list.add(4);
// for (int i = 0; i < list.size(); i++) {
// if (list.get(i) == 3) {
// list.remove(i);
// }
// }
//增强型for循环调用的是迭代器的next,list.remove然后会调用fastRemove
//前面是迭代器的版本号,后面是list里面持有的版本号,list调用remove,版本号+1
//但是前面迭代器的版本号是没变的。
// for (Integer i : list) {
// if (i == 3) {
// list.remove(i);
// }
// }
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
Integer next = it.next();
if (next == 3) {
it.remove(); //迭代器的remove,而不是list的
}
}
//主要就是看这个迭代器是不是list自己的
System.out.println(list);
}