在add方法中,有一段注释为
// overflow-conscious code
即考虑了溢出情况,那么究竟是为何溢出,以及他是如何处理的呢?仔细阅读源码,了解处理的逻辑
add()的基本逻辑,1.判断是否要扩容 2.添加进数组
了解几个基本概念
集合数组的长度 elementData.length
集合元素的个数 size
待添加的元素长度(1
),待添加的集合长度(num
)
需求的最小扩容长度minCapacity = size + 1
, minCapacity = size + num
以上概念在add(E e)
addAll(Collection<? extends E> c)
中都有体现
public boolean add(E e) {
//单个元素size + 1
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();//将待添加的集合转为数组
int numNew = a.length;
// 集合数组size + numNew
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
那么进一步看判断扩容方法,
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}
//int DEFAULT_CAPACITY = 10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
首先内部逻辑里面calculate只是判断本集合是否为空元素集合,如果是空元素集合,返回Math.max(DEFAULT_CAPACITY, minCapacity)
,即Math.max(10, 扩容长度)
。
如:通过空参构造器新建一个arraylist,根据构造器定义,此时集合数组长度为0,通过add添加一个string型数据“hello”,则会返回Math.max(10, 1)
,即初次扩容长度为10。
而本文讨论的溢出情况更多是第二种情况,即非空元素集合
如果是非空元素集合,则直接返回minCapacity
,那么接着作为形参传入ensureExplicitCapacity()
private void ensureExplicitCapacity(int minCapacity) {
//标记集合被修改了几次,和iterator中的expectedModCount对应,
//具体在ConcurrentmodificationException异常中会体现。
//简单来说,就是arraylist是非同步,线程不安全的集合。
//在使用迭代器迭代的过程中,我们希望是只读不写,但却并没有阻止对集合元素的修改,于是便有可能发生迭代异常。
//在使用迭代器的过程中,如果不是用iterator内的方法remove,而是直接调用list.remove,甚至是其他对集合元素的改动,则modCount和expectedModCount会不吻合,即抛出该异常。(iterator.remove会强制同步modCount和expectedModCount)
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
这里涉及到第一个溢出点。
传入的形参minCapacity溢出,即 size + num
溢出
首先数组的最大长度Integer.MAX_VALAUE = 0x7fffffff;
即
2
31
−
1
2^{31}-1
231−1约为21亿,若:size = 1
,numNew= MAX_VALAUE
, 则此时minCapacity = MAX_VALAUE + 1
,发生溢出。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); //和newCapacity = oldCapacity * 1.5 不同,*1.5后自动类型转换为double就不再溢出了
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)//MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 一些JVM会保留头文件信息
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow 此处是对最开始溢出情况的处理
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
这里面处理的基本逻辑是(先不考虑溢出)
1.临时变量newCapacity
是原数组长度的1.5倍
2.取newCapacity
和实际需要的minCapacity
中的较大值,重新赋给newCapacity
3.若newCapacity
在MAX_ARRAY_SIZE
和Integer.MAX_VALUE
之间,判断实际需要的minCapacity
是否也在MAX_ARRAY_SIZE
和Integer.MAX_VALUE
之间。若在,则直接扩容到数组长度理论最大值,若不在,则扩容到MAX_ARRAY_SIZE
那么结合第一种溢出情况,我们可以总结以下两种溢出:
1.需求的最小长度溢出,即minCapacity = size + num
溢出
2.原数组长度扩容1.5倍溢出,即newCapacity = oldCapacity + (oldCapacity >> 1)
溢出
如果直观点写,可以写成类似于
if (minCapacity < 0){//需求最小的扩容长度直接溢出,最严重的情况,直接抛异常
throw exception...
}else if (newCapacity < 0){//1.5倍扩容溢出,则取需求最小扩容长度
newCapacity = minCapacity
if (newCapacity > MAX_ARRAY_SIZE){ //对临界长度MAX_ARRAY_SIZE的优化
newCapacity = Integer.MAX_VALUE;
}
}else {//均未溢出,则取1.5倍扩容长度和最小扩容长度的较大值
newCapacity = Math.max(newCapacity, minCapacity)
if (newCapacity > MAX_ARRAY_SIZE){ //对临界长度MAX_ARRAY_SIZE的优化
newCapacity = (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
}
elementData = Arrays.copyOf(elementData, newCapacity)
而在jdk1.8中的写法逻辑更复杂一点,这里有个比较老的解答:链接,大意是关于使用if(a - b < 0)
而不是 if (a < b)
的形式来比较大小,虽然也没解释清楚为什么会写成这样……