1 核心知识点
- 底层数据存储结构
- 初始化容量
- 扩容机制
- 线程安全
- 时间复杂度
2 关键代码分析
从add方法开始分析
public boolean add(E e) {
// 步骤1
ensureCapacityInternal(size + 1); // Increments modCount!!
// 步骤2
elementData[size++] = e;
return true;
}
步骤1:确保内部容量充足,走进ensureCapacityInternal方法查看,这里注意要确保的容量大小是size+1,也就是保证容量能够再添加一个元素
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
发现这个方法里面也分成两部分,第一部分是calculateCapacity()计算本次添加需要的容量大小,继续跟进去看看
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
这里逻辑比较简单,首先判断elementData这个变量是否等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这里DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空的Object[],如果elementData是空Object[],那么指定的容量minCapacity和默认的容量DEFAULT_CAPACITY比较大小,这里的DEFAULT_CAPACITY=10,意思就是初始化容量是10,随着元素的添加,超过10就返回传递的容量值size+1。
当确定了需要的容量大小后,接着我们看第二部分ensureExplicitCapacity(),是怎么确Object[]保容量够用的
private void ensureExplicitCapacity(int minCapacity) {
// 改变数量
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
首先会给modCount的值+1,这个值的作用是保证在遍历ArrayList的时候,集合里面的元素数量没有发生变化,这部分分析放到文章底部进行详细的说明,然后判断需要的容量大小是否超过现在的容量大小的,如果超过了,那么就需走进grow()方法扩容
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
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);
}
(1)新容量是旧容量的1.5倍,newCapacity=oldCapacity+0.5*oldCapacity
(2)判断新的容量大小是否已经满足需要的容量大小,如果不满足,那么新容量的大小就是需要的容量大小
(3)再判断新容量的大小是否超过了最大容量大小(0x7fffffff,2的31次方减1),如果超过则抛异常
(4)创建一个新的容量大小的Object数组,并将原来的元素拷贝到新数组当中,这个操作非常重,所以在代码实现的时候尽量避免频繁扩容
到这里已经完成了容量的保证,接下来要做的就是将新的元素保存到Object[]当中
会到步骤2,elementData[size++] = e,制定数组的下一个索引位置元素为新增的元素,最后返回true,标识添加成功
3 modCount作用说明
因为ArrayList是一个集合,最常见的操作就是遍历,那么一定会使用到ArrayList的遍历器,看看遍历方法iterator的实现
public Iterator iterator() {
return new Itr();
}
继续跟进Itr(),这是一个内部类
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;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
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() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} 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();
}
}
因为遍历的时候我们通常要判断是否有下一个元素,这里是通过next()方法进行判断
@SuppressWarnings("unchecked")
public E next() {
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];
}
注意checkForComodification()方法,看看具体实现
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
判断modCount是否等于expectedModCount,如果不等于,就抛异常,这里的modCount是ArrayList的全局变量,add()添加元素的时候,这个值会自增1,expectedModCount则是在创建迭代器的时候,将modCount 赋值给了expectedModCount,所以一开始expectedModCount=modCount,如果在遍历的过程当中,集合发生了元素数量变化,那么modCount就会变化,那么next()的时候就会抛异常,主要就是用来保障集合在遍历过程中,元素数量不能发生变化。
问题来了,如果想在遍历的时候,删除元素怎么处理,可以通过迭代器的remove方法删除,一起来看看remove方法为什么不会抛异常
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
荣国上面代码可以看到,在进行remove操作的时候,会重新将expectedModCount赋值成modCount,也就是说,当modCount发生变化,expectedModCount也会相应的发生变化,是满足expectedModCount = =modCount这个等式的,所以不会报错
4 线程安全问题
先说结论,非线程安全,代码演示,多线程将100个1放入ArrayList中存放
@Test
public void testThreadSafe() {
List list = new ArrayList();
for (int i = 0; i < 500; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
// 这里睡眠50ms,是为了等待其它线程并行跑起来,如果不睡眠,add操作太快,很难出现
// 线程安全问题
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
list.add(1);
}
}).start();
}
try {
// 这里的150000>200*500,保证所有线程都已经run起来
Thread.sleep(150000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(list.size());
}
输出结果是495
预期打印结果是500,实际输出是495,说明多线程的时候。有元素被下一个元素给覆盖掉,导致元素添加丢失
可以使用Collections.synchronizedList(list)方法
@Test
public void testArrayListSafe() {
List list = new ArrayList();
List integers = Collections.synchronizedList(list);
for (int i = 0; i < 500; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
// 这里睡眠50ms,是为了等待其它线程并行跑起来,如果不睡眠,add操作太快,很难出现
// 线程安全问题
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
integers.add(1);
}
}).start();
}
try {
// 这里的150000>200*500,保证所有线程都已经run起来
Thread.sleep(150000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(integers.size());
}
//输出结果是500
最终输出结果是500,符合预期,线程安全,看下线程安全实现原理
public static List synchronizedList(List list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
synchronizedList方法,返回了一个新的SynchronizedList对象,先看下这个构造器是否有做什么操作
SynchronizedList(List list) {
// 查看父构造器
super(list);
this.list = list;
}
// 父构造器给mutex这个变量赋值,是当前对象
SynchronizedCollection(Collection c) {
this.c = Objects.requireNonNull(c);
mutex = this;
}
然后看看这个对象的add方法是怎么实现的
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
通过这里我们不难发现,线程安全的实现原理是通拖synchronized实现,使用的锁是当前对象
5 时间复杂度分析
这里采用大O表示法
- 添加元素
ArrayLis的底层数据结构是Object[],添加元素的时候,因为ArrayList通过size记录了索引的位置,只需要Object[size+1]=newElement,所以时间复杂度是O(1)
- 查找元素
查找指定索引index的元素,element = Object[index],时间复杂度也是O(1)
插座指定元素data,需要遍历整个数组,然后找到元素为data的位置,时间复杂度是O(n)
- 删除元素
删除指定索引index的元素,需要将index之后的元素往前移动一个索引位置,这样index位置的元素就被删掉,时间复杂度O(n)
删除指定元素data,需要遍历整个数组,然后找到元素为data的位置index,需要将index之后的元素往前移动一个索引位置,这样index位置的元素就被删掉,时间复杂度O(n),时间复杂度是O(n)
6 面试技巧
问题:说说ArrayList?
- 将底层数据结构以及扩展原理说清楚,同时也可以强调一下通过位移计算扩容长度的性能优势
- 突出modCount的作用和不同操作下的时间复杂度,到这里,基本已经回答了面试官对ArrayList的问题,同时还带有一点点扩展,这里停下来,让面试官思考和进一步提问,有经验的面试官接下来就会问到线程安全,正好进入我们准备的知识点
- 主动说明ArrayList非线程安全,以及怎么实现线程安全
本文由博客一文多发平台 OpenWrite 发布!