简述ArrayList
- ArrayList 是一个动态数组,它是线程不安全的,允许元素为null。
- 其底层数据结构依然是数组,它实现了List<E>, RandomAccess, Cloneable, java.io.Serializable接口,其中RandomAccess代表了其拥有随机快速访问的能力,ArrayList可以以O(1)的时间复杂度去根据下标访问元素。
- 因其底层数据结构是数组,所以可想而知,它是占据一块连续的内存空间(容量就是数组的length),所以它也有数组的缺点,空间效率不高。
- 由于数组的内存连续,可以根据下标以O(1)的时间读写(改查)元素,因此时间效率很高。
- 当集合中的元素超出这个容量,便会进行扩容操作。扩容操作也是ArrayList 的一个性能消耗比较大的地方,所以若我们可以提前预知数据的规模,应该通过public ArrayList(int initialCapacity) {}构造方法,指定集合的大小,去构建ArrayList实例,以减少扩容次数,提高效率。
ArrayList结构
看一下继承、实现了什么
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
我们首先需要明白并且牢记在内心的是,ArrayList本质上是一个数组,但是与Java中基础的数组所不同的是,它能够动态增长自己的容量。
通过ArrayList的定义,可以知道ArrayList继承了AbstractList,同时实现了List,RandomAccess,Cloneable和java.io.Serializable接口。
那么这些提供了什么功能呢?
- 继承了AbstractList类,实现了List,意味着ArrayList是一个数组队列,提供了诸如增删改查、遍历等功能。
- 实现了RandomAccess接口,意味着ArrayList提供了随机访问的功能。RandomAccess接口在Java中是用来被List实现,用来提供快速访问功能的。在ArrayList中,即我们可以通过元素的序号快速获取元素对象。
- 实现了Cloneable接口,意味着ArrayList实现了clone()函数,能被克隆。
- 实现了java.io.Serializable接口,意味着ArrayList能够通过序列化进行传输或者持久保存。
属性分析
/**
* 默认初始容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 共享的空数组
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 使用默认大小的共享的空数组
* 与EMPTY_ELEMENTDATA的区别:当向数组添加第一个元素时,知道数组该扩容多少.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* ArrayList基于该数组实现,用该数组保存数据
* ArrayList的容量是该数组的长度
* 数组的默认大小为DEFAULT_CAPACITY
* transient:在实现Serializable接口后,将不需要序列化的属性前面加上transient
*/
transient Object[] elementData; // 没有被私有化是为了简化内部类访问
/**
* ArrayList的大小(实际所含元素的个数)
*/
private int size;
/**
* 被修改的次数
*/
protected transient int modCount = 0;
/**
* 数组的最大值
* -8是因为要保留数组的一些头信息
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
构造器
/**
* 给定容量的构造器
*/
public ArrayList(int initialCapacity) {
//如果给的容量为正数,则数组初始化就是这个值
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
//如果为0就采用默认
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
//否则抛出异常
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
* 无参构造器,默认容量为10
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 带泛型参数的构造器
*/
public ArrayList(Collection<? extends E> c) {
//先把参数转为数组类型
//toArray()方法重载自
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// 官方的bug,说不一定能返回Object[]类型
if (elementData.getClass() != Object[].class)
//如果真的不是Object[]类型,就强制转回Object[]类型
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 如果传入的容器参数为0,就把他替换为空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
常用方法
增加
add(E e)
/**
* 在数组末尾增加元素
* 先不管ensureCapacityInternal的话,
* 这个方法就是将一个元素增加到数组的size位置上,然后size=size+1。
* 再说回ensureCapacityInternal,它是用来扩容的,准确说是用来进行扩容检查的。下面我们来看一下整个扩容的过程
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
// 下面的操作可分为下面两步
// elementData[size] = e
// size=size+1
elementData[size++] = e;
return true;
}
这个add(E e)函数涉及很多函数,下面逐一分析
// 检查容量大小
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 计算容量大小
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果是空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 从默认容量和穿进来的容量选择一个最大值
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
//直接返回minCapacity
return minCapacity;
}
// 扩容判断
private void ensureExplicitCapacity(int minCapacity) {
//记录修改的次数
modCount++;
// 判断是否需要扩容
// elementData.length数组的大小,并不是数组的元素个数,size才是
if (minCapacity - elementData.length > 0)
// 进行真正的扩容操作
grow(minCapacity);
}
扩容
重点:grow是真正的扩容操作,所以单独拿出来讲
/**
* 进行真正的扩容操作
*/
private void grow(int minCapacity) {
// 获取旧的列表大小
int oldCapacity = elementData.length;
// 新的容量是在原有的容量的基础上+50% 右移一位就是二分之一
// 上面1处>>表示右移,也就是相当于除以2,减为一半
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果扩容一半之后还不足,则新的容器大小等于minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新的容量大于MAX_ARRAY_SIZE,则进行hugeCapacity()操作
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 复制原先的数组,并给他一个新容量
elementData = Arrays.copyOf(elementData, newCapacity);
}
// 最大不能超过Integer.MAX_VALUE
private static int hugeCapacity(int minCapacity) {
// 因为一旦大小超过了Integer.MAX_VALUE,数值就会为负数
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
扩容机制如下:
- 先默认将列表大小newCapacity增加原来一半,即如果原来是10,则新的大小为15;
- 如果新的大小newCapacity依旧不能满足add进来的元素总个数minCapacity,则将列表大小改为和minCapacity一样大;即如果扩大一半后newCapacity为15,但add进来的总元素个数minCapacity为20,则15明显不能存储20个元素,那么此时就将newCapacity大小扩大到20,刚刚好存储20个元素;
- 如果扩容后的列表大小大于2147483639,也就是说大于Integer.MAX_VALUE - 8,此时就要做额外处理了,因为实际总元素大小有可能比Integer.MAX_VALUE还要大,当实际总元素大小minCapacity的值大于Integer.MAX_VALUE,即大于2147483647时,此时minCapacity的值将变为负数,因为int是有符号的,当超过最大值时就变为负数
删除
/**
* 删除指定位置的元素
* 把这个元素后面的元素全部往左移一位(下标减一)
*/
public E remove(int index) {
// 数组下标越界检查
rangeCheck(index);
// 记录修改次数
modCount++;
// 得到要删除的元素
E oldValue = elementData(index);
// 需要移动的元素的数量=实际元素个数-当前要删除元素下标-1
int numMoved = size - index - 1;
// 如果这个值大于0,说明后续还有元素需要左移
if (numMoved > 0)
// 被删除元素的下标为index
// 删除原理:index之后的所有元素都往前移一位,覆盖前面的元素,总共需要移动numMoved个元素
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 最后一个元素的值赋值为null,这样就可以被GC回收了
elementData[--size] = null;
// 返回删除的值
return oldValue;
}
常见问题:
ArrayList为什么线程不安全?
主要分析ArrayList中的add()函数为什么线程不安全
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
通过上面的分析可以看到add()函数中有两个步骤,这两个步骤都在多线程的情况下都有可能会出现问题
首先分析第一个步骤:ensureCapacityInternal(size+1);
我这里画了一个EXCEL方便分析
然后分析第二个步骤elementData[size++] = e;
其实这里就是size++;不是线程安全
ArrayList与LinkedList区别
- List是接口类,ArrayList和LinkedList是List的实现类。
- ArrayList是动态数组(顺序表)的数据结构。顺序表的存储地址是连续的,所以在查找比较快,但是在插入和删除时,由于需要把其它的元素顺序向后移动(或向前移动),所以比较耗时。
- LinkedList是链表的数据结构。链表的存储地址是不连续的,每个存储地址通过指针指向,在查找时需要进行通过指针遍历元素,所以在查找时比较慢。由于链表插入时不需移动其它元素,所以在插入和删除时比较快。
ArrayList和LinkedList的时间复杂度
ArrayList 是线性表(数组)
- get() 直接读取第几个下标,复杂度 O(1)
- add(E) 添加元素,直接在后面添加,复杂度O(1)
- add(index, E) 添加元素,在第几个元素后面插入,后面的元素需要向后移动,复杂度O(n)
- remove() 删除元素,后面的元素需要逐个移动,复杂度O(n)
LinkedList 是链表的操作
- get() 获取第几个元素,依次遍历,复杂度O(n)
- add(E) 添加到末尾,复杂度O(1)
- add(index, E) 添加第几个元素后,需要先查找到第几个元素,直接指针指向操作,复杂度O(n)
- remove() 删除元素,直接指针指向操作,复杂度O(1)
ArrayList和Vector的区别
- ArrayList是线程不安全的,Vector是线程安全的
- 扩容时候ArrayList扩0.5倍,Vector扩1倍
ArrayList有没有办法线程安全?
Collections工具类有一个synchronizedList方法
可以把list变为线程安全的集合,但是意义不大,因为可以使用Vector
Vector为什么是线程安全的?
通过对比ArrayList和Vector的源码可以清楚的看到,Vector之所以线程安全是因为加了大量的synchronized
如何复制某个ArrayList到另一个Arraylist中去?
- 使用clone()方法,比如ArrayList newArray = oldArray.clone();
- 使用ArrayList构造方法,比如:ArrayList myObject = new ArrayList(myTempObject);
- 使用Collection的copy方法。
注意1和2是浅拷贝(shallow copy)。
浅拷贝和深拷贝的定义
- 浅拷贝:只复制一个对象,对象内部存在的指向其他对象数组或者引用则不复制
- 深拷贝:对象,对象内部的引用均复制
为了更好的理解它们的区别我们假设有一个对象A,它包含有2对象对象A1和对象A2
对象A进行浅拷贝后,得到对象B但是对象A1和A2并没有被拷贝
对象A进行深拷贝,得到对象B的同时A1和A2连同它们的引用也被拷贝
参考:
https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/2-ArrayList.md
https://juejin.im/post/5b2c5eefe51d4558c0442e95?utm_source=gold_browser_extension
http://developer.51cto.com/art/200905/124592.htm