ArrayList源码浅析
Posted darkclouds
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ArrayList源码浅析相关的知识,希望对你有一定的参考价值。
也快要秋招了,博客也没有任何的代码,只有几个遇到的问题记录,所以就写些吧,顺便复习下,如果有哪块写的有问题,欢迎大家批评指正。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
//默认的一个初始化数组大小
private static final int DEFAULT_CAPACITY = 10;
//这两个都是空数组,主要是用来初始化容器或者判断某些给的形参数组是否为空
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//实际存放对象的数组
transient Object[] elementData;
//当前的数组中存放的对象的个数
private int size;
//我们平时最常用的无参构造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//比较少用的有参构造方法,直接将给的形参容器复制到我们的arraylist中
public ArrayList(Collection<? extends E> c) {
//将容器转化为数组
elementData = c.toArray();
//容器是否不是空的,给size(也就是当前的arraylist中对象的个数)中赋值。
if ((size = elementData.length) != 0) {
//判断字节码对象是否相等,也就是类型是否一致,字节码对象只会在第一次加载这种类时加载字节码,且只加载一次
if (elementData.getClass() != Object[].class)
//将内置的数组赋值为该容器转化的数组
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
//将内置的数组赋值为空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
//我们平时常用的add方法
public boolean add(E e) {
//确保容量足够,因为每次添加一个对象,所以给的参数是size+1
//这个方法调用的是最关键的地方,在这之后会调用一个方法将容器的容量扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//上面已经确保了容量足够,且没有异常
//所以直接给当前数组的不是null的那个最末尾的下表的下一个赋值
//注意:size是真正的当前存储的对象的个数
//而elementData这个数组是很有可能有一部分位置是空的,也就是null
elementData[size++] = e;
return true;
}
//确定容量方法,其内调用了一个确定最小容量和一个确定详细容量两个方法
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//这个方法内就是判断下elementData是否是空的,是空的就直接判断这个size+1是否比默认的初始容量10大
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//比较minCapacity是否比10大
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
//总之就是返回一个最小的容量数值
}
//该方法确定当前的容量是否足够,并决定是否增加容量
private void ensureExplicitCapacity(int minCapacity) {
//modCount用来记录修改次数,在迭代器内判断是否出现并发修改异常,这个先不管
modCount++;
//上面也说过了,elementData很可能是有一部分是null
//所以还需要判断一下,elementData是否足够大
//一般添加一个元素,elementData.length和size是一样大的
//但是不排除我们会使用addAll(Collection<?> c)方法
if (minCapacity - elementData.length > 0)
//容量不够,增加容量,将calculateCapacity方法确定的最小容量传入
grow(minCapacity);
}
//增长容量方法
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//(oldCapacity >> 1)是右移操作,相当于除2
//但是这个是最快的,直接将命令给cpu的,我们平时除2也可以这样,能稍微快点,上过组成原理的同学应该知道
//这里就是得到一个elementData.length的1.5倍的值,准备和minCapacity比较出一个合理的值
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果这个1.5倍的值大于最小容量,那么我们直接取值最小容量就可以
//没必要用1.5倍的值,数组是固定大小,会浪费内存
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//MAX_ARRAY_SIZE的定义为private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
//MAX_ARRAY_SIZE已经很大,接近int的极限,如果比这个值还要大
if (newCapacity - MAX_ARRAY_SIZE > 0)
//得出一个容量值,这个值会非常大
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//这句英文说这个这最小容量通常接近于我们elementData的实际存储对象个数
//大概他觉得我们最长使用的是add(E e)这样的添加很少的元素对象,不会一次添加大量元素对象
//System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength));
//这个Arrays.copyOf之后会调用上面这一句代码
//original是elementData,newLength是newCapacity,copy是完成扩容后的数组
//最后会return copy,也就是完成了这一次扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
//比较出一个合适的容量值的方法
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError
("Required array size too large");
//这里Integer.MAX_VALUE这个值可能会报错
//因为jvm会有一个默认的内存大小,这个值为21亿的MAX_VALUE可能会超过这个内存
//和数组下标越界异常类似,但是这回是内存不够,可以调整jdk默认内存解决
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
//知道了add(E e)方法后,其他方法也大相径庭
//
public E remove(int index) {
//检查是否超出边界范围,只有一句判断是否判处数组越界异常
//private void rangeCheck(int index) {
// if (index >= size)
// throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//}
rangeCheck(index);
//同add方法一样,modCount用来记录修改次数
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
//其实ArrayList的改变大小就是靠System.arraycopy这个本地方法完成的
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//清除数组对那个删除对象的引用,垃圾回收器才会回收那个没有引用标记的对象
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
}
上面提到了两个可能会有问题的地方,一个是Integer.MAX_VALUE这个值会使得jvm内存不够分配报异常,一个是modCount记录修改次数,也可能报异常
先说第一个地方
try { int[] a=new int[Integer.MAX_VALUE-2]; System.out.println(a.length); } catch (java.lang.OutOfMemoryError e) { e.printStackTrace(); } try { int[] b=new int[Integer.MAX_VALUE]; System.out.println(b.length); } catch (java.lang.OutOfMemoryError e) { e.printStackTrace(); }
这个代码执行后会报错误,Java heap space是内存快到极限时抛出,Requested array size exceeds VM limit是已经在内存中找不到该内存位置了,解决方法是调整jvm默认分配内存大小
第二个地方modCount,先看这么一段代码
ArrayList<Integer> list=new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
list.remove(0);
}
运行后,会 抛出java.util.ConcurrentModificationException,这样的一个异常,为什么会抛出这样的一个异常呢?并没有多个线程同时修改集合啊
先看下Iterator<Integer> iterator = list.iterator()中的iterator是什么
继续往下,看到一个内部类
这个类中的next方法,可以看到调用了checkForComodification方法,下面的if (i >= elementData.length) throw new ConcurrentModificationException();是为了防止多线程修改
调用的checkForComodification方法
到这里知道了,每次ArrayList去增删改元素对象的时候,改变了modCount,而其内部类对象的expectedModCount只是修改前的一个modCount的值,
所以调用next方法时,如果使用ArrayList的增删改,那么就会抛出这么一个异常,解决方法是使用迭代器的remove方法
可以看到,这个remove方法中,expectedModCount和modCount每次都会同步下,所以不会报异常了,
但是它只有remove方法,如果想要使用其它方法可以使用list.listIterator()获取一个更加强大的迭代器
以上是关于ArrayList源码浅析的主要内容,如果未能解决你的问题,请参考以下文章
Collections.synchronizedList CopyOnWriteArrayListVector介绍源码浅析与性能对比文末福利