ArrayList

前言

java中的ArrayList是什么呢?我们带着问题来寻找答案吧


正文

ArrayList有用过吗?它是一个什么东西?可以用来干嘛?

用过的,ArrayList是数组列表,主要用来装载数据,当我们装载的是基本类型的数据int,long,boolean,short,byte...的时候我们只能存储他们对应的包装类,因为它的底层实现是数组Object[] elementData。

与它类似的是LinkedList,和LinkedList比较,它的查找和访问元素的速度较快,但新增、删除的速度较慢。

小结:ArrayList底层是用数组实现的存储。

特点:查询效率高,增删改效率低,线程不安全。使用频率很高。

为啥线程 不安全还使用他呢?

因为我们正常使用的场景中,都是用来查询的,不会涉及太频繁的增删,如果涉及频繁的增删,可以使用LinkedList,如果需要线程安全就使用Vector,这就是三者的区别了,实际开发过程中还是ArrayList使用最多的。

不存在一个集合工具是查询效率又高,增删效率也高的,还线程安全的,至于为啥大家看代码就知道了,因为数据结构的特性就是优劣共存的,想找个平衡点很难,牺牲了性能就安全,牺牲了安全就快速。

您说它的底层实现是数组,但是数组的大小是定长的,如果我们不断的往里面添加数据的话,不会有问题吗?

ArrayList可以通过构造方法在初始化的时候指定底层数组的大小的。

通过无参构造方法来初始化ArrayList,会赋值自身属性 Object[] elementData 为另一个自身属性Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {},该赋值属性为一个空数组,所以数组容量为0,只有后面调用add方法添加数据的时候才会分配默认DEFAULT_CAPACITY = 10的初始容量。

通过有参构造方法则会判断传入的容量大小,赋值Object[] elementData为>0的入参容量的Object[initialCapacity],或空数组,或直接抛出 IllegalArgumentException。

ArrayList

数组的长度是有限制的,而ArrayList是可以存放任意数量对象,长度不受限制,那么他是怎么实现的呢?

其实实现方式比较简单,他就是通过数组扩容的方式去实现的。

就比如我们现在有一个长度为10的数组,现在我们要新增一个元素,发现已经满了,那ArrayList会怎么做呢?

第一步他会重新定义一个长度为10+10/2的数组也就是新增一个长度为15的数组,然后把原数组的数据,原封不动的复制到新数组中,这个时候再把List的指针修改指向新数组,ArrayList就这样完成了一次改头换面。

ArrayList

Tip:很多小伙伴说,你举例干嘛用10这么长的数组举例,这样画都要多画一点格子了,画个2、3不就可以理解了么?因为我们在使用ArrayList的时候一般都是调用无参的构造方法,没有给定初始化容量的,这个时候ArrayList默认的大小就刚好是10。

ArrayList

能具体说下1.7之前和之后初始化的时候的区别么?

arrayList 1.7开始变化有点大,一个是初始化的时候,1.7以前会调用this(10)才是真正的容量为10,1.7即本身以后是默认走了空数组,只有第一次add的时候容量会变成10。

ArrayList的默认数组大小为什么是10?

据说是因为sun的程序员对一系列广泛使用的程序代码进行了调研,结果就是10这个长度的数组是最常用的最有效率的。也有说就是随便起的一个数字,8个12个都没什么区别,只是因为10这个数组比较的圆满而已。

我记得你说到了,他增删很慢,你能说一下ArrayList在增删的时候是怎么做的么?主要说一下他为啥慢。

恩恩,好的,我先说一下他的新增逻辑吧。

他有指定index新增,也有直接新增的,在这之前他会有一步校验长度的判断ensureCapacityInternal,就是说如果长度不够,是需要扩容的。

ArrayList

在扩容的时候,老版本的jdk和8以后的版本是有区别的,8之后的效率更高了,采用了位运算,右移一位,其实就是除以2这个操作。

1.7的时候3/2+1 ,1.8直接就是3/2。

ArrayList

指定位置新增的时候,在校验之后的操作很简单,先是数组扩容,容量够了后,就是数组的copy。

直接将数组从指定的index位置copy后半截,把它复制到index+1开始的后面部分,然后将inedx位置改为新值就完成了。

ArrayList

ArrayList

至于为啥说他效率低,我想你应该也知道了,第一只要涉及到扩容,他就要重新开辟一个内存空间并把数组复制进去,第二指定index添加他也要截断数组复制一次。

如果指定index又遇到扩容,就要开辟内存空间,再复制2次数组,就更慢了不是嘛。

我问你个真实的场景,这个问题很少人知道,你可要好好回答哟!ArrayList(int initialCapacity)会不会初始化数组大小?

会初始化数组大小!但是List的大小没有变,因为list的大小是返回size的。

而且将构造函数与initialCapacity结合使用,然后使用set()会抛出异常,尽管该数组已创建,但是大小设置不正确。

使用ensureCapacity()也不起作用,因为它基于elementData数组而不是大小。

还有其他副作用,这是因为带有ensureCapacity()的静态DEFAULT_CAPACITY=10。

进行此工作的唯一方法是在使用构造函数后,根据需要使用add()多次。

ArrayListArrayList

ArrayList插入删除一定慢么?

取决于你删除的元素离数组末端有多远,ArrayList拿来作为堆栈来用还是挺合适的,push和pop操作完全不涉及数据移动操作。

那他的删除怎么实现的呢?

删除其实跟新增是一样的,不过叫是叫删除,但是在代码里面我们发现,他还是在copy一个数组。

ArrayList

为啥是copy数组呢?

继续打个比方,我们现在要删除下面这个数组中的index5这个位置,那代码他就复制一个index5+1开始到最后的数组,然后把它放到index开始的位置,然后把最后一个位置设为null,并把size--。

ArrayList

index5的位置就成功被”删除“了其实就是被覆盖了,给了你被删除的感觉。同理他的效率也低,因为数组如果很大的话,一样需要复制和移动的位置就大了。

ArrayList是线程安全的么?

当然不是,线程安全版本的数组容器是Vector。Vector的实现很简单,就是把所有的方法统统加上synchronized就完事了。

你也可以不使用Vector,用Collections.synchronizedList把一个普通ArrayList包装成一个线程安全版本的数组容器也可以,原理同Vector是一样的,就是给所有的方法套上一层synchronized。

但是因为是方法层面的加锁,这2种方式在多方法混用的场景下也不是线程安全的。

ArrayList用来做队列合适么?

队列一般是FIFO(先入先出)的,如果用ArrayList做队列,就需要在数组尾部追加数据,数组头部删除数组,反过来也可以。

但是无论如何总会有一个操作会涉及到数组的数据搬迁,这个是比较耗费性能的。

结论:ArrayList不适合做队列。

那数组适合用来做队列么?

数组是非常合适的。比如ArrayBlockingQueue内部实现就是一个环形队列,它是一个定长队列,内部是用一个定长数组来实现的。

另外著名的Disruptor开源Library也是用环形数组来实现的超高性能队列,具体原理不做解释,比较复杂。

简单点说就是使用两个偏移量来标记数组的读位置和写位置,如果超过长度就折回到数组开头,前提是它们是定长数组。

ArrayList的遍历和LinkedList遍历性能比较如何?

论遍历ArrayList要比LinkedList快得多,ArrayList遍历最大的优势在于内存的连续性,CPU的内部缓存结构会缓存连续的内存片段,可以大幅降低读取内存的性能开销。

总结

ArrayList就是动态数组,用MSDN中的说法,就是Array的复杂版本,它提供了动态的增加和减少元素,实现了 Collection和 List接口,灵活的设置数组的大小等好处。

ArrayList
继承图例

你知道的越多,你不知道的越多,选择适合自己的技术栈最重要。