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

JDK成长笔记2:探究ArrayList常用方法源码(上)

时间:2023-04-01 19:32:42 Java

上一节,相信大家不仅会学到ArrayList构造函数的知识,还会学到先上下文的思想然后是细节,甚至是猜测。在接下来的几节中,您将学习ArrayList常用方法的源码原理。学完之后,你就不会再被ArrayList的面试题搞得手足无措,使用ArrayList得心应手。在今天的小节中,主要可以学习到以下几点:ArrayList扩展原理影响ArrayList性能的根本原因是什么?学习阅读源代码的新思路和新方法。开始吧!当ArrayList第一次调用add方法时会发生什么?首先需要修改上一节之前的Demo,如下:importjava.util.ArrayList;importjava.util.List;publicclassArrayListDemo{publicstaticvoidmain(String[]args){//默认大小为0ListhostList=newArrayList<>();hostList.add("host1");}}上面的代码假设你通过add方法在hostList中添加了一个host主机地址。当ArrayList第一次调用add方法时会发生什么?您可以点击查看以下代码列表:/***将指定的元素追加到该列表的末尾。**@parame元素附加到这个列表*@returntrue(由{@linkCollection#add}指定)*/publicbooleanadd(Ee){ensureCapacityInternal(size+1);//递增modCount!!元素数据[大小++]=e;返回真;多说几句,源码注释和方法名有时可以帮助你理解这个方法的大概功能。这是非常关键的一点。例如,注释表示add方法是将特定元素追加到列表末尾;方法名ensureCapacityInternal表示保证内部容量。看完你应该能猜到,这个方法大致和确认容量有关,可能用来判断容量是否可以添加元素。还需要注意的是,看方法的时候,不要着急直接从第一行开始看。在根据注释和方法名有了大致的了解之后,在开始之前还需要看一下整个方法的结构。比如调用什么其他的方法,有几个for循环或者if结构。就像这个add方法,主要有2个步骤。第一步是调用一个方法,应该是保证内部容量是否足够添加元素。第二步是数组基本赋值操作和size++。如下图所示:##ArrayList即使数组大小为0也能添加元素?那么当你知道了整个方法的来龙去脉之后,就可以再看细节了,先看第一步:ensureCapacityInternal()。这个方法的入参是size+1,size说的大家应该都见过,它是一个成员变量intsize。它的默认值为0,所以在size+1之后,这个方法的实参为1,那么形参minCapacity的值为1。(实参就是传入方法的实际值,形参为方法括号中使用的参数名称)。可以看到ensureCapacityInternal的代码清单如下:privatevoidensureCapacityInternal(intminCapacity){ensureExplicitCapacity(calculateCapacity(elementData,minCapacity));}那么可以看到这里调用了calculateCapacity方法和ensureExplicitCapacity方法。我们先去calculateCapacity,你可以记下它的实参是minCapacity=1,还记得elementData吗?它是ArrayList核心的成员变量Object[]数组。calculateCapacity的代码如下:privatestaticfinalObject[]DEFAULTCAPACITY_EMPTY_ELEMENTDATA={};transientObject[]elementData;privatestaticfinalintDEFAULT_CAPACITY=10;privatestaticintcalculateCapacity(Object[]elementData,intminCapacityTCAP){if(DElementDataDA{returnMath.max(DEFAULT_CAPACITY,minCapacity);}returnminCapacity;}这里可以看到两个熟悉的成员变量:elementDataandDEFAULTCAPACITY_EMPTY_ELEMENTDATA.我在ArrayList无参数构造函数中看到了。第一次调用add方法时,两者的引用地址和值应该是一样的,因为构造函数中有这么一句this.elementData=DEFAULTCAPACITY_EMPTY_ELEMENTDATA;所以这里输入第一个if条件,然后用DEFAULT_CAPACITY和minCapacity进行比较,可以看到DEFAULT_CAPACITY变量的值为10,也就是说Math.max运算后会返回10。至此,就可以完成之前画的图了,会变成如下:也就是说calculateCapacity返回后,返回ensureCapacityInternal方法,ensureExplicitCapacity的实参将为10,因为calculateCapacity(elementData,minCapacity)=10.privatevoidensureCapacityInternal(intminCapacity){ensureExplicitCapacity(calculateCapacity(elementData,minCapacity));}然后进入ensureExplicitCapacity方法,看到如下代码:privatevoidensureExplicitCapacity(intminCapacity){modCount++;//溢出意识代码if(minCapacity-elementData.length>0)grow(minCapacity);}这时候形参minCapacity也是刚才传入的10。从名字就可以看出这个方法。ensureExplicitCapacity表示保证准确的容量。让我们首先看一下该方法的上下文。第一行有一个modCount++,然后可以看到一行代码注释。溢出意识代码是指具有溢出意义的代码。之后还有一个if条件,如果满足就会进入grow方法。在这一点上,你甚至可以猜测。这里的grow方法是不是说容量不够会增长,也就是扩容?因为我们知道无参构造函数创建的ArrayList的大小默认是一个0的Object[]数组elementdata。而elementdata.length必须是0,是否可以添加元素呢?当然不是。那我们一行一行的来看。好像第一行的modCount不知道是什么。可以点开看看,是一个成员变量,还有很多注释。看来你没明白什么意思。所以在这里我想告诉大家另一种看源码的思路:抓大放小。比如modeCount貌似跟加操作没有关系,先跳过吧!(其实这个modCount就是并发操作ArrayList时fail-fast机制的实现,下节讲)当前执行代码如下图:WilltheArrayListexpandwhenadding元素第一次?继续往下看,很明显minCapacity=10,elementData的空数组长度必须为0。所以10-0>0会满足if条件,进入grow方法,其实参为10.其代码清单如下:privatevoidgrow(intminCapacity){//溢出意识代码intoldCapacity=elementData.length;intnewCapacity=oldCapacity+(oldCapacity>>1);如果(newCapacity-minCapacity<0)newCapacity=minCapacity;如果(newCapacity-MAX_ARRAY_SIZE>0)newCapacity=hugeCapacity(minCapacity);//minCapacity通常接近大小,所以这是一个胜利:elementData=Arrays.copyOf(elementData,newCapacity);}grow方法参数minCapacity值是10。进入grow方法后,从上下文来看,有两个if分支,有两个局部变量oldCapacity和newCapacity,还有一个步骤是通过Arrays.copyOf对elementData进行赋值操作方法。oldCapacity应该为0,因为我们知道elementData数组是空的。然后你会看到一句话:intnewCapacity=oldCapacity+(oldCapacity>>1);这句话是什么意思?事实上,oldCapacity被右移了1位。如果你还记得计算机基础知识,右移一位有点像除以2,底层只是二进制运算。举个例子:比如oldCapacity=10,如果先左移1就乘2,右移1就除2,如下图:然后intnewCapacity=oldCapacity+(oldCapacity>>1);其实这句话的意思就是在原来尺寸的基础上,尺寸增加了一半。但是由于oldCapacity=0,所以将size增加一半,newCapacity=0+0>>1=0+0=0。此时minCapacity=10。这样会进入第一个if条件,因为newCapacity-minCapacity<0,即0-10<0,然后进行一步赋值操作,newCapacity会变成和minCapacity一样,值为10。这里可以总结一下,也就是说,如果你在创建ArrayList的时候没有指定大小,那么第一次给ArrayList添加元素时,它会指定一个默认的容量为10。你可以继续完善你的图。grow方法的逻辑如下:ArrayList计算出扩容的大小后,做了什么?除了上面计算扩容大小的代码外,它也是grow的核心逻辑之一。第一次添加元素时,在grow中,最后会执行一行代码:elementData=Arrays.copyOf(elementData,newCapacity);这是grow方法中的另一个核心步骤,数组的复制。您可以单击Arrays.copyOf以查看它在后台执行的操作。代码如下:publicstaticT[]copyOf(T[]original,intnewLength){return(T[])copyOf(original,newLength,original.getClass());}publicstaticT[]copyOf(U[]original,intnewLength,ClassnewType){T[]copy=((Object)newType==(Object)Object[].class)?(T[])新对象[newLength]:(T[])Array.newInstance(newType.getComponentType(),newLength);System.arraycopy(原始,0,复制,0,Math.min(original.length,newLength));返回副本;}还是看上下文,这里Arrays.copyOf的实参是elementData(Object[]空数组)和newCapacity=10。可以看到它内部直接调用了一个重载的方法。在重载方法中,首先调用了一个三元表达式和一个System.arraycopy方法调用,然后是System.arraycopy(original,0,copy,0,Math.min(original.length,newLength));总而言之,它看起来像操作了原件和副本。然后你可以仔细分析细节。ArrayList的劣势,原因就在这里!根据传入的参数elementData的类是Object[].class的类型,可以看出三元表达式会执行(T[])newObject[newLength]。然后将执行System.arraycopy方法。你可以查看JDK的API,它的主要功能是复制数组。因为这个方法的API不是很懂。这里我告诉你,它的方法签名如下:System.arraycopy(Objectsrc,intsrcPos,Objectdest,intdestPos,intlength)。在这里,如果遇到难懂的代码,除了画图,另一种方式就是举例。举个例子:好了,知道了这个API的基本用法,我们再来看源码中的代码:System.arraycopy(original,0,copy,0,Math.min(original.length,newLength));首先,original是我们传进来的elementData。它的长度是0。newLength是传进来的newCapacity,是10。copy是我们刚刚创建的Object[]数组,长度是10。Math.min取后是0最低。也就是说变成了:System.arraycopy(original,0,copy,0,0);这个很好理解,就是从原来的位置0移动0个元素到复制数组,从复制数组0位置开始覆盖。也就是说什么都没有复制,直接返回我们新建的数组T[]copy,而这个数组的大小是10。最后Arrays.copyOf得到的是一个长度为10的数组,但是里面没有元素。然后执行这句话。elementData=Arrays.copyOf(elementData,newCapacity);grow方法的结尾也被执行了。grow方法的执行意味着ensureCapacityInternal的执行完成。这个方法已经保证了内部数组的容量足够大,可以容纳新的元素。回到最开始,第一步执行完,保证内部容量后,第二步直接执行elementData[size++]=e;通过对数组的赋值操作,第一次添加元素就完成了!这里可以发现,添加的时候,如果size不够,就会进入grow方法,进入grow方法进行1.5倍的扩容。而且,代码规范一般都建议我们指定ArrayList的大小。如果不指定,可能会导致频繁扩容和复制,导致性能低下。publicbooleanadd(Ee){ensureCapacityInternal(size+1);//递增modCount!!元素数据[大小++]=e;返回真;它已被彻底研究。最终的源码示意图如下:最后给大家总结一下收获:1、ArrayList的扩展机制大家知道了:底层使用grow方法。其核心之一是计算扩展大小,即按右移1位计算。一般展开尺寸为1.5倍。另一个核心是复制数组。底层使用System.arraycopy创建一个新数组,最终实现空间扩展。.2.你也知道,频繁的扩容会导致频繁的数组拷贝,会很耗时,影响ArrayList的性能。3.更重要的是,在这一节中,你学会了抓大放小的思路,更加熟练了先上下文后细节,甚至猜测的思路。您还做了适当的示例并画了图来分析源代码。大家可以通过今天学习的思路和方法看看ArrayList的另一个方法的源码:add(intindex,Eelement)这个方法的意思是在指定位置添加一个元素,研究下它的源码原理。金句甜品今天除了知识技能的增长,再给大家带来一道金句甜品来结束今天的分享:改变自己比改变别人更重要。其实,生活中的很多矛盾和问题,都源于我们总是要求别人做一些看不见的事情,夫妻之间,亲戚之间,朋友之间……当别人没有按照你的期望去做时,你可能会不开心,抱怨,甚至吵架,都有脾气。如果一个人有脾气,其实是你对事对人不够宽容。想想你对美女和丑女的容忍度肯定是不一样的。比如,不要总是希望父母理解你的所作所为,而是要改变自己去理解他们。他们的认知和你不一样。比如我父亲,他的年龄几乎是我的两倍,今年就60岁了。他的很多思想观念和毛主席时代是一样的。有时候他不了解这个时代。他的家人壁纸和手机壁纸都是毛主席的画像。他连微信转账都不会。他仍然使用现金。他不知道什么是大数据和云计算。他总是被微信朋友圈忽悠。我不应该让他们用微信转账,用支付宝支付,也不应该老是给他解释大数据云计算。相反,我应该改变自己。我应该理解他。我应该理解他可能没有多少文化。他的认知越低,他的认知就越低,有时他就越固执。我要改变自己,不能老是逼爸爸学习,当然,如果他想学,我会耐心地教他。渐渐地,他还是学会了转账的基本功,因为他喜欢抢红包。所以,其实大家可以考虑一下。生活中,很多时候男女朋友或者夫妻吵架,大多都是因为你要求对方做什么而引起的。那你不要总想着去改变别人,去问别人,改变自己比改变别人更重要。只有改变自己,才能更好地影响他人。相信每个人每天都改变一点点,学习一个成长故事,会更多地影响他人,和你一起学习成长。最后,看完源码可以饭后问问同事或者同学,也可以share出来告诉他。欢迎大家在评论区留言与我交流。(免责声明:JDK源码增长基于JDK1.8版本,部分章节会提及旧版本的特性)本文由博客发布平台OpenWrite发布!