上面有个问题是为什么需要声明两个空数组。我们在看到上面源码的时候有一个方法为calculateCapacity,这个方法内部逻辑只有在通过无参构造初始化ArrayList的时候才会改变将要返回的minCapacity。而返回的这个值将会决定下面的数组是否需要扩容。如果我们通过指定大小的方式初始化ArrayList并指定大小为0,这说明我们需要的就是一个空的ArrayList,不需要去扩容,你细品;
新增时,没有对值进行校验,所以新增值可以为null,且没有做重复值判断,所以元素可以重复;
ArrayList中的数组的最大值是Integer.MAX_VALUE,超过这个值,JVM就不会给数组分配
内存空间了;
扩容是原来容量大小+容量大小的一半,简单说就是扩容后的大小是原来容量的1.5倍。
扩容完成之后,就是简单的赋值了,赋值时并没有加锁,所以是线程不安全的。
扩容的本质在grow方法的最后,扩容是通过Arrays.copyOf(elementData,newCapacity);这行代码实现的。这个方法实际上调用的方法是我们经常使用的System.arraycopy:
/** *@param src 被拷贝的数组 *@param srcPos 从数组那里开始 *@param dest 目标数组 *@param destPos从目标数组那个索引位置开始拷贝 *@param length 拷贝的长度 *此方法是没有返回值的,通过dest的引用进行传值 */ public static native void arraycopy(Object src, int srcPos,Object dest, int destPos,int length);这个方法是一个native方法,虽然不能看到方法内部的具体实现,但通过参数也可以管中窥豹。这个方法会移动元素。所以说数组如果要扩容,需要重新分配一块更大的空间,再把数据全部复制过去,时间复杂度 O(N);而且你如果想在数组中间进行插入和删除,每次必须搬移后面的所有数据以保持连续,时间复杂度 O(N)。由于数组又是一块连续的内存空间,能够根据索引快速访问元素。
上面也就解释了一开始那个问题:ArrayList为什么插入慢,查询快。
ArrayList有多种删除方法,这里以根据值删除的方式进行说明(其他原理类似):
public boolean remove(Object o) { //如果要删除的值是null,删除第一个是null的值 if(o==null){ for(int index=0;index<size;index++) if(elementData[index]==null){ fastRemove(index) return true; } }else{ //如果要删除的值不为null,找到第一个和要删除的值相等的删除 for(int index=0;index<size;index++) //这里是根据 equals来判断值相等的,相等后再根据索引位置进行删除 //所以根据对象删除时,一般来说,如果你确定要删除的是某一类的业务对象,则需要重写equals if(o.equals(elementData[index]){ fastRemove(index) return true; } } return false }核心其实是fastRemove方法:
private void fastRemove(int index){ //记录数组的结构要发生变动了 nodCount++; //numMoved表示删除index位置的元素后,需要从index后移动多少个元素到前面去 //减1的原因,是因为size从1开始算起,index从0开始算起 int numMoved=size-index-1; if(numMoved>0) //从index+1位置开始被拷贝,拷贝的起始位置是index,长度是numMoved System.arraycopy(elementData, index+1, elementData, index, numMoved); //数组最后一个位置赋值null,帮助GC(没有引用则自动回收了) elementData[--size] = null; }从源码中,我们可以看出,某一个元素被删除后,为了维护数组结构,我们都会把数组后面的元素往前移动,同时释放最后一个引用,便于回收。
总结本文主要从ArrayList的源码入手,分别从初始化、新增、扩容、删除四个方面展开学习。我们发现ArrayList内部其实就是围绕了一个数组,在数组容量不足时将数组扩容至更大,所以也就自然被称作基于动态数组。
微信搜索Java成神之路或扫描下方二维码,发现更多Java有趣知识,让我们一起成长!