本文将从源码角度来分析和对比一下集合扩容相关的知识,涉及到的集合框架有:ArrayList,Vector,HashMap,ArrayMap,SparseArray。下面先从ArrayList开始。
ArrayList
ArrayList是以数组实现的一个集合类,在ArrayList的源码中可以看到,所有元素都是被储存在elementData这个全局的数组变量中,而所谓的扩容也是针对这个数组对象进行操作。具体来说,当一个添加元素的动作,即add或addAll被执行时,都会先调用ensureCapacityInternal(…)方法进行容量预检,如果当前elementData数组的容量不足以完成本次添加操作便会进行自动扩容。该方法代码如下:
//这是一个私有方法,ArrayList提供了另一个public的扩容方法ensureCapacity以满足外界手动扩容的需求 //其实质也是调用了本方法,在此不做累述 //minCapacity是指本次添加操作后所需要的数组容量,即elementData.length + newSize private void ensureCapacityInternal(int minCapacity) { //这个if判断如果成立,则代表当前ArrayList为空,而本次添加操作是第一次添加。 //此时需要将ArrayList的容量扩充至DEFAULT_CAPACITY=10和本次添加操作所要求的minCapacity二者中的较大者。 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { //操作计数器+1 modCount++; // overflow-conscious code // 确保需要进行扩容,即当前数组的容量小于本次添加操作所要求的新的总容量 if (minCapacity - elementData.length > 0) grow(minCapacity); }
在ensureCapacityInternal方法中,一开始会对本次添加操作是否为第一次添加进行判断。从源码:private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 中可以得知,DEFAULTCAPACITY_EMPTY_ELEMENTDATA变量指代的是一个空的对象数组,这也是通过无参构造函数new出来的ArrayList对象的初始状态,也即 elementData = {};这种情况下的第一次扩容,如果所要求的新容量小于10,则会直接扩容至10。
下面继续看到grow方法,这个方法是ArrayList扩容操作的实现
private void grow(int minCapacity) { // overflow-conscious code //记录当前elementData数组的容量 int oldCapacity = elementData.length; //新的容量暂定为当前容量的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); //如果1.5倍容量仍不满足minCapacity所要求的,则直接将容量定为minCapacity if (newCapacity - minCapacity < 0) newCapacity = minCapacity; //如果新的容量超过了Integer.MAX_VALUE - 8,则做最大化处理 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: //将当前数组copy到新的数组中 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; }
最后的扩容操作,Arrays.copyOf方法会新创建一个大小为newCapacity的数组,然后通过System.arraycopy方法将之前elementData数组中的元素复制到新数组中(起点的index为0),并将新数组返回给变量elementData完成扩容。
Vector
Vector和ArrayList其实身出同门,都是AbstractList的子类并且都实现了List接口,所提供的功能也基本相同,最大的不同在于Vector所有的公有API都是加锁的,也即Vector是线程安全的。但这也正是它不受欢迎的原因之一,太重了。。。言归正传,我们来关心一下Vector的扩容操作。其实基本跟ArrayList相同,方法如下:
private void ensureCapacityHelper(int minCapacity) { // overflow-conscious code //当前容量不够,扩之 if (minCapacity - elementData.length > 0) grow(minCapacity); }
可以看到,基本只是方法名不一样而已,再来看看Vector的grow函数,这里还真有些不同:
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; //不同之处在这里,MARK int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }
上面代码块中MARK出的那一句便是不同之处,可以看出Vector的扩容是先判断有没有大于0的capacityIncrement,该变量是通过:
public Vector(int initialCapacity, int capacityIncrement) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity); this.elementData = new Object[initialCapacity]; this.capacityIncrement = capacityIncrement; }
这个带参构造函数传入的,如果不调用这个构造函数,则capacityIncrement为0。在capacityIncrement不为0的情况下,则每次扩容都暂定扩大capacityIncrement个,反之扩容oldCapacity个,即当前容量的一倍。后续的扩容操作和ArrayList一致,也是通过Arrays.copyOf方法来完成,这里不再重复分析。
HashMap
下面来看另一个熟人–HashMap。不同于上面两个List的实现类,HashMap是一个采用哈希表实现的键值对集合,继承自AbstractMap,实现了Map接口并使用拉链法解决Hash冲突,其内部储存的元素并不是在连续内存地址的,并且是无序的。此处我们只关心其扩容操作的逻辑和实现,先说一下,由于要重新创建数组,rehash,重新分配元素位置等,HashMap扩容的开销要比List大很多。下面介绍几个和扩容相关的成员变量:
//哈希表中的数组,JDK 1.8之前存放各个链表的表头。1.8中由于引入了红黑树,则也有可能存的是树的根 transient Node<K,V>[] table; //默认初始容量:16,必须是2的整数次方。这样规定是因为在通过key来确定元素在table中的index时 //所用的算法为:index = (n - 1) & hash,其中n即为table容量。保证n是2的整数次方就能保证n-1的低位均为1, //这样便能保留hash(key)得到的hash值的所有低位,从而保证得到的index在n范围内分布均匀,因为hash算法的结果就是均匀的 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认加载因子为: 0.75,这是在时间、空间两方面均衡考虑下的结果。 //这个值过大会导致发生冲突的几率增加,容易形成长链表,降低查找效率;太小则会导致频繁的扩容,降低整体性能。