在Java编程中,常常需要集中存放多个数据,从传统意义上讲,数组是我们的一个很好的选择,前提是我们事先已经明确知道我们将要保存的对象的数量。一旦在数组初始化时指定了这个数组长度,这个数组长度就是不可变的,如果我们需要保存一个可以动态增长的数据(在编译时无法确定具体的数量),
List 这个集合类是便为我们提供了相当于动态数组的功能。这个类中add方法尤为重要。
1 目标
本次源码分析的目标是深入了解 List类中 add 方法的实现机制。
2 分析方法
首先编写测试代码,然后利用 Intellij Idea 的单步调试功能,逐步的分析其实现思路。
测试代码如下:
List<String> mList=new ArrayList<String>();
mList.add("张三");//断点
mList.add("李四");
mList.add("王五");
mList.add(1,"赵六");//断点
3 分析流程
点击调试按钮,开始分析流程。
3.1 构造函数
首先进行的是构造函数的分析,点击 Shift+F7进入构造函数实现。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
点击进入后,我们看到的是以上代码,这里疑惑就来了,这个elemenData代表什么呢?这个DEFAULTCAPACITY_EMPTY_ELEMENTDATA 又是什么意思呢?
我们可以在定位在elemenData处按 Ctrl+B进入查看做进一步分析。
transient Object[] elementData;
通过上述代码我们知道,elementData是一个Object型的数组。那为什么会定义成Object型的呢? 我们可以想一下,如果是String,int或是其他类型的,那么存放的数据是不是就受到了限制了呢?而Object型的就能保证我们可以将任何类型的数据存放进去。
可以看到,Object前面有一个transient,那这个transient是什么意思呢?
我们都知道一个对象只要实现了Serializable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关心具体序列化的过程,只要这个类实现了Serializable接口,这个类的所有属性和方法都会自动序列化。然而在实际开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,而其他属性不需要被序列化,打个比方,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。
同理我们来看DEFAULTCAPACITY_EMPTY_ELEMENTDATA:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
可见这是一个Object型的常量数组,并将其赋了空值。到这里我们就能知道构造方法里是将elemenData数组初始化为空。
3.2 add(E e)方法
接下来我们分析add( E e)方法的实现机制。
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
确定内部容量是否够了,size是数组中数据的个数,因为要添加一个元素,所以size+1,先判断size+1的这个个数数组能否放得下,就在这个方法中去判断 数组.length是否够用了。
从上述代码可以看到,这里调用了ensureCapacityInternal方法,我们来看看其实现机制:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
由此我们可以看出这是一个确定内部容量的方法,这里我们先定位在DEFAULT_CAPACITY上Ctrl+B进去可以看到其值为10;首先判断初始化的elementData是不是空的数组,也就是没有长度因为如果是空的话,minCapacity=size+1;其实就是等于1,空的数组没有长度就存放不了,所以就将minCapacity变成10,也就是默认大小,但是在这里,还没有真正的初始化这个elementData的大小。
我们继续按F7调试下一步,将会运行ensureExplicitCapacity方法
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
minCapacity如果大于了实际elementData的长度,那么就说明elementData数组的长度不够用,不够用那么就要增加elementData的length。有的同学可能会疑惑minCapacity到底是什么呢?我们来分析一下:
由于elementData初始化时是空的数组,那么第一次add的时候,minCapacity=size+1;也就minCapacity=1,在上一个方法(确定内部容量ensureCapacityInternal)就会判断出是空的数组,就会将minCapacity=10,到这一步为止,还没有改变elementData的大小。
elementData不是空的数组了,那么在add的时候,minCapacity=size+1;也就是minCapacity代表着elementData中增加之后的实际数据个数,拿着它判断elementData的length是否够用,如果length不够用,那么肯定要扩大容量,不然增加的这个元素就会溢出。
F7调试下一步执行grow方法,arrayList核心的方法,能扩展数组大小的真正秘密。我们下一节详细分析该方法。
3.3 grow(int minCapacity)方法
按住 Shift+F7进入该方法的实现代码。
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);
}
我们慢慢来分析代码的含义:
int oldCapacity = elementData.length;
//将扩充前的elemenData大小给oldCapacity
int newCapacity = oldCapacity + (oldCapacity >> 1);
//newCapacity的大小就是1.5倍的oldCapacity,相当于将elemenData之前的大小扩充为原来的1.5倍。
if (newCapacity -minCapacity < 0)
newCapacity = minCapacity;
//这段代码判断将elemenData大小扩充1.5倍过后与minCapacity作比较,在§3.2节我们已经知道 minCapacity默认值为10。判断成立便将minCapacity赋值给newCapacity,在这里就是真正的初始化elemenData的大小了。这段代码适应于elemenData数组为空的时候,elementData.length=0,则oldCapacity=0,newCapacity=0,所以该判断成立。
if (newCapacity - MAX_ARRAY_SIZE> 0)
newCapacity = hugeCapacity(minCapacity);
//这里有个MAX_ARRAY_SIZE,可是MAX_ARRAY_SIZE代表什么呢?
按住Ctrl+B查看源码:
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
根据注释我们了解到MAX_ARRAY_SIZE代表最大容量限制。所以代码含义是如果newCapacity超过了最大容量限制的话就调用hugeCapacity方法,将能给的最大值给newCapacity。hugeCapacity方法又是怎么一回事呢?我们下一节再来介绍。
elementData = Arrays.copyOf(elementData, newCapacity);
//最后copy数组,改变数组容量。
3.4 hugeCapacity()方法
按住 Shift+F7进入该方法的实现。
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
从上述代码可以看出,hugeCapacity方法用于确定elemenData数组的最大容量。如果minCapacity > MAX_ARRAY_SIZE说明扩容时扩得过大,则返回Integer.MAX_VALUE,否则返回MAX_ARRAY_SIZE。在上一节已经说过MAX_ARRAY_SIZE的含义,那MAX_ARRAY_SIZE和Integer.MAX_VALUE的值具体又是多少呢?
按住Ctrl+B查看源码:
@Native public static final int MAX_VALUE = 0x7fffffff;
3.5 add(int,E)方法
按住 Shift+F7进入该方法的实现代码:
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
此方法用于在特定位置将元素添加进集合,也就是插入元素。先调用rangeCheckForAdd方法检查插入的位置是否合理,然后调用ensureCapacityInternal方法将index之后的元素都往后移动一位(具体分析看上面的小节),并在目标位置上将要插入的元素存放进elemenData数组。最后数组大小加一。
4 总结
本文分析了List类的 add 方法,包括在末尾添加元素和在指定位置添加元素。通过分析我们知道List集合就相当于一个elemenData数组,该数组可以实现自动扩容。当我们调用add方法的时候实际上的函数调用顺序为:
后面两个方法并不是每次调用add方法时,当添加进的元素数量大于elemenData默认容量时会调用grow方法进行扩容,而当扩容过大时会调用hugeCapacity方法进行最大容量限制。
对比一下传统的数组,在使用数组时我们需要事先固定好其大小,然而在实际开发中我们往往是不能预测究竟会存放多少数据进去的,当我们需要改变数组大小时,又要回过头开修改代码,这岂不是徒增麻烦。而List集合就避免了这种麻烦,给开发人员与维护人员提供了极大地便利,所以在实际开发中被人们广泛使用。了解其实现机制是很有意义的。