Java容器专栏: Java容器源码详细解析(面试知识点)
这里仅作ArrayList的源码解析,不进行与LinkedList的查、插、删等常见的性能对比。也不对List接口的有序可重复允许null值等做过多的说明。既然看源码了,想必这些都懂了,我就不必再赘述了。
详细说明都在源码里,耐下心看吧,其实挺简单的。
(一)ArrayList底层数据结构
可调整大小的动态数组。
(二)ArrayList继承关系
除了实现List接口外,还实现了下面三个接口:
1、Serializable标记性接口:可序列化
2、Cloneable标记性接口:可克隆
3、RandomAccess标记性接口:可快速随机访问
主要目的是允许通过算法更改其行为,以便在应用于随机访问列表或顺序访问列表时提供良好的性能。简单体现在:
对于ArrayList的遍历
//随机遍历
for(int i=0, n = list.size(); i < n; i++){
list.get(i);
}
上面的随机遍历,效率要比下面的迭代器遍历效率要高!
//顺序遍历
for(Iterator iterator = list.iterator(); i.hasNext(); ){
i.next();
}
ArrayList基于数组实现,带下标,随机访问复杂度为O(1)
LinkedList基于链表实现,随机访问需要依靠遍历实现,复杂度为O(n)
当一个List拥有快速访问功能时,其遍历方法采用for循环最快速。而没有快速访问功能的List,遍历的时候采用Iterator迭代器最快速。
ArrayList使用for循环遍历快,而LinkedList使用迭代器快。因为ArrayList实现了RandomAccess接口,而LinkedList没有。
当我们不明确获取到的是Arraylist,还是LinkedList的时候,我们可以通过RandomAccess来判断其是否支持快速随机访问
if(list instanceof RandomAccess){
//随机访问
for(int i=0, n = list.size(); i < n; i++){
list.get(i);
}
}else{
//顺序访问
for(Iterator iterator = list.iterator(); i.hasNext(); ){
i.next();
}
}
ArrayList除了实现上面三个接口外,还继承了抽象类AbstractList:该类实现了List接口的骨架实现。
(三)ArrayList源码分析
1、几个重要的成员变量
private static final int DEFAULT_CAPACITY = 10; // 默认容量为10
private static final Object[] EMPTY_ELEMENTDATA = {}; // 空实例数组
// 默认大小的空实例数组,最初和上面的空实例数组是一样的
//区别:若是默认的,在第一次调用add方法才会扩容至DEFAULT_CAPACITY(10)
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // 实际存放元素的数组
private int size; // 数组当前实际的元素个数
上面的 size 是指 elementData 中实际有多少个元素,
elementData.length 为List的容量,表示最多可以容纳多少个元素
//可以通过反射获取到elementData(即ArrayList)的实际容量
try{
Field field = list.getClass().getDeclaredField("elementData");
field.setAccessible(true);
Object[] obj =(Object[]) field.get(list);
System.out.println(obj.length);
}catch(Exception e){
e.printStackTrace();
}
2、三个构造方法
// 根据创建具有指定初始容量的ArrayList
public ArrayList(int initialCapacity){
if (initialCapacity > 0) {
//如果指定容量 > 0,则直接为elementData创建相应大小的Object数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//如果指定容量为0,则elementData赋值为成员变量中的空实例数组(长度为0)
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
// 创建一个默认的ArrayList
public ArrayList(){
//将elementData赋值为默认大小的空实例数组(长度为10)
//注意刚开始也只是空数组,只有在第一次调用add是,判断若为默认的创建方式
//才会扩容(grow)成长度为10的数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 根据其他集合来创建ArrayList
public ArrayList(Collection<? extends E> c){
//将其他容器转换成数组赋值给elementData
elementData = c.toArray();
//如果长度不为0
if ((size = elementData.length) != 0) {
// c.toArray可能不会返回Object[]类型,这里保险处理,保证
if (elementData.getClass() != Object[].class)
//拷贝size大小的elementData到elementData中,类型为Object[]
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
//如果长度为0,则赋值为实例空数组(长度为0)
this.elementData = EMPTY_ELEMENTDATA;
}
}
3、添加元素操作
//添加一个元素
public boolean add(E e) {
//每次添加元素到集合中时都会先确认下集合容量大小
ensureCapacityInternal(size + 1);
//然后将 size 自增 1并赋值
elementData[size++] = e;
return true;
}
//确保内部容量足够(充足或扩容)
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//计算容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//在这里判断!!若为默认创建的,则返回DEFAULT_CAPACITY(10)作为容量
//这里最多进去一次,其他的就都是下面直接返回传入进来的 “size+1”
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
//对list的修改次数(每次add和remove都会增加,是AbstractList抽象类的成员变量)
modCount++;
// 如果容量不足,则
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//扩容方法 minCapacity:最小满足容量
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//默认将扩容至原来容量的 1.5 倍
// >>位运算,右移一位代表oldCapacity / 2,位运算效率更高
int newCapacity = oldCapacity + (oldCapacity >> 1);
//若扩容1.5倍后仍不够,就直接将容量设置为minCapacity (普通的add方法传入的是size+1)
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果扩容后,大于数组的最大长度
if (newCapacity - MAX_ARRAY_SIZE > 0)
//调用此方法设置合适的容量大小
//若最小满足容量小于MAX_ARRAY_SIZE,则扩容到MAX_ARRAY_SIZE(值为Integer.MAX_VALUE - 8)
//若最小满足容量已经大于MAX_ARRAY_SIZE了,则扩容到Integer.MAX_VALUE(值为0x7fffffff)
newCapacity = hugeCapacity(minCapacity);
// 将原数组中的数据复制到大小为 newCapacity 的新数组中,并将新数组赋值给 elementData。
elementData = Arrays.copyOf(elementData, newCapacity);
}
还有其他添加元素的方法
//在指定下标出设置元素,下标原本及其后面的元素向后移动。(不覆盖)
//注意:index不能超过size,即最多可以插入到最后一个元素的下一个位置中
public void add(int index, E element) {
//检查index是否合理(不能超过size,当然也不能小于0)
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1);
//index之后的元素向后移一格
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
//故名思义,将传入的容器添加到list中
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew);
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
……
4、删除元素操作
//根据下标删除元素,返回被删除的对象
public E remove(int index) {
//判断下标(只过滤了超过size的情况)
/*
// 为什么不判断index为负数的情况?
// 因为在下面elementData(index)进行了负数判断了。
*/
rangeCheck(index);
//增加修改次数
modCount++;
//获取到指定元素(返回值)
E oldValue = elementData(index);
//计算需要移动的元素个数
int numMoved = size - index - 1;
//如果存在需要移动的元素(因为如果删除的是最后一个元素,就不需要移动了)
if (numMoved > 0)
/*参数含义:
第一个参数:原数组
第二个参数:从原数组的此下标开始
第三个参数:目标数组
第四个参数:从目标数组的此下标开始
第五个参数:copy的长度
*/
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//最后一个元素位置设置为null,可以被GC回收
elementData[--size] = null;
//返回被删除的元素
return oldValue;
}
//移除指定的对象,返回boolean表示是否移出成功
//可以移除null
//且每次移除都是按照下标从小到大遍历,遇到的第一个“相同”对象就移除,也仅移除一个
//也就是如果list中有多个“hello”字符串,调用此方法移除,仅移除第一个“hello”
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 {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
//是第一个remove方法简化版,不需要判断下标,也不需要返回值
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
}
5、获取元素操作
//没啥好所的,底层是数组,就直接数组+下标获取(数组随机访问)
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
6、其他较重要方法
//将list的容量缩减到和size相同。(数组填满)
public void trimToSize() {
//同样增加修改次数
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA //如果size为0,则赋值为EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size); //否则直接复制size大小
}
}
//set区分于add(int index, E element)
//set是覆盖,add是插入(可能需要后移)
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
(四)线程安全性
ArrayList不是线程安全的。
举例:
比如在add方法中,需要执行ensureCapacityInternal(size + 1)判断容量是否足够。以容量为10,已存9个元素(size==9)为例,那么如果是多线程同时add,有可能线程A、B同时获取到size=9,都判断不需要扩容,就直接存进去,结果可想而知啦,第二个存的线程就报数组越界异常ArrayIndexOutOfBoundsException了。
(五)fail-fast机制
在上面的源码中我们可以看到经常出现modCount++这样的代码。其实,modCount是用来实现fail-fast机制的。modCount 用来记录 ArrayList 结构发生变化(可变操作)的次数,包括增加、删除元素以及数组容量的压缩、扩容。
无论是多线程操作共享ArrayList还是单线程中,只要在进行序列化或者使用迭代器进行迭代遍历等操作时,每次需要比较操作前后 modCount 是否改变,如果改变了需要抛出异常ConcurrentModificationException。
//抛异常(经过测试,若移除的是倒数第二个元素,则不会抛出异常,我也不懂,希望大家能够指教!)
public class Test{
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
Iterator<String> iterator = list.listIterator();
while (iterator.hasNext()) {
String str = iterator.next();
if (str.equals("a")) {
list.remove(str);
}
}
}
}
如何避免?
调用Iterator的remove方法,而不是调用Arraylist的remove方法。(Iterator中的remove方法移除后会把modCount重写赋值给expectedModCount,下一个循环时expectedModCount与modCount相等。)
(六)ArrayList序列化机制
上面我们说到ArrayList实现了序列化接口Serializable,说明是可以被序列化的。但是同时我们在ArrayList源码中的成员变量中发现,存放实际元素的数组elementData是被transient修饰的,说明它不被序列化。
但是实际测试结果证明,ArrayList能够被序列化,同时elementData也能够经过反序列化后获取到。为什么明明用transient修饰还是能够被序列化?加上transient又是为了什么?
其实默认的序列化,调用的是ObjectOutputStream 的 defaultWriteObject( )以及 ObjectInputStream 的 defaultReadObject( )。但是ArrayList类中定义了writeObject() 和 readObject ()。序列化过程中,虚拟机允许类自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// 上面提到的fail-fast机制
int expectedModCount = modCount;
s.defaultWriteObject();
// 序列化时不是直接写入按照容量大小,而是实际元素个数
s.writeInt(size);
// 循环写入元素
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
//fail-fast检错机制
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
//先把elementData赋值为空数组
elementData = EMPTY_ELEMENTDATA;
//读出所有数据
s.defaultReadObject();
//这个ignored了
s.readInt();
if (size > 0) {
// 类似于克隆,size大小
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
ensureCapacityInternal(size);
Object[] a = elementData;
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
通过查看源码,很明显ArrayList不会走序列化的默认方法,而是走自定义的方法。
因为elementData经过扩容后,有可能后面有很大的空间都没有存放元素,若按照默认的序列化方式,则这些空间也需要同时进行序列化保存,浪费空间。
所以elementData修饰为transient,不能进行序列化,然后通过自定义的序列化方法,类似于trimToSize()方法的效果,序列化的数组长度等于实际元素个数。
(七)ArrayList的clone机制
是浅拷贝,同样的是根据实际元素个数进行拷贝,不是根据容量。
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
//复制elementData实际元素个数size
v.elementData = Arrays.copyOf(elementData, size);
//默认修改次数为0
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// 这个不可能发生,因为实现了Cloneable
throw new InternalError(e);
}
}