当前位置: 首页 > 后端技术 > Java

合集——ArrayList源码分析

时间:2023-04-01 22:36:04 Java

基于JDK1.7的分析我们先来看数组。数组在内存中划分出一块连续的地址空间,用于存放元素。它直接操作内存,所以数组的性能需要比集合类稍微好一点,这是使用数组的一大优势。但是数组也有缺点,就是在初始化的时候必须指定数组的大小,在后续的操作中不能改变数组的大小。在实际情况中,我们经常会遇到一开始并不知道要存储多少元素,而是希望容器能够自动扩充自己的容量,从而能够存储更多的元素。ArrayList可以很好的满足这样的需求,它可以自动扩展大小以适应存储元素的不断增加。它的底层是基于数组实现的,因此具有数组的一些特点,如查找修改快,插入删除慢。我们来看看它是如何封装数组的。先看它的成员变量和三个主要的构造函数。//默认初始化容量privatestaticfinalintDEFAULT_CAPACITY=10;//空对象数组privatestaticfinalObject[]EMPTY_ELEMENTDATA={};//对象数组privatetransientObject[]elementData;//集合元素个数privateintsize;//构造传入初始容量的方法publicArrayList(intinitialCapacity){super();if(initialCapacity<0){thrownewIllegalArgumentException("非法容量:"+initialCapacity);}//创建指定容量的Object类型数组this.elementData=newObject[initialCapacity];}//无参构造函数publicArrayList(){super();//将一个空数组实例传递给elementDatathis.elementData=EMPTY_ELEMENTDATA;}//引入外部集合的构造方法publicArrayList(Collectionc){//持有传入集合内部数组的引用elementData=c.toArray();//更新集合元素个数和大小size=elementData.length;//判断引用的数组类型,将引用转换为Object数组引用if(elementData.getClass()!=Object[].class){elementData=Arrays.copyOf(elementData,size,Object[].class);可以看到ArrayList内部的存储结构是一个Object类型的数组,所以它可以存储任意类型的元素。构造ArrayList时,如果传入初始大小,则会创建一个新的指定容量的Object数组。如果没有设置初始大小,则不会分配内存空间,而是使用一个空的对象数组。实际放元素的时候再分配内存。我们来看看它的增删改查方法。//增加(添加)publicbooleanadd(Ee){//添加前检查数组是否需要扩容,此时数组的最小长度为size+1ensureCapacityInternal(size+1);//向数组末尾添加元素elementData[size++]=e;returntrue;}//增加(插入)publicvoidadd(intindex,Eelement){//插入位置范围检查rangeCheckForAdd(index);//检查是否需要扩展ensureCapacityInternal(size+1);//将元素移到插入位置后面System.arraycopy(elementData,index,elementData,index+1,size-index);//在要插入的位置赋新值elementData[index]=element;size++;}//删除publicEremove(intindex){//index不能大于sizerangeCheck(index);模数++;EoldValue=elementData(index);intnumMoved=大小-索引-1;if(numMoved>0){//将index后面的元素向前移动一位System.arraycopy(elementData,index+1,elementData,index,numMoved);}//引用nullelementData[--size]=null;returnoldValue;}//改为publicEset(intindex,Eelement){//index不能大于sizerangeCheck(index);EoldValue=elementData(index);//替换为新元素elementData[index]=element;返回oldValue;}//校验publicEget(intindex){//index不能大于sizerangeCheck(index);//返回指定位置的元素returnelementData(index);}每添加一个元素到集合中,都会先检查容量是否足够?否则,扩大容量。我们来看看增删改查的操作特性。添加(add):就是把这个元素加到最后。操作速度很快。增量(插入):由于需要移动插入位置后面的元素,涉及到数组的复制,所以操作比较慢。删除:由于需要将删除位置之后的元素往前移,还涉及复制数组,所以操作比较慢。修改:直接修改指定位置的元素,不需要移动元素,也不需要复制数组,操作速度快。Check:直接返回指定下标的数组元素,运算速度快。因为查找和修改直接定位到数组下标,不涉及元素移动和数组复制,所以速度更快。添加(插入)删除需要移动元素,涉及到数组复制,操作速度慢,而且每次添加操作还可能扩充数组,也会影响性能。我们来看看ArrayList是如何动态扩容的。privatevoidensureCapacityInternal(intminCapacity){//如果此时数组还是空的if(elementData==EMPTY_ELEMENTDATA){//与默认容量相比,取较大的值minCapacity=Math.max(DEFAULT_CAPACITY,minCapacity);}//数组初始化后执行这一步ensureExplicitCapacity(minCapacity);}privatevoidensureExplicitCapacity(intminCapacity){modCount++;//如果最小容量大于数组长度,则扩展数组if(minCapacity-elementData.length>0){grow(minCapacity);}}//集合的最大容量privatestaticfinalintMAX_ARRAY_SIZE=Integer.MAX_VALUE-8;//增加数组的长度privatevoidgrow(intminCapacity){//获取数组的原始容量intoldCapacity=元素数据.length;//新的数组容量增加一半intnewCapacity=oldCapacity+(oldCapacity>>1);//检查新容量是否小于最小容量if(newCapacity-minCapacity<0){newCapacity=minCapacity;}//检查新容量是否超过最大数组容量if(newCapacity-MAX_ARRAY_SIZE>0){newCapacity=hugeCapacity(minCapacity);}//复制原数组到新数组elementData=Arrays.copyOf(elementData,newCapacity);}在添加每个元素之前,会调用ensureCapacityInternal方法来检查集合容量。在这个方法内部,会检查当前集合的内部数组是否还是空数组,如果是,则新建一个默认大小为10的Object数组,如果不是,则证明当前集合已经初始化,然后调用ensureExplicitCapacity方法检查当前数组的容量是否满足最小需求容量,如果不满足则调用grow方法扩容。在grow方法内部可以看到,每次展开都会将原数组的长度增加一半。扩容实际上是创建一个容量更大的数组,将原数组的所有元素复制到新数组中,然后丢弃原数组。数组改为使用新数组。总结:1.可以自动扩容以适应存储元素的不断增加。每次向集合中添加元素时,都会先检查容量是否足够,否则会扩容。每次扩展都会将原始数组长度增加一半。实际扩展上面是创建一个容量更大的数组,将原数组的所有元素复制到新数组中,然后丢弃原数组,使用新数组。(添加)、查找、修改速度快,而添加(插入)、删除速度慢。3、内部存储结构是一个Object类型的数组,可以存储任意类型的元素。4、ArrayList的所有方法都不是同步的,所以不是线程安全的。5、构造ArrayList时,如果传入初始大小,则会新建一个指定容量的Object数组。如果未设置初始大小,则不会分配内存空间,而是使用空对象数组。当真正要放置元素时,再分配内存。6.构造ArrayList时,尽量指定容量,减少扩容时的数组拷贝操作。如果不知道大小,可以分配默认容量10。7.每次对下标的操作都会进行安全检查,如果数组越界,立即抛出异常。