概论
集合类是java.util下面的类,挑了几个常用的重点学习,大体体系结构如下(思维导图是用随便下载的Mindjet做的):
注意: 这个图里的关系并不是严格按照jdk1.8源码画的,比如在源码里其实LinkedHashSet类是HashSet类的子类,但在图中二者属于并列关系。这张图只是为了给各种集合类进行逻辑关系上的整理。
ArrayList
- 继承的类:AbstractList(在此类的基础上,ArrayList主要是增加了扩容机制)
- 实现的接口: List、Cloneable、RandomAccess、java.io.Serializable。其中,RandomAccess接口并未给出具体实现,只是单纯的标识这是一个可以进行随机访问的类。也就是说——并不是实现了该接口就可以实现随机访问功能,而是本身就有随机访问功能的类用该接口标识自己的功能。
- 字段(具体解释见注释)
private static final long serialVersionUID = 8683452581122892189L;
/**
* 默认容量为10.
* 注意:不带参数创建一个ArrayList后,容量其实为0。开始添加元素后才变成10.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 静态成员,所有对象实例维护同一个EMPTY_ELEMENTDATA
* 创建一个长度为0的空ArrayList时,维护的数组就是这个空数组
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 创建一个具有默认容量(也就是不带参数)的Arraylist所维护的数组就是该数组
* 虽然跟上面那个都是空数组,但是在第一次扩容时,两者扩大的容量不一样。这个是扩
* 大到默认容量,另一个则遵从其他的扩容规则,后面会说。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* ArrayList底层用来存储数据的Object数组是elementData
* elementData的长度即为ArrayList对象的容量
* 不带参数创建一个ArrayList时elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,长度为0
* 加入第一个元素后长度会变为DEFAULT_CAPACITY,即为10
* */
transient Object[] elementData; // non-private to simplify nested class access
/**
* 含有的元素个数(不是整个ArrayList的容量,而是加入的元素个数)
*/
private int size;
- 方法
- 构造函数有三种:第一种指定初始容量,第二种是不带参数的构造函数,第三种用另一个集合对象实例来初始化。
/**
* 给定一个初始容量构造ArrayList
* 根据指定的初始容量initialCapacity创建一个大小为该数值的数组
*/
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;
}
/**
* 用另一个集合初始化Arraylist对象
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
- 扩容
先看一下添加元素的add方法——需要先执行一个ensureCapacityInternal方法,保证容量够用,再添加元素。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
那么问题来了,这个ensureCapacityInternal方法是如何保证容量够用的呢?在容量不够的时候如何扩容?
首先我们看到这个方法的int型参数minCapacity,较为直观的理解起来就是需要的最小容量,达到这个容量我们才能继续添加元素。比如当前有100个元素,如果我想再添加一个,那至少就得有101个位置,也就是minCapacity=101。
但是,如果根据这个最小容量一个一个的扩充数组大小的话,效率是很低的,因为其实arrayList扩容前后的数组其实是两个数组。扩容时,我们需要先确定新数组的大小,创建新数组,然后再把旧数组的元素复制过来。如果每次添加元素都要进行这样的操作,无疑开销很大。因此,一个比较科学的方法就是在必要的时候(容量不够用了)进行一次扩充,增添n个位置,在这n个位置用完了后再次扩充。而这里的最小容量minCapacity则是用来衡量“容量是否够用”的。如果目前的容量大于等于所需的最小容量,则无需扩容。
除了上面这一段中提及的规则,代码里还考虑了一些特殊的情况。比如,通过下面的代码可以看到,如果一个对象刚刚被创建,还未添加任何元素,并且是由无参数的构造函数构造的(只有在这种情况下elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA),则minCapacity若小于默认容量则变为默认容量,否则不变。也就是从0到1的过程中,容量直接扩为10。而元素个数从10增加到11时,以下这个方法不产生任何影响,minCapacity此时仍然为11。
我个人的理解是,由于ArryList的扩容机制是每次扩大成原来容量的1.5倍,对于很小的数字比如1、2、3、4、5来说,他们的0.5倍是1或2,如果每次扩大1个、2个,就出现了刚才说到的一旦添加元素就要扩容的问题,效率比较低。因此在源码中,出于效率考虑,对于缺省构造函数构造的arrayList,一开始直接扩容为10.
考虑完这样的情况后,调用ensureExplicitCapacity方法。
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
modCount记录该对象被修改的词数,增、删、改时都要加1,查询则不需要。
在ensureExplicitCapacity(int minCapacity)中,若minCapacity(需要的最小容量)大于此时底层数组的长度(这里的length是数组实际容量,字段size是数组中存放的元素个数,size<=elementData.length ), 则调用grow()方法,即扩容。否则什么都不做,也就是ensureCapacityInternal(size + 1)
方法结束,执行elementData[size++] = e;
, 完成元素的增加。
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
接下来我们看grow()函数:
首先用变量oldCapacity记录原数组容量,然后用变量newCapacity记录旧容量的1.5倍作为增长后的新容量(这里用位运算右移代替了除法,效率更高)。若果扩容1.5倍后仍然达不到所需的最小容量minCapacity,则将新容量直接设置为minCapacity。
这里举一个例子,如果创建一个初始容量为1的arrayList(注意:这里讨论的是指定了初始容量的构造函数,因此不会默认扩容到10),在添加第二个元素时,minCapacity为2,而扩容1.5倍后的newCapacity=1+1*0.5,小于2,这就是刚才所描述的情况,此时直接将新容量newCapacity设置为最小容量minCapacity,也就是2。
此外,当数组当前的容量非常大时,1.5倍的length大于Interger.MAX_VALUE,发生溢出,此时的newCapacity为负数,小于minCapacity,则将其变为minCapacity。 也就是说,当数组的容量足够大时,扩容过程变为每次容量加一。
如果新容量newCapacity大于MAX_ARRAY_SIZE,则将newCapacity赋值为hugeCapacity(minCapacity)。
我们先来看一看MAX_ARRAY_SIZE,这是ArrayList类的一个静态变量,表明最大容量。ArrayList的size、minCapacity等变量均是int类型,说到最大容量,大家的第一反应可能是Integer.MAX_VALUE。事实上,有一些虚拟机会在数组中存放一些信息,也就是说虚拟机能分配给一个arrayList对象的内存是size(E)*Integer.MAX_VALUE, 而如果数组中存放Integer.MAX_VALUE个E类型数据,加上数组中存放的额外信息,总开销大于size(E)*Integer.MAX_VALUE,会发生溢出。因此在源码中,MAX_ARRAY_SIZE设置为略小于 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;
当minCapacity小于0时,说明数组当前容量已经达到Integer.MAX_VALUE, 加一就溢出变成了负数,此时抛出OOF。
当设置的新容量大于最大容量时,调用hugeCapacity方法,接下来我们来看看这个hugeCapacity方法:如果需要的最小容量大于刚才所说的最大容量,则将新容量设为Integer.MAX_VALUE,否则将新容量设置为最大容量MAX_ARRAY_SIZE。
关于这个方法,stackOverflow上有一个答主解释的比较清楚:https://stackoverflow.com/questions/35582809/java-8-arraylist-hugecapacityint-implementation
在这里用中文解释一下他的意思:当一个数组容量不够了需要扩容,扩容1.5倍后发现新容量大于我们设置的最大容量(Integer.MAX_VALUE-8),但我们所需要的最小容量并没有超过最大容量时,由于数组长度超过最大容量在很多JVM上会引发out of memory,所以我们将新容量设置为最大长度,这样的话至少在下次扩容前,元素的添加都是安全的。而试想当我们的数组长度已经添加到了最大容量(Integer.MAX_VALUE-8)时,我们还想再添加元素。此时minCapacity和newCapacity都为最大容量+1,虽然可能会引发oof,但还是将容量直接扩为Integer.MAX_VALUE,可以添加元素,但不保证不发生内存溢出的问题。
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
至此,就确定了新容量newCapacity,创建一个容量为新容量的数组,并将原有元素复制进去。
/**
* 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;
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 = Arrays.copyOf(elementData, newCapacity);
}
- 缩容
每次扩容成1.5倍带来的一个问题是,当数组不再添加元素后,所需容量可能小于实际容量,造成了空间的浪费,此时可以调用trimToSize(),令容量等于元素个数。
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
- 减少扩容带来的开销
扩容需要创建新数组+复制原有元素,频繁扩容会到来一定的时间开销。因此,如果预先能估计出所需的容量,可提前用ensureCapacity(c)来将ArrayList对象的容量扩充到c。
/**
* Increases the capacity of this <tt>ArrayList</tt> instance, if
* necessary, to ensure that it can hold at least the number of elements
* specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
```