ArrayList类
关于这个类想必大家已经非常熟悉了,是我们编写代码的好帮手。今天我们来从源码的角度在深入熟悉我们的ArrayList.并且对比CopyOnWriteArrayList比较两者之间区别。首先我们先来看ArrayList。
ArrayList的构造函数
// 这是类的成员们
// DEFAULT_CAPACITY这个是默认数组的大小。前提是我们并没有设置数组的长度
private static final int DEFAULT_CAPACITY = 10;
// 下面的这两个数组,这是用来分辨elementData数组,后面再说
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 元素真正存放的地方
transient Object[] elementData;
// 数组里面元素的个数。
private int size;
这两种初始化方法,一种是用户设置了数组大小,一种是没有设置数组大小。没有设置大小的话,那么数组默认为DEFAULTCAPACITY_EMPTY_ELEMENTDATA ,并且默认大小为10;如果设置了大小,且大于0,那么会为数组初始化设置的大小的数组;如果设置的大小为0,那么数组默认为EMPTY_ELEMENTDATA 。默认大小为0;
可是不论是EMPTY_ELEMENTDATA 还是DEFAULTCAPACITY_EMPTY_ELEMENTDATA ,数组一开始的长度为0。
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
添加一个元素
public boolean add(E e) {
// 在添加一个元素的时候,首先要判断是否需要进行扩容
ensureCapacityInternal(size + 1);
// 将元素加入
elementData[size++] = e;
return true;
}
我们着重来看一下扩容这部分,这分了两种情况:
- 当数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,刚一开始加第一个元素时,minCapacity为1,从1和10之间选出最大的数作为现在数组的最小容量。现在数组的实际长度为0,当然需要扩容,并且我们需要将数组扩容到10.
- 当数组不为DEFAULTCAPACITY_EMPTY_ELEMENTDATA
此时数组的实际长度要不是用户设置的大小,要不是0;
如果是用户设置的大小,那么只要minCapacity比这个大小小,那么就不需要扩容。
但如果用户设置的大小为0那么从第一次开始,以后每一次都需要扩容。前面几种情况下,只有等到数组满了之后才开始进行扩容,且新扩容的大小是newCapacity = oldCapacity + oldCapacity / 2;比如之前是10 扩容之后就是15;最后的这种情况每一次加元素都要扩容,可想而知有多麻烦。
人家好歹一次扩容,还能歇一会。最后这个,每次都要扩容。
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
删除一个元素
public E remove(int index) {
// 当前下标是否超出边界
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
// 将数组从index + 1处的到最后一个元素,挪到数组从index下标开始的地方
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
CopyOnWriteArrayList
这个类是为了弥补ArrayList的不足。因为ArrayList在多线程的情况下,无法保证线程安全。如果一个线程加数据,一个线程删数据,那很有可能会出现下面这种错误。
线程1将m放入最后一格中,上面我们说过,加元素之前要先判断是否需要扩容,我们现在还不需要扩容。但是很不幸的,我们执行完这一句时间片段到了,此时的size为9
而线程2要进行删除,假如要删除d,这个线程得到了运行机会,当他已经删除了d之后,但是还没有将size减1,也没有将8位置上的值赋值为null时间片段就到了。
回到线程1,现在要将m加到index为9的那,size变为10。线程1结束,线程2开始将size-1,并且将size-1下标处的元素赋值为null;
那么怎么解决这个问题呢,CopyOnWriteArrayList这个类就可以很好地解决。
添加一个元素
这里面涉及到一个很重要的类ReentrantLock ,作用和synchronized很相似,lock和unlock相当于synchronized的左右花括号。但还是有一些不同的有兴趣的同学可以去看他的源码,这里就不做过多介绍。
CopyOnWriteArrayList不需要进行扩容。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 得到当前的数组
Object[] elements = getArray();
int len = elements.length;
// 将当前的数组增加一格
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
// 将新数组设置为当前数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
删除一个元素
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
// 这表示要删除的是数组的最后一个元素
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
// 产生一个新的数组,长度为之前的长度减一
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
我们发现现在不论是删除还是添加,都已经加上了锁,并且是同一把锁,这样就可以保证线程安全性了。