ArrayList作为我们平常使用最多的容器之一,有必要深入了解下它了。在查看其源码之前,我们先抛出以下问题,带着问题我们更具有目的性。
1、ArrayList的本质是什么?
2、ArrayList的使用是否会出现OOM?
3、ArrayList是否线程安全?
4、ArrayList的使用过程中,做了哪些事情?
5、ArrayList的效率如何,适用于哪些场景?
其实针对第一个问题,我们看真正存储我们数据的容器就可以猜测得到了。
transient Object[] elementData;
是的,其实ArrayList就是一个可以自增长的数组,这点在我们分析第4点的时候会得到验证。那么既然是数组,其本质就是在内存中分配一段连续的区域来进行存储的。请注意,是一段连续的内存区域,如果不能够连续,即使有在多的区域也无法满足我们的要求。从而我们可以推断出第二个问题的答案,其实ArrayList使用是会导致OOM的,我们可以进行如下验证:
new Thread(){
@Override
public void run() {
super.run();
List<String> list = new ArrayList<>();
List<String> list1 = new ArrayList<>();
for (int i = 0; i < 1000000; i++){
list1.add("I am "+i);
}
while (true){
list.addAll(list1);
list.add("a");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("size:"+list.size());
}
}
}.start();
上面的代码很简单,就是在线程里尝试不断的让ArrayList添加数据,最终会抛出如下异常:
java.lang.OutOfMemoryError: Failed to allocate a 307546884 byte allocation with 4194304 free bytes and 136MB until OOM
很粗暴的感觉有木有?有的朋友可能会说你这样搞个死循环,最后肯定会出问题丫。是的,我们不是也是想知道它出的是什么问题么,当看到OutOfMemoryError的时候,不是已经验证了ArrayList会导致OOM这一结论。只是为什么,我们还需要进一步的考究。好的,是时候上正菜了,先看其构造函数。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
//无参构造函数 elementData数组赋值为默认的一个空长度的数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
private static final Object[] EMPTY_ELEMENTDATA = {};
//initialCapacity为初始容量
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);
}
}
//以集合的方式来构造ArrayList
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
//只是这里,如果传进来的非Object[].class类型,需要进行一下转换
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
以上的代码整体都很简单,就是初始化一个ArrayList对象,构造方法参数不一样,执行的流程略微有一些差别,但最终的目的不都是为elementData 数组申请内存。好的,数组已经有了,我们看下其操作,这里我们主要看常用的add、remove,其实其余的都类似,只是各自负责的功能不一样。
//直接添加元素,添加在数组的末尾
public boolean add(E e) {
//这个方法实际是做扩容检测以及扩容的,看后面的代码解释 size是实际数组里面已经填充了数据的大小
ensureCapacityInternal(size + 1); // Increments modCount!!
//赋值
elementData[size++] = e;
return true;
}
//在指定位置添加元素
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//这个方法实际是做扩容检测以及扩容的,看后面的代码解释 size是实际数组里面已经填充了数据的大小
ensureCapacityInternal(size + 1); // Increments modCount!!
//将index之后的数据向后移动
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//赋值
elementData[index] = element;
size++;
}
private static final int DEFAULT_CAPACITY = 10;
private void ensureCapacityInternal(int minCapacity) {
//如果是默认的数组,则重新定义其长度大小然后调用ensureExplicitCapacity
//比如初始是0,现在add,则minCapacity为1 最终取值后minCapacity=DEFAULT_CAPACITY 10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
//modCount在每次操作ArrayList的时候都会++,防止在迭代器遍历的时候进行数据操作 那样的话会报java.util.ConcurrentModificationException异常
modCount++;
// overflow-conscious code
//判断需求的长度是否大于当前数组长度,如果大于则说明需要扩容
if (minCapacity - elementData.length > 0)
//真正进行扩容操作的方法
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
//数组当前的长度
int oldCapacity = elementData.length;
//新的长度 为旧长度+旧长度>>1 实际就是每次扩容一半大小 比如10 则扩容到15
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:
//执行数组扩容,并将之前的数据拷贝过去 具体执行者为Arrays类的静态方法copyOf
elementData = Arrays.copyOf(elementData, newCapacity);
}
public static byte[] copyOf(byte[] original, int newLength) {
byte[] copy = new byte[newLength];
//拷贝数组数据的执行者,我们平常做数组拷贝不也这样用么?
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
上面的代码就是add操作的流程代码。先判断是否需要扩容,如果不需要则回到add方法将数据赋值给数组的下标为size+1的数据。如果需要扩容,则进行扩容,每次扩容大小为1.5倍,然后在执行赋值操作。扩容需要做数组复制操作,如果add的不是数组末端,也需要做数组移动操作。接下来,我们在看下remove操作。
public E remove(int index) {
//边界条件判断
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
modCount++;
//数组index下标的值
E oldValue = (E) elementData[index];
//进行下标界定并移动数组 比如现在有10个值,将第5个值移除,那么需要将后面的值依次向前移动一位
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
//返回移除的值
return oldValue;
}
//这个方法最终调用的关键方法fastRemove
public boolean remove(Object o) {
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;
}
//可以看到这个方法最终也是执行了数组移动
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
}
ArrayList进行数据移除,其实就是在数组中,找到对应的下标或者对应的数据,然后依次将后面的数据向前移动一位,之前在对最后一位的数据赋空,注意这里的最后一位是指数组里面实际有数据的大小,由size进行记录。
好了,看完上面的关于初始化、添加、移除的代码,有没有一种其实就是在对数组进行的操作,本质上与我们操作数据并没有什么区别,区别在于进行了封装,可以进行自动扩容,数组的移动也在调用对应的方法之后自动完成,基于上面的分析我们可以来总结下我们在开始提出的问题了。
1、ArrayList的本质是什么?
其本质就是对数组进行的封装,能实现数组的扩容,在操作的时候会对数组进行对应的移动,这一切的封装只是为了方便我们的操作。
2、ArrayList的使用是否会出现OOM?
我们在开始就验证了这个问题,是会出现OOM的。因为其本质是使用了数组,而数组是内存中一段连续的区域。在扩容中,如果找不到我们所需求的那样一段连续的内存区域,则会出现OOM。
3、ArrayList是否线程安全?
我看到不管add或者remove,在操作过程中并没有synchronized关键字的修饰,也没有锁的出现,那么我们可以断定ArrayList是非线程安全的。
4、ArrayList的使用过程中,做了哪些事情?
在add的时候会进行数组扩容,每次为1.5倍,之后进行数组的复制,在将值添加进去。
remove的时候会将待移除数据之后的数据全部向前移动一位,在将最后一位值赋空。
5、ArrayList的效率如何,适用于哪些场景?
从前面的问题可以总结出ArrayList实际就是对数组的操作,如果添加操作,数组的扩容会对效率有一定的影响。如果添加在非末端,那么还需要额外进行一次数组的移动过程,效率是有所牺牲的。移除如果在末端,不受影响,如果在非末端,需要进行一次数组移动过程,有一定的影响。ArrayList更适用于访问次数多(访问实际相当于对数组的访问,直接可以通过下标进行访问,而且快),添加、移除少操作(特别是随机位置的添加、移除)的场景。否则,则需考虑另外的如LinkedList。