Java中的ArrayList底层源码分析
作者:爱喝咖啡的程序员
一. 基本原理和优缺点
优点:
1.通过下标读取元素的速度很快,这是因为ArrayList底层基于数组实现,可以根据下标快速的找到内存地址,接着读取内存地址中存放的数据。
2.随机读的性能很高,仍然是因为ArrayList底层基于数组实现。(随机读: 一会儿list.get(2),一会儿list.get(10))
缺点:
1.数组的长度是固定的,如果ArrayList的初始长度太小,后续又不断的向list写入数据,会导致数组频繁的扩容和复制数据,而这些过程非常影响系统的运行。
2.由于是数组实现的,如果想往中间插入一个元素,会导致这个元素后面所有的元素都要往后挪动一个位置。
二. 源码分析
1.1 默认的构造函数
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
如果使用默认的构造函数,初始化时是一个空数组,数组的类型是Object[ ],数组的长度为0。
当执行add操作后,才会为ArrayList进行一次扩容,这里使用的是ArrayList的初始长度,10。
1.2 add(E e)
list.add("张三"); list.add("李四");
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
每次执行ArrayList的add()方法,先会判断一下,看看当前数组是否已满,能不能放得下本次待加进去的元素,如果数组已经被塞满了,那就进行扩容,创建一个新数组,把老数组中的数据拷贝到新数组中,确保新数组能装得下足够多的元素。
刚刚我们说了,使用ArrayList的无参构造函数,初始化时数组的长度为0。
list.add(“张三”);
elementData[size++]=e;此时会把元素插入到下标为0的位置,接着把size设置成0+1=1。(显然,size就是ArrayList实际存储元素的个数)
list.add(“李四”)l;
elementData[size++]=e;此时会把"李四"插入到下标为1的位置,接着把size设置成1+1=2。
1.3 add(int index, E element)
list.add("张三"); list.add("李四"); list.add("王五"); list.add(1, "赵六");
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(index); 检查说,待插入的元素下标不能大于当前元素的个数,也不能小于0。
问题: 不能小于0倒是好理解,为什么待插入的元素下标可以等于当前元素的个数呢?
答: 若相等,则代表插入这个元素,正好不需要挪动旧数组中任何的元素,不会造成任何的坏影响。但是当待插入元素的下标大于当前元素,会导致数组跳跃式的插入元素,这对于数组来说,是绝对不允许的。
就拿当前的例子来说,[“张三”, “李四”, “王五”],现在想要执行list.add(4, “赵六”);如果真的让你得逞了,岂不是会出现 [“张三”, “李四”, “王五”, , “赵六”] 导致数组中存放的数据不连续的恐怖后果?
private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
接着,执行ensureCapacityInternal(size + 1),数组扩容,确保数组有足够的空间能容纳待插入的元素,别搞得插入元素后,把数组撑爆了。
然后,执行System.arraycopy(elementData, index, elementData, index + 1,size - index); 挪动旧元素,为待插入的元素腾位置。案例中,System.arraycopy(elementData, 1, elementData, 2,2); 就是从elementData的第二个元素开始,拷贝2个元素到elementData的第三个元素的位置上。
我们可以看看elementData,原本[“张三”, “李四”, “王五”] ,现在是[“张三”, “李四”, “李四”, “王五”]
最后,elementData[index] = element; 把赵六插入到index=1的位置上,现在是[“张三”, “赵六”, “李四”, “王五”]
1.4 ensureCapacityInternal
这个方法的作用是扩容,它可以说是ArrayList中最为核心的代码了,所以单独拿出来说一下。
private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); }
执行add操作时,总是会先执行ensureCapacityInternal(),计算出加上了本次待新增的元素后,总共需要多大的空间来存储。
假设初始化数组的大小是10,陆陆续续的已经有10个元素填入了数组,此时想要加入第11个元素。
首先,执行calculateCapacity(elementData, minCapacity); 这个方法主要是用来初始化空数组,有些时候,我们使用new ArrayList()初始化了数组,没有指定数组的初始大小,那么当往数组中插入元素时,ArrayList就会为我们初始化数组,默认的长度是10,如果你一次性加的元素太多(比如你使用的是addAll( ) ),则按照加入的元素个数来定。
private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; }
接着执行ensureExplicitCapacity(),modCount指的是数组被修改的次数,如果数组没有足够的空间放下新增的元素,就会触发扩容。
这个很好理解,比如你的数组长度是10,已经放满了,现在想要插入第11个元素,肯定是插不进去的,所以自然就要扩容。
private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
那么怎么扩容呢?很简单,增加原有数组容量的一半。比如原来的数组长度为10,扩容一次后,数组长度=10 + 10/2 =15。
如果原数组经过1.5倍的扩容后,仍然放不下待插入的元素,怎么办?那么新数组的长度就以添加新元素后的最小长度为准。
比如说,原来的数组长度为10,默认情况下,扩容一次长度就是15,现在我一次想要插入100个元素(通过addAll()方法),此时数组肯定是装不下,这个时候,新数组的长度就等于10+100=110了。
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); }
最后一步,从当前数组中最后一个元素的后一个位置开始,加入新的元素。
elementData[size++] = e;
1.5 set(int index, E element)
list.add("张三"); list.add("李四"); list.add("王五"); list.set(1, "赵六");
public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; }
首先,执行rangeCheck(index),这一步就是在检查,你想替换的下标是否存在元素。从1.2节我们知道了,size代表着ArrayList内实际存放的元素个数,别忘了ArrayList的底层是数组实现的,所以不可能跳跃的存储元素,因此,下标从0到size-1,一定会有元素。如果你现在想要替换的下标大于等于size,那么在ArrayList中,连这个元素都不存在,那你还怎么替换元素?替换空气吗?
private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
接着,执行E oldValue = elementData(index); 看看,这里直接把要替换的元素给取出来了。在上述案例中,list.set(1, “赵六”)会把下标为1的元素给取出来,也就是李四,所以oldValue=李四。
然后,执行elementData[index] = element; 在我们的案例中,也就是elementData[1] = “赵六”,把下标为1的位置上的元素替换成了"赵六"。
最后,返回被替换的旧数据,也就是李四。
1.6 get(E element)
list.get(1);
public E get(int index) { rangeCheck(index); return elementData(index); }
E elementData(int index) { return (E) elementData[index]; }
就是这么简单,根据下标,直接从数组中获取数据。没什么好说的,这个方法的性能是ArrayList所有方法中最高的。
1.7 remove(int index)
list.remove(1); list.remove(2);
public E remove(int index) { rangeCheck(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; }
执行rangeCheck(index),看看你想删除的元素的下标是否存在,有没有越界。比如数组中只有3个元素,你偏要删第10个元素,那ArrayList上哪去帮你删?
执行int numMoved = size - index - 1; 所谓删除元素,说白了就是把待删除元素后面的元素,全部前进一格,相当于是add(index, E)的逆向操作。
问题: 不就是挪动待删除元素右边的元素么,干脆写成int numMoved = size + 1;这样可以不?
答: 不可以。numMoved 代表的是需要移动的元素的个数。
执行elementData[–size] = null; 现在已经把元素往前挪了一格,比如旧数组为[“张三”, “李四”, “王五”, “赵六”],现在想删除index=1的元素,当执行完System.arraycopy,也就是移动(实际上是复制)元素的过程后,数组为 [“张三”, “王五”, “赵六”, “赵六”],最后,我们只需要删除末尾元素即可。
所谓的删除,就是置为null,由于之前的对象没有了引用,接着让JVM来回收即可。
三. 结论
1.在对ArrayList做随机位置的插入和删除操作时,会涉及到数组大量元素的移动(其实就是拷贝和删除),所以性能都不高。(具体可以参考add(int index, E element) 和 remove(int index)这两个方法)
2.执行add或者add(int index, E element)时,如果插入的比较频繁,偶尔会由于旧数组容量不够,导致扩容,扩容就会导致创建新数组,复制数据,所以性能不高。
3.set()或者get(),这两个方法都是靠数组下标来定位待操作的元素,接着替换或者读取元素,这个性能还是不错的。
4.一旦经历了扩容后,就算后期删除了元素,ArrayList也不会主动缩容。
到此这篇关于Java中的ArrayList底层源码分析的文章就介绍到这了,更多相关ArrayList底层源码内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!