ArrayList源码解析--深入理解常用的JAVA对象

字数 1186阅读 65

JAVA开发有很多常用的数据类型,例如:ArrayList,Vector,HashMap,HashTable,StringBuffer,StringBuilder等。本文尝试从以下三个方面分析ArrayList源码。

问题:

【Question1】:ArrayList如何存储数据?

【Question2】:都说ArrayList非线程安全,Vector是线程安全,究竟怎么回事?

【Question3】:从性能方面考虑,使用ArrayList需要注意什么?

【Answer1】:类名当中的array就已经说明,它是以数组的形式存储数据的。那么,紧接着3个问题来了:

1.1 数组初始容量capacity为多大?如果太小,很快就装满了。如果过大,数组又没被填满,就会造成内存的浪费。

1.2 数组被填满之后再往里加数据,数组怎么应对?

1.3 数据太多,多到超过Integer.MAX_VALUE。数组怎么应对?

answer1.1:两个办法,要么用户自己指定capacity的大小;要么给capacity一个初始值。ArrayList的实现中:

办法一:让用户自己指定初始容量。此办法可以申请少于10个元素容量的数组。

办法二:初始容量为0。第一次添加数据时,申请默认容量为10的数组空间。把数据放入数组。此策略优点是,实例化ArrayList又不用时,可以避免内存浪费。该策略称为惰性初始化。缺点,无法申请少于10个元素容量的数组。

answer1.2:add数据过程:

先检查数组是否还有剩余元素空间,再添加数据。如果有,直接数组末尾添加数据。如果没有,就先把当前数组copy到一个长50%的空数组中,再往新数组的尾部添加元素。

[举例]

ArrayList添加元素过程与用整理箱装东西类似。在没有东西要装之前是没有买整理箱的,在有了第一件需要装起来的物品之后,买了一个只能装10件物品的小整理箱,然后把第一件物品放进去。再有新物品以来时,先检查整理箱是还有空间,如果有就往里面放;如果没有就买一个比现在整理箱大50%的新整理箱,然后旧整理箱里的物品全部转移动新整理箱中,然后把新物品放入新整理箱。如此持续下去,当发现需要装的物品太多,多到整个房子都装不下了,就抛异常。

answer1.3:直接抛异常。

【Answer2】:Vector在add,get方法前加了"synchronized",而ArrayList没有。

【Answer3】:实例化ArrayList时,指定ArrayList的容量。

copy数组的过程很费时间。所以,为了提高性能,尽可能事先估计列表长度,在实例化ArrayList时就指定长度。以避免添加元素过程中,反复通过copy数组来扩容。

[实验验证]:

1、实验过程:

往ArrayList添加300w个字符串(随机生成的int),分别执行10次,经过初始化长度的ArrayList平均用时50.1ms;未初始化长度的ArrayList平均用时148.1ms,每次添加300w个字符串,ArrayList扩容33次,也就是copy数组33次。

2、实验结论:

2.1、实例化ArrayList时指定ArrayList的容量,确实可以提高性能;

2.2、性能相差3倍;

3、结论分析:

3.1、性能相差3倍,但性能差别的绝对值只有100ms左右,相差不是很大。原因很可能是实验用的字符串对象比较小,所以copy数组的速度很快。如果是大型的POJO对象,差别会更明显。

3.2、300w条字符串add操作在200ms之内完成。所以,如果要添加的对象不多,对象也不大,可以不用太关心性能的问题。

3.3、如果是大对象或者对象又比较多,最好考虑指定初始容量大小。当然,也可以在大量添加对象之前,调用

ensureCapacity方法预先扩容。

【后续任务】

数组扩容的其他算法。在ArrayList中采用了数组copy的办法来扩容,可以考虑别的扩容算法。

推荐阅读更多精彩内容