ArrayList基本是我们在开发过程中最常用的一个类了。
大家基本常用的方法有add(),addAll(),get(),remove(),size()或者有的还会用到iterator()。
那么这些方法都是怎么实现的呢?内部原理是什么,其实我也一直没有仔细想过,一直
就是拿来就用。那么现在我们来深入研究一下ArrayList的实现原理吧。
我们先看在开发工具中ctr + 左键 进入ArrayList源码
首先:我们看到ArrayList属于java.util包下,java.util包提供了一系列工具类

然后可以看到ArrayList继承自AbstractList,并实现了Cloneable,Serializable,RandomAccess接口,而且ArrayList本身是支持泛型。

然后我们继续进入AbstractList中。
可以看到AbstractList继承自AbstractCollection,并实现了List接口。

我们先看AbstractCollection。
可以看到它实现了Collection接口。
然后回头看,AbstractList在继承AbstractCollection抽象类的同时还实现了List接口,而我们平常使用ArrayList的时候,声明的又是List。
那我们就来看一看List接口是什么。

惊奇的发现,List原来继承自与AbstractCollection的同一个接口Collection(java中接口可以继承接口,也可以继承多个接口,class不能继承接口,不能继承多个类,但是可以实现多个接口)。
看到这里,会有些奇怪。AbstractList为什么继承自一个实现了Collection接口的抽象类,然后又要实现一个继承了Collection接口的接口呢?是不是很绕口,哈哈,我自己都要晕了。
在这里就先卖个关子,留着这个疑问跟我们继续往下看吧。
从前面看来,Collection接口应该最顶层的了。那我们就来看Colection接口。

纳尼!
为什么还没有到顶层呢!?竟然还有个父接口!
没办法,我们既然都走到了这一步,只能硬着头皮在走向去了,就看看到底何时走到头!

咦!看来十万里长征终于走到头了,这个接口没有继承自任何接口,而且还只有一个方法。
》》》》
Iterator iterrator();
这个方法是不是很熟悉?迭代器啊!我们用这个方法来迭代ArrayList中的元素。
list list = new ArrayList();
l.add("aa");
l.add("bb");
l.add("cc");
for (Iterator iter = l.iterator(); iter.hasNext();) {
String str = (String)iter.next();
System.out.println(str);
}
/*迭代器用于while循环
Iterator iter = l.iterator();
while(iter.hasNext()){
String str = (String) iter.next();
System.out.println(str);
}
可以看到,都是通过iterator方法返回了一个Iterator对象。
我们来看看Iterator对象到底是个什么?
好家伙,又是个接口,对于java中为什么这么多接口呢,这个留待我们以后再说啦。
我们这里看到Iterator接口中的3个方法,hasNext(),next(),remove()方法。我们在前面看到的代码中看到调用ArrayList的iterator()方法返回的Iterator对象是可以调用这几个方法的。
那么ArrayList调用的iterator()方法,和Iterator接口的方法具体是怎么实现的呢?
2.ArrayList深度剖析
1.Collection
前面说到了Collection接口是继承自Iterable接口。那么我们看下Collection的具体实现。
我们可以看到第一个红框中圆形的图标中间一个I。这表示Collection是一个接口。
然后在第二个红框中看到了我们在Iterable接口中看到的iterator()方法。因为Collection也是一个接口,并继承自Iterable,所有,继承的方法只能也是接口方法。
然后我们可以看看Collection接口中的方法,是不是有很多都是我们使用ArrayList的时候常用的。
比如: add()、addAll()、clear()、remove()等。基本也可以说明,我们ArrayList所使用的方法都是实现自Collection的了。
2.AbstractCollection
然后我们接着往下看,看实现了Collection接口的类AbstractCollection的具体实现。

1.还是看左上角第一个红框中的小原标。里面是个C,表示这是一个类(class),然后小原标右上角有一个大写字母A。表示这个类是一个abstract方法。
2.我们主要关注下add()、iterator()、size()方法的实现。
咦,add方法直接抛出一个UnsupportedOperationException异常。看到这里一下子懵逼了。那这样我们每次调用arraylist的的时候,不就都会抛出异常了吗?但是我们每次调用又都是好使的。好吧,这里再卖一个关子,你可以记录一下我们卖的关子,到后面的point自然会有答案的噢。
然后看一下iterator()方法。
还是没有实现,然后变成一个abstract方法了,这也说明了在AbstractCollection的子类,如果不是抽象类,那就必须的实现这个方法了。
最后看一下size()方法。
好吧,还是抽象方法。
基于篇幅原因,我们不能每个接口都介绍到,只能大概介绍一些常用的。如果有兴趣可以按照我的分析思路,选择自己感兴趣的去研究。
3.AbstractList
我们继续,前面讲了,AbstractList不仅继承了AbstractCollecttion抽象类,还同时实现了List接口,而且List接口的还是AbstractList抽象类实现接口Collection接口的子接口。
还记得我们在前面留的第一个疑问吗?
为什么AbstractList为什么要实现List接口呢?
我们可以看看List接口。
是不是比它的父接口Collection多了很多方法?而且很多方法都是我们在ArraryList集合中常用的。
比如:
add(int,E),指定插入元素的位置。
get(int),获取指定位置的元素。
remove(int),删除指定位置的元素。
等等。。
到这里,有种豁然开朗的感觉,为什么AbstractList在继承了AbstractCollection抽象类之后,还要实现List接口呢?
恩,因为Collection只是最基础的集合接口。List接口继承Collection接口的同时,又扩展了方法,形成我们常用的List接口,然后List接口也就是我们平常用的List集合类框架的顶层接口。
所以在我们平常使用时,基于多态、动态版定、里氏代换等原理原则。在声明变量时,声明的是List,然后实例化ArrayList。
接下来我们来看AbstractList的具体实现,然后重点关注一下List接口的方法实现。
对于List接口的实现,我们就暂时先只关注:add(int,E),get(int),remove(int)这三个方法和add(E)这个List接口和AbstractCollection都有的方法。
1.add(int,E)
可以看到,方法里面又是直接抛出了一个UnsupprtedOperationException异常。又?我为什么要说又?
原来是前面在看AbstractCollection抽象类的add(E)方法的时候,这个方法也是直接抛出了这个异常。在这里,我还是不能告诉你为什么,不要打我,带着疑问继续往下看吧。嘿嘿。
2.get(int)
好嘛,又是一个抽象方法,说明在非抽象类的子类中必须要实现此方法,从前面我们对ArrayList追根溯源的时候可以看到,AbstractList的子类就是ArrayList了,所以ArrayList中,一定存在了很多不可告人的秘密!
3.remove(int)
看到这里,请让我去哭一会儿。。 我就什么都不说了,只能央求你继续带着悬念看下去了。
4.add(E)
先看注释,表示这个方法是添加个指定对象到集合的尾部。
然后我们看方法内部,调用了List接口的方法add(int,E)。
在前面,我们看到了add(int,E)方法是直接抛出了异常,看到这里,悬念更深了,为什么都是直接抛出异常呢?我们平常调用也是没有问题呀。恩,悬念即将揭晓,请看下一部分。
4.ArrayList
不容易啊,终于走到了我们的目标重点ArrayList。
带着前面的疑问,我们要看看add(E),add(int,E),get(int),remove(int)方法。
1.add(E)
我们可以看到,在方法内部声明了一个局部Object数组变量,然后将一个全局的array变量赋值给它,我们看看array变量是在哪里声明的。
我们看到,是在代码第62行声明的一个用transient关键字修饰的Object数组。
继续看方法的代码,声明了一个int 变量s,并将size赋值给它,由字面来看,size应该是我们list的长度了,口说无凭,继续看代码。

果然size是list的长度,接着往下看,判断当前记录的集合的size是不是等于array数组对象的length实际长度。
然后我们会看到下面这段代码,
当记录的size等于数组对象a的实际length,新建一个数组对象newArray。
然后指定newArray数组的长度,长度是由一个3元运算符计算出来的。
s + (s < MIN_CAPACITY_INCREMENT) / 2 ? MIN_CAPACIT_INCREMENT : s >> 1)。
其中,MIN_CAPACITY_INCREAMENT是ArrayList每次容量增加的时候,最小的增加单位。

所以,这里的增加容量的具体含义是:
每次需要增加容量的时候,如果当前数组记录的长度小于MIN_CAPACITY_INCREAMENT的一半(也就是6),那么新的对象数组长度为原本数组的长度s加上MIN_CAPACITY_INCREMENT的值12,整个新对象数组的长度为原先长度的1.5倍。
如果当前数组记录的长度s大于MIN_CAPACITY_INCREMENT的一半,则扩容后的数组长度为原本记录数组长度s加上s右移一位的值,而每当一个数的值右移一位,右移之后的值就为原先值的一半。也就是s + s/2 = 1.5s,新对象数组的长度也是原对象长度的1.5倍。
可以得出结论,每次只要是数组需要扩容,扩容的容量都是原容量的1.5倍。这样既不会因为容量扩容的太小导致频繁扩容,频繁进行后一步的数组的复制操作,也不会因为扩容的太多,导致有很多的空闲内存被占用。
然后调用System.arraycopy()方法,将旧的数组的全部数据复制到newArray数组中去并将新的数组对象newArray的引用指向全局array和局部数组对象a。
最后,将要被添加的参数object添加进数组中的最后一位,然后将size的值加一,最后将modCount值加一。
但是在ArrayList中并没有找到modCount的值,ctrl 加 左键点击modCount,发现是ArrayList的父类AbastractList中声明的。
注释表示,这是记录list修改次数的一个变量。至于为什么要记录,暂时先将它放到一边,暂时不考虑(其实是我还没有注意到。。。)。
2.add(int,E)
然后我们来看add(int,E)方法。
从注释可以看到,第一个参数 index是要被插入的对象的索引位置,也就是这个对象要插入到数组中的位置。
首先,还是在方法内部声明了一个数组对象 a,并将全局的数组对象array的引用指向a。
然后将全局的数组长度size赋值给局部int变量s。
判断要插入的位置是否超出了当前数组的长度,或者要插入的位置是否小于0(最小的位置就是0,不能为负数)。
然后判断当前记录的数组长度s是否小于当前数组的实际长度,如果小于,则表示当前记录的数组的长度可能是有错误的。
那么执行一次数组拷贝,将数组a从index开始的内容,拷贝到a从index + 1开始的内容,拷贝长度为 s - index。
看到这里是不是有点蒙?
我们画个图来捋一捋思路。
可以看到,这行代码是数组a复制内容到数组a自己。
我们假设数组当前有5条记录5,3,1,4,2。然后size记录的数组的长度却是4。当前插入数据的索引值index为2。
从图中可以看到,我们是把从index开始的a[2],a[3]复制到 index+1的 a[3],a[4]处。
最后,数组的实际值应该为{5,3,1,1,4}。
看到这里是不是有点疑问,多了一个1,然后少了1个2,我们这么想,当数组a的实际长度为5的时候,当前缓存的长度却比实际的要小。说明缓存记录是有问题,然后此时又要进行一次add操作,那么为了不影响缓存记录和实际数组长度的冲突,只能将当前数组的最后一位放弃。然后中间圆圈中的多出来的一个1,刚好为后面要执行的add操作留出一个位置。
从代码可以看到,当执行完arraycopy方法后,是直接设置a[index]=object。是直接指定了数组下标的index索引进行赋值,而不是前面add(int)方法中赋值的是数组实际长度的索引值(索引值总是比实际长度小一位,所以设置数组实际长度的值,也就是添加一个新值)。然后再给s加一位,现在s就是5了,就和数组a的实际长度一致了。按照这个思路,只要当前缓存的size小于实际数组的长度,每次都会将数组的最后一位给抛弃,然后当新添加一个值后将size加一,来不断地追加size的值直到size等于数组的实际长度。
3.get(int)方法
从代码可以看到,先判断index的值是否大于等于当前数组记录的size,如果大于等于,那么就抛出一个数组越界的异常。
如果index正常,则从数组中获取下标为index的结果返回。
从我们看的这几个常用方法,可以知道ArrayList的内部其实是由一个数组来控制的,我们的增加、删除都是操作的数组,所以才叫Array的List。