前几节我做了大量铺垫:变量、数组、接口、封装、多态、集合框架图等具体的大家参考自己的知识树来记忆,这一节属于加强理解,在这里我假定大家都了解以上所述的知识点,不会的同学一定要先搞清楚再继续往下。这一节将会为大家揭开常用的集合实现类ArrayList的神秘面纱,这也是面试中常问的一个数据结构。
好的废话不多说先找到我们的入口方法:构造器,毕竟我要通过new ArrayList()的方式来创建集合对象,这样会调用默认的构造器,那么现在把我们的构造器祭出来:
/**
* Constructs an empty list with an initial capacity of ten.
*/
贴心翻译:构建了一个有着一个10初始容量的空列表
注意:这里是错误的,大家可以看看这个默认值是一个空的数组。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
没啥好说的,这里的elementData 就是用来存放数据的地方,这里就是把这个变量赋值了一个初始值为空的数组。那么大家new出来这个集合肯定要用,来看看常用的操作:add、remove、contains等方法,这些方法我们都知道定义在父接口Collection中,代表了集合的基本操作,上次我说了先看接口定义了解对象具有的行为抽象,然后再看具体实现了解开发人员的思路,那么我们开始逐个击破这几个方法的实现。
一、add方法
/**
* 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})
*/
贴心翻译:追加指定的数据放到列表的末尾~如果添加成功返回true
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!增加修改次数
elementData[size++] = e;
return true;
}
Ok以上就是add的方法我们来逐一讲解,先来看看ensureCapacityInternal方法:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//这里很明确,就是如果保存数据的容器是刚才初始化的空数组,那么取最小容量和默认容量的最大值,如果我们没有设置minCapacity那么这里就是DEFAULT_CAPACITY=10
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
//这一步呢就是确保容量足够放置我们的数据,说白了就是保证装数据的数组要足够长度来容纳我们的数据
}
下面就是ensureExplicitCapacity方法:
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
//首先把修改次数增加,modCount就是ModifyCount代表了你对集合的修改次数~
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
//这里呢就是如果现有的数组不足以容纳我要存放的数据那么我需要增加数组的容量,还记得我们传进来的minCapacity值是size+1么所以如果加一后的长度已经大于了现有数组的长度了,也就是数组overflow了,我是不是需要增加数组长度,理所当然对吧。
}
下面来继续看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
*/
贴心翻译:通过minimum 参数来增加容量来保证列表能够存放我们的数据
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);
这段代码很明了对吧,就是if判断比较多,来我给大家解释解释:
- 把原有的列表长度保存到oldCapacity
- 将原来的长度在原有长度上增加一半,比如我就让初始的长度为10,当我要增加一个元素的时候,那么现在传入进来的就是11,那么我需要对10容量的列表扩容,那么就通过10+10>>1=15对吧,右移表示除2,那么新长度newCapacity就是15。
- 这时,拿新扩容的容量15和传入的容量11作比较,如果新扩容的容量还不足以存放数据,那么我就把传入的容量作为新容量
- 判断如果我扩容后长度达到可以容纳的最大值,即Integer.MAX_VALUE - 8【这里-8是因为数组也要保存元数据,比如数组的长度,这是属于JVM的规范啦,感兴趣的同学可以异步我的jvm系列】,那么就进行更大容量的扩容hugeCapacity,来看看这个方法具体怎么做的:
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//如果传入参数小于0那么就认为超过内存限制了,就抛出异常
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
//如果传入的容量大于数组的最大容量了,那么就分配一个整形的最大值,也就是这里就假设当前运行的JVM规范能够给的最大值,也就是不减8的容量,否则还是返回整形最大值-8的值。
}
},其实这个方法hugeCapacity就是强制给一下-8后得值,如果还不行那么就GG对吧。
- 那么都设置好了之后,就采用Arrays.copyOf方法把现有的数组复制到新数组
那么,以上就是完整的add方法,是不是很简单,符合我们的逻辑思维吧,总结一下就是增加的时候看看容量够不够,如果够那么直接把新增的数据放到数组的末尾即可,即elementData[size++] = e;如果容量不够,那么我们就把数组进行扩容后再放数据。
- remove方法
1、public E remove(int index);
2、public boolean remove(Object o);
remove方法有两个,可以看到他们的返回值和参数都不一样,那这两个方法代表了什么呢?1方法可以很明显看到参数的名字叫做index即索引,而返回值是个E泛型【我后面会说不打紧】,那么就可以很容易退出来这肯定是删除elementData即保存数据的数组指定下标index处的数据,而2方法呢,传入的是一个对象o,返回一个boolean,那么我们是不是可以推测出是这个方法是找出elementData里的和这个参数对象相同的对象然后删除呢,返回的值是是否成功对吧?Ok,我们来详细看看具体的实现是不是和我们推测的一样呢?
public E remove(int index) {
rangeCheck(index);//这里是范围检测,也就是看看你是不是删了不存在的索引,比如超出当前数组的长度size的,看看它的代码:
if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
modCount++;//同样把修改次数增加
E oldValue = elementData(index);//把要删除的索引位置处的值拿过来
int numMoved = size - index - 1;//计算需要移动的次数,那么这里为何需要移动呢,来看下面的图:
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);//如果需要移动,那么就把需要移动的数据统统往前复制过去
elementData[--size] = null; // clear to let GC do its work
//看上面的英文,就是取消对最后一个对象的引用,能够让垃圾回收器回收这个对象
return oldValue;//最后返回删除的值
}
下面是remove的方法:
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
//如果要删除一个null值,那么就循环这个数组找到第一个为null的值删除后返回true,来看看fastRemove是啥:
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
可以看到一个私有的方法,它用来快速删除和remove(int index)不同的是他不保存删除的元素,直接进行删除~
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
//下面这个就更简单啦,遍历数组用equal比较传入的对象和数组对象,然后找到后调用fastRemove(int index)删除即可
return false;
}
- Contains方法
最后来看看怎么去判断一个元素在不在elementData中呢:
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
来看看indexOf(Object o)方法怎么定位一个元素的:
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
//似曾相识的代码对吧,就在上面remove方法上看到过,只不过这里不返回true了返回找到的索引值
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}//同样这里也是
return -1;
}
总结,ok以上就是ArrayList的源码,是不是很简单,就是里面包了一个数组,底层通过对数组的扩容,复制,删除来构建了这样一个可扩容的列表,这是不是一步一步的进化过来,首先有了数组才有了ArrayList,并且有几个常见的面食点在于:
- 初始容量:10
- 扩容大小:在原有基础上增加一半
- 最大数组大小:最大整形值-8
Ok,我就不带大家去关联自己的知识树了,经过前面几节,相比大家都掌握了知识网络的记忆方式,希望大家好好的静下心来看完这些源码讲解,然后闭上眼睛好好思考是否掌握了,知识在于积累,心急学不到东西的~