ArrayList和Vector的区别
- ArrayList和Vector底层都是 Object数组
- Vector是线程安全的,对所有的操作都添加了 synchonized 锁,因此并发效率低
- ArrayList 是线程不安全的
ArrayList和LinkedList的区别
- ArrayList底层是Object数组,LinkedList底层是双向链表
- ArrayList实现了 RandomAccess接口,允许随机访问,LinkedList没有继承,因为底层的空间是不连续的因此不能随机访问
- ArrayList和LinkedList都是线程不安全的
- ArrayList添加元素需要进行扩容,同时需要进行空间的预留,LinkedList每个节点需要额外分配前后节点的空间
- ArrayList添加元素到末尾的时间复杂度是O(1),在其他位置添加元素因为要进行元素的移动,时间复杂度是O(n)。LinkedList在首尾添加元素和查询的时间复杂度是O(1),在其他位置添加和查询元素的时间复杂度是O(n)
ArrayList初始化
- 当使用无参构造创建ArrayList时,会将数组初始化为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,此时数组为空数组,直到添加了第一个元素后才长度会被初始化到10
- 当有参构造传入长度是0时,会将数组初始化为 EMPTY_ELEMENTDATA ,与DEFAULTCAPACITY_EMPTY_ELEMENTDATA进行区分,因为都是空数组,因此在判断容量时会根据地址判断,如果是DEFAULTCAPACITY_EMPTY_ELEMENTDATA 容量的默认值是10
- 当使用有参构造函数创建ArrayList时,则会直接创建大小为参数的数组
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 带初始容量参数的构造函数。(用户自己指定容量)
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {//初始容量大于0
//创建initialCapacity大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {//初始容量等于0
//创建空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {//初始容量小于0,抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
添加元素流程
add
- 判断元素是否需要扩容
- 在确保容量安全后,将元素赋值到数组中
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
//这里看到ArrayList添加元素的实质就相当于为数组赋值
elementData[size++] = e;
return true;
}
ensureCapacityInternal与ensureExplicitCapacity
- 判断目前所需的最小容量,如果是正常数组则为当前size+1,如果是无参构造的数组则为10
- 如果当前的长度小于目前所需的最小容量则需要进行数组的扩容
//获取需要的最小容量
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 获取“默认的容量”和“传入参数”两者之间的最大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
//如果当前的最小容量已经大于目前的容量则说明需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
//调用grow方法进行扩容,调用此方法代表已经开始扩容了
grow(minCapacity);
}
grow
- 进入到grow方法后,则说明存储元素的数据需要进入扩容流程
- 首先需要确定扩容后的大小,默认会扩容为原数组大小的1.5倍,如果该大小仍然小于所需的最小值,则扩容后大小变为所需最小值
- 如果长度大于 Integer.MAX_VALUE-8,则数组大小直接扩容到Integer.MAX_VALUE
- 在确认好扩容流程后,通过Arrays.copyOf 进行数组的复制,本质上是申请一个新的数组,再将旧数组的数据复制到新数组中
- 因此在日常使用中,需要减少ArrayList的扩容,在初始化ArrayList时,尽量赋予初始值
/**
* ArrayList扩容的核心方法。
*/
private void grow(int minCapacity) {
// oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
//将oldCapacity 右移一位,其效果相当于oldCapacity /2,
//我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
int newCapacity = oldCapacity + (oldCapacity >> 1);
//然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//再检查新容量是否超出了ArrayList所定义的最大容量,
//若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
//如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
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);
}
//比较minCapacity和 MAX_ARRAY_SIZE
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
线程不安全
空指针异常
- 当多个线程同时执行到 elementData[size++]=e时,size++后没有同步到主内存。
- 多个线程同时执行到 ensureCapacityInternal 判断无需扩容,此时多个线程会设置到size标记,然后再进行size++操作,中间会有一个size永远为null
数组越界
当多线程同时调用 ensureCapacityInternal方法,判断无需扩容,但在真正添加方法时,当其他线程添加后,会出现数组越界情况
并发修改异常
size没有同步到主内存时,其他的线程会把值覆盖到size下标上