说明:本文(Android中的ArrayList, 跟java中的ArrayList相差无几)主要是针对不了解ArrayList原理的开发者,也欢迎大神指导指正。
相信很多java或者android开发者,ArrayList是使用的最频繁的一个集合,但是你们是否真的了解ArrayList的真谛?
问题
1.ArrayList的内部采用什么数据结构?
2.ArrayList最大容量是多少?
3.每次ArrayList扩容,默认自动扩容多大?
4.ArrayList会抛出什么异常?什么情况下会抛?
5.ArrayList如何安全的删除item?
6.ArrayList是线程安全吗?如果不是如何避免?
7.使用ArrayList有什么优化点?
大佬,如果你觉得这些是小儿科,走好不送~也欢迎各位大佬指导指正
好,我们言归正传,我们要全面了解ArrayList,带着问题去看源码,下面我们通过一一解答上面的问题来一一解读ArrayList的真谛:
问题1:ArrayList的内部采用什么数据结构
ArrayList内部采用的是数组数据结构,我们看它内部代码:
transient Object[]elementData;
问题2:ArrayList最大容量是多少?
最大容量是Integer.MAX_VALUE,但部分虚拟机上是Integer.MAX_VALUE -8。我们来看下最大容量变量定义:
/**
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE -8;
注释说的很明确,ArrayList在有些虚拟机上保留几个字节(8个字节),所以在这些虚拟机上最大容量是Integer.MAX_VALUE -8。
也许你会说,大哥你不是说最大容量是Integer.MAX_VALUE吗?但是代码定义是Integer.MAX_VALUE -8呀,哪来的Integer.MAX_VALUE呀?大佬,稍安勿躁,来我们看下ArrayList中这段代码
private static int hugeCapacity(int minCapacity) {
if (minCapacity <0)// overflow
throw new OutOfMemoryError();
return (minCapacity >MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
看到了吧?小于0会报oom, 如果大于MAX_ARRAY_SIZE返回的是Integer.MAX_VALUE,但是在部分虚拟机上会报oom
好,我们总结下:ArrayList最大的容量是Integer.MAX_VALUE,在部分虚拟机上实质是Integer.MAX_VALUE -8,超过了Integer.MAX_VALUE -8就会报oom
问题3:每次ArrayList扩容,默认自动扩容多大
这个问题的正确答案有5个答案,看到部分网上面试题讲解时大多都只说了一种,其实不准确。
所谓扩容就是往ArrayList添加元素时,如果当前ArrayList的容量不足以容纳新增的元素时,此时就需要扩大它自身的容量添加新的元素。那么问题来了这个扩大自身容量,每次扩大多少才比较合适呢?新增的元素有多少个就扩大多少个?no,no....如果频繁的添加那么就会造成频繁的动态扩容,这样的开销是挺大的,那多少合适呢?我们来看下源码:
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return true (as specified by {@link Collection#add})
*/
public boolean add(E e) {
//这个方法主要是用于容量不足时,自动扩容
ensureCapacityInternal(size +1); // Increments modCount!!
elementData[size++] = e;
return true;
}
上面是ArrayList添加一个元素的源码,通过这个函数我们知道ArrayList的add方法添加元素默认是添加到最后一个的。add方法中调用了ensureCapacityInternal扩容方法,我们看下ensureCapacityInternal方法:
// minCapacity = 原容量大小+新增元素的个数
private void ensureCapacityInternal(int minCapacity) {
//如果ArrayList是通过new ArrayList()产生的,那么最小扩容取DEFAULT_CAPACITY, minCapacity中的
// 最大值
if (elementData ==DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
ensureCapacityInternal方法内部有调用了ensureExplicitCapacity方法,我们看下ensureExplicitCapacity方法:
// minCapacity = 原容量大小+新增元素的个数
private void ensureExplicitCapacity(int minCapacity) {
// ArrayList修改次数加1
modCount++;
// overflow-conscious code
// 如果当前ArrayList的当前容量比新的要扩展到的容量大,则不需要扩容
// 如果当前ArrayList的当前容量比新的要扩展到的容量小,则通过grow方法进行扩容
if (minCapacity -elementData.length >0)
grow(minCapacity);
}
接下来我们看下grow方法:
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity =elementData.length;// 当前ArrayList的容量
int newCapacity = oldCapacity + (oldCapacity >>1); // 当前ArrayList的容量增大到原来的1半
if (newCapacity - minCapacity <0)// 如果ArrayList的容量增大到原来的1半还不足以容量要新增的元素容量,则扩容容量采用minCapacity
newCapacity = minCapacity;
if (newCapacity -MAX_ARRAY_SIZE >0)// 如果新的容量大于最大容量的定义,则通过hugeCapacity方法返回最新的容量值
newCapacity =hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
hugeCapacity方法上面已经讲过,这里就不讲了
总结下这个问题的答案:
答案1:DEFAULT_CAPACITY(值是10),说明:如果通过new ArrayList()来生成,第一次扩容默认采用DEFAULT_CAPACITY
答案2:原容量的一半:oldCapacity + (oldCapacity >>1),说明:默认扩容的容量是原容量的一半newCapacity,条件是newCapacity比minCapacity大且小于最大容量的定义
答案3:原容量+新增的元素的个数,说明:默认扩容的容量是原容量的一半newCapacity,但是如果newCapacity比minCapacity小,再扩容的大小为minCapacity
答案4:Integer.MAX_VALUE,说明:newCapacity > Integer.MAX_VALUE - 8,则扩容的大小是Integer.MAX_VALUE
答案5: Integer.MAX_VALUE - 8
问题4:ArrayList会抛出什么异常?什么情况下会抛?
IndexOutOfBoundsException: 当读写操作的下标越界时
ConcurrentModificationException: 发生在多线程同时增、删的操作
IllegalArgumentException: new ArrayList如果指定的大小为负数时;或者subList的fromindex > toIndex时
NullPointerException: new ArrayList如果传入的集合为空时;或者添加的集合为空时;或者删除的集合为空时;或者将ArrayList给定的数组为空时;
OutOfMemoryError: 扩容的大小为负数或者超出了虚拟机的容许的ArrayList容量时
NoSuchElementException:发生在迭代器next或者previous操作越界时
IllegalStateException: 发生在迭代器设置、删除操作未先next操作的情况下
最常见的莫过于IndexOutOfBoundsException、ConcurrentModificationException、NullPointerException
问题5:ArrayList如何安全的删除item
也许你会说,直接remove不就可以了。大佬,估计你撸的码不够。。。
为什么会问这个问题呢?因为开发过程中这个问题还是时常遇到的。
非安全情况下删除item会出现什么现象呢?
情况1:单线程中在for循环中删除,就会抛出IndexOutOfBoundsException
情况2:多线程中同时增、删页会抛出IndexOutOfBoundsException
情况3:多线程迭代器中同时增、删页会抛出ConcurrentModificationException
我们针对情况1,我们的解决方案是:
使用迭代器进行删除,参考如下代码:
ArrayList list =new ArrayList(10);
Iterator it = list.iterator();
Integer item =null;
while(it.hasNext()){
item = it.next();
if(item ==null){
it.remove();
}
if(item ==10){
it.remove();
}
}
对于情况2和情况3在问题6中作答
问题6:ArrayList是线程安全吗?如果不是如何避免?
答案肯定是非线程安全,如果一个线程在删除;一个线程在添加;一个线程在读取,那么就造成ArrayList的size变化,具体变成多少是不确定的,所以很容易出现IndexOutOfBoundsException。
既然是非线程安全,那有什么方法可以避免吗?
方案一:如果能确定在单线程中实现相应的业务逻辑,直接采用单线程就可以
方案二:使用CopyOnWriteArrayList
方案三:ArrayList加锁:synchronized或者lock
方案四:直接在初始化时用 Collections.synchronizedList 方法进行包装
List list = Collections.synchronizedList(new ArrayList(20));
问题7:使用ArrayList有什么优化点?
通过上面的讲解结合源码,最大的优化点在于添加元素和初始化操作;如果你确定你的ArrayList的容量或者大概的容量,那么在new ArrayList时指定容量,比如你的某个页面ArrayList的容量大概在在10个左右,那么你就通new ArrayList(10)来初始化。
初始化指定容量的好处在于如果容量范围确定,可以避免添加元素时因为扩容而增加的开销
到此ArrayList的解读、常见问题及解决方案、优化均已经讲解完,恭喜你又成长了(为你点赞!)
以上均是个人的见解,如果有不同异议或者不正确的,欢迎指正