周末回家写的ArrayList源码分析
一,ArrayList源码分析
再次看ArrayList的源码,一会儿就看完了,真快呀,这也就是自己所感慨的慢就是快吗?本次源码分析自己还是按照以往的风格来分析。
按照单点突破,逐个方法分析了,分析的思路还是和以往一样,不过这是我个人的风格。
如果你有自己的源码分析风格,可以按照自己的思路去做,这里如果可以帮助到你就再好不过了,下面自己就按照自己的思路和文章写法慢慢分析了。
二,方法分析
2.0,左右可滑动查看文字注释
2.1,构造函数
public ArrayList() {
//这里主要是构造了一个空的数组,这个常量就是{}
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
这里简单的说下,构造函数很重要,涉及的内容也很多,比如说,初始化一个对象要经历一个什么样的过程? 造轮子的时候,我们可以在构造函数里面做点什么呢? 都侧面说明了构造函数的特性,需要好好理解一下。
2.2,add()方法
public boolean add(E e) {
//进行扩容操作,看下第二步的内容
ensureCapacityInternal(size + 1);
//将元素e增加到数组的末尾位置
elementData[size++] = e;
//默认就是添加元素成功了
return true;
}
//第二步操作
private void ensureCapacityInternal(int minCapacity) {
//这一步的判断是有可能是第一次添加元素
//此时我们new ArrayList()操作后
//此时的成员变量elementData正好等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//由于默认初始容量是10,所以取两者之间的最大值,这里发现工具类已经体现出来了
//简化了代码的同时,增加了可读性
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//看第三步操作
ensureExplicitCapacity(minCapacity);
}
//第三步操作
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
//判断minCapacity是否大于初始的元素空间了
// 若大于,则需要扩容
if (minCapacity - elementData.length > 0)
//继续看如何扩容的操作吗?
grow(minCapacity);
}
//第四步操作
private void grow(int minCapacity) {
//获取原有的数组空间大小
int oldCapacity = elementData.length;
//容量在原有空间的基础上在增加1/2容量
//如oldCapacity=10,此时的newCapacity=10+5=15
int newCapacity = oldCapacity + (oldCapacity >> 1);
//拿增加后的容量和minCapacity进行对比,获取最大的容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//数组空间扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
//第五步操作,很简单,这里就不分析了
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0)
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
经过上面的层层分析之后,我想你已经明白了,添加一个元素到集合里面,要做哪些操作了,由于数组的空间默认是有限的,即分配了就不可以改变了,所以需要在程序里面动态去判断,这里或许你已经明白为什么这么做了,那么我们继续往后分析咯。
2.3,size()方法
一般,我自己分析源码的时候,都是先从add()方法分析之后,就会一连串的分析一下size()方法和isEmpty()方法,因为这两个方法简单,且比较容易理解和记忆。
//此时的成员变量size就是表示集合元素的个数的,获取集合元素个数直接返回size即可
public int size() {
return size;
}
2.4,isEmpty()方法
//若集合里面没有元素,此时的成员变量size就等于0,默认size就是0
public boolean isEmpty() {
return size == 0;
}
2.5,contains()方法
public boolean contains(Object o) {
//判断集合里面是否含有元素o,看第二步操作
return indexOf(o) >= 0;
}
//第二步操作
public int indexOf(Object o) {
if (o == null) {
//循环遍历集合的元素,一一比对是否存在集合的元素是否等于元素0,
//若相等则返回对应元素的下标
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
由于集合里面是既可以装非null元素,也可以装填null元素的,所以这里要区分查询,时间复杂度为O(n),这里是重点,需要重点理解一下,回过头来,看看是不是和你开发的应用编写逻辑是否很像是吧。
2.6,indexOf()方法
public int indexOf(Object o) { if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
这个方法,上面已经分析过了,这里就不分析了,避免重复性文字说明,这也是代码复用的好处,显而易见的,这个方法是为了获取元素的索引下标的。
2.7,lastIndexOf()方法
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
这个方法就是从后向前查找元素出现在集合的下标位置,找到就立刻返回了,此时的时间复杂度为O(n),n就是问题大小的规模
2.8,get()方法
public E get(int index) {
//预检查机制
rangeCheck(index);
return elementData(index);
}
//第二步操作
private void rangeCheck(int index) {
//index不可能大于等于size大小的,因为索引下标是从0开始的
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//构建一个异常提示信息
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
这个方法就是为了获取指定索引下标的元素的,首先要先检查索引位置是否合法,不符合时,直接抛出索引非法异常的问题,由于数组可以基于索引下标快速找到元素,获取元素的时间复杂度为o(1)了
2.9,remove()方法
public E remove(int index) {
//先检查索引位置是否合法
rangeCheck(index);
modCount++;
//获取待删除的索引位置的元素
E oldValue = elementData(index);
//获取需要移动元素的个数
int numMoved = size - index - 1;
//数组元素的移动操作,这里涉及到c语言的底层实现,因为调用了native关键字修饰的方法
//所以在代码层面就不过多分析,就是数组元素的移动操作
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//这个注释已经很好的说明了,为啥要将删除后的元素置为null,
//不是很了解的可以看下java虚拟机相关的内容哈
//在之前的源码分析过程中,已经多次提到了将元素置为null的原因了
//这里自己就不再解释了
//--size 集合的元素个数减一
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
2.10,clear()方法
public void clear() {
modCount++;
//循环将集合的元素置为null,等待gc机制在某个时刻触发,将不可达对象进行回收处理
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
三,源码分析总结
3.1,总结一下
关于ArrayList集合源码的常用方法,自己到这里就分析完了,或许你有发现我基本上都是分析大部分的方法,对于那些很少用到的方法,自己不会过多分析,知道有这么个方法就可以,比如说取交集的retainAll(),循环遍历元素的foreach()方法,我觉得还是以实用为主,不太可能每个方法都要去分析的。
此时这里给个个人的小建议吧,不会或者不了解java8操作的可以去了解下,因为太常用了,我这里也不是说会有"教会徒弟,饿死师傅"的心态,我一直觉得,互联网下,不会像传统手艺人那样存在这样的一种现象,如果别人不告诉你,你也可以通过自己的搜索,慢慢学到一点东西的,不过是时间问题而已,所以你应该感谢那些无私分享自己心得,避免你多走弯路的前辈们,因为他们,所以你少了很多困难,这也是互联网所倡导的开源的好处,所以,我现在分享自己的一点内容,不是说我多好,而是我曾经也受惠了哪些帮助过的人,这里觉得自己也想帮助自己的同时,帮助到别人是在好不过的一件事情了,所以这里多说了一点,喜欢我的内容,不妨转发分享一下吧。