概述
在我们使用集合的过程中ArrayList是最频繁使用之一,以前也只是使用而已,都没有认真看过这种源码,现在了解下之后做下总结,我们看下ArrayList的add(E e)方法
public boolean add(E e) {
// 判断是否需要进行内部扩容,默认情况下size大小为10
ensureCapacityInternal(size + 1); // Increments modCount!!
// 在位置size处新增元素,size是全局变量
elementData[size++] = e;
return true;
}
从ArrayList集合的add方法的代码上我们可以发现首先要判断当前数组大小是否已经超过数组最大容量,这里需要了解的是elementData是一个Object[]类型的数组,然后在elementData的下标位置size处添加元素,这个也保证了ArrayList集合的有序性,我们在看下ensureCapacityInternal()方法及相关的实现
private void ensureCapacityInternal(int minCapacity) {
// 判断elementData是否是空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 初始容量为默认值10
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 进行容量判断扩容
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
// 记录修改变化的次数,用于避免获取到不正确的结果
modCount++;
// 判断当前需要的容量是否已经大于数组长度
// overflow-conscious code
if (minCapacity - elementData.length > 0)
// 进行扩容
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 新的容量大小为原来的大小的1.5倍
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:
// 将原来的数组中的值复制到新的数组中,并返回给elementData
elementData = Arrays.copyOf(elementData, newCapacity);
}
从上面的扩容代码ensureCapacityInternal()方法中我们可以明显看出在初始化ArrayList集合时,并且没有指定集合大小时的默认值为10,在ensureExplicitCapacity()方法中判断当前的需要的容量值是否已经大于已经存在的数组容量,大于就通过grow()方法进行扩容,在grow()方法中,新的容量大于为原来的容量大小的1.5倍,然后在将原来elementData中的值复制到新的数组对象中并返回给elementData,这样ArrayList集合就完成了数据的新增以及扩容操作。这里还需要注意的是minCapacity的值与size并不一定是相同的,至少是size<=minCapacity的关系。
下面看下get(int index)方法及相关的实现
public E get(int index) {
// 检查下标是否大于集合中元素的个数
rangeCheck(index);
// 返回值
return elementData(index);
}
private void rangeCheck(int index) {
// 判断下标是否大于集合中元素的个数
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
E elementData(int index) {
// 返回数组指定下标处的值
return (E) elementData[index];
}
在get()方法中,它首先检查了传入的index下标是否超过了数组elementData中元素的个数,然后对下标index处的值进行返回。我们思考下在获取index下标值的时候是否会存在线程安全的问题?
接着看下ArrayList的remove(int index)方法
public E remove(int index) {
// 检查下标是否大于集合中元素的个数
rangeCheck(index);
// 记录修改变化的次数,用于避免获取到不正确的结果
modCount++;
// 获取elementData数组下标处的值
E oldValue = elementData(index);
// 需要移动的元素个数
int numMoved = size - index - 1;
if (numMoved > 0)
// 从elementData的index+1元素开始复制numMoved个元素到elementData的index开始的位置
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将原来数组下标size处的值置空
elementData[--size] = null; // clear to let GC do its work
// 返回删除掉的值
return oldValue;
}
看下remove()方法,它还是首先会检查传入的下标index是否超过了集合中元素的个数,然后记录下集合中元素的变化次数,获取到elementData下标index处的值作为返回值,然后开始进行元素的移动操作,它是通过直接替换的方式进行的,从而完成index处元素的删除操作。我们思考下如果在多个线程的情况下,一个线程正在执行remove操作,一个线程执行的get操作,这个时候会不会出现线程安全的问题?那么我们该怎么来实现ArrayList的线程安全呢?synchronized和ReadWriteLock都可以实现,我这里需要说的是Java自己提供的CopyOnWriteArrayList实现的线程安全,在说这个之前我们看下modCount的一个用法
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
// 这里进行了当前的modCount和进入循环时的modCount的判断,如果发生了变化则说明当前循环已经
// 发生了错误的数据
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
接下来我们就看下CopyOnWriteArrayList这个集合,这个集合主要是用于读多写少的情况下,至于原因我们接下来将会了解到,先看下add(E e)方法
public boolean add(E e) {
// 初始化重入锁
final ReentrantLock lock = this.lock;
// 加锁,finally中释放锁
lock.lock();
try {
// 获取已有的数组array存放到的数组对象变量中
Object[] elements = getArray();
int len = elements.length;
// 进行新数组的+1扩容,这样可以防止空间浪费,复制所有的原数组中的元素到新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 在新数组末尾添加新值
newElements[len] = e;
// 将新数组引用赋值给全局变量array
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
我们可以通过add()方法看到它首先就进行了加锁操作,这里就保证了线程的安全,然后获取当前对象中已有的数组array的引用到elements变量中,再创建一个新的数组,这个新数组的大小为elements长度+1,这样的方式可以防止空间浪费,同时将原来的数组中的元素全部复制到新的newElements数组中,在这个新数组的末尾添加新增的值,最后将新数组引用赋值给集合中的全局变量array,这样原来的array引用最终会被jvm回收掉。到了这里不知道大家有没有注意到新的数组是复制原来数组的全部数据的,如果原来的数组本来就已经很大了,假如快接近内存的2/3了,这个时候复制的话肯定会造成内存泄漏的,因为原来的数组还没有来得及释放,那么大家在思考下HashMap是怎么扩容的呢?它会不会造成内存泄漏呢?当然HashMap不会,扩容的时候只是新建一个数组,然后修改对象的引用,并没有进行数据的复制。也因为在CopyOnWirteArrayList集合上每次新增都会进行数据的复制,所以它的效率非常低,这也是前面提到的它适合读多写少的原因。
下面看下CopyOnWirteArrayList集合的get(int index)方法
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
在代码中可以看出,它并没有像ArrayList那样首先去检查下标值是否大于集合中元素的个数,而是直接通过下标去获取值,如果下标越界,则直接抛出ArrayIndexOutofBoundsException异常,不去检查应该是因为数组的长度就等于数组中元素的个数,而在ArrayList中数组的长度并不一定等于元素中的个数。
最后看下CopyOnWirteArrayList集合的remove(int index)方法
public E remove(int index) {
// 初始化重入锁
final ReentrantLock lock = this.lock;
// 加锁,finally中释放锁
lock.lock();
try {
// 获取已有的数组array存放到的数组对象变量中
Object[] elements = getArray();
int len = elements.length;
// 获取下标index处的值
E oldValue = get(elements, index);
// 获取需要移动的元素个数
int numMoved = len - index - 1;
// 不需要移动元素
if (numMoved == 0)
// 直接复制元素,将新数组引用赋值给全局变量array
setArray(Arrays.copyOf(elements, len - 1));
else { // 需要移动元素
// 新建一个数组大小,长度为原数组长度-1
Object[] newElements = new Object[len - 1];
// 分2段数据复制到新数组中
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
// 将新数组引用赋值给全局变量array
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
在remove()方法的代码中,它也是首先进行加锁操作,然后在进行元素的复制操作的,最后修改数组的引用,因此从这几个方法中我们都可以看出来这里是采用了2个数组进行存储,一个用于指向old数组,一个用于指向new数组,虽然最终都是指向了new数组,但是在remove或者add的时候,如果这个时候有get操作,这个get肯定是去获取已有的数组,而不是还在操作中的remove或add方法中的新数组,那么这个时候就可能会产生幻读,因此这也是使用CopyOnWirteArrayList集合需要注意的一点。
最后
以上就是眼睛大鞋子为你收集整理的ArrayList和CopyOnWriteArrayList的add,get,remove方法的简单分析的全部内容,希望文章能够帮你解决ArrayList和CopyOnWriteArrayList的add,get,remove方法的简单分析所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复