送分题,ArrayList的扩容机制了解吗?
1。ArrayList了解过吗?它是啥?有啥用?
众所周知,Java集合框架拥有两大接口Collection和Map,其中,Collection麾下三生子List、Set和Queue。ArrayList就实现了List接口,其实就是一个数组列表,不过作为Java的集合框架,它只能存储对象引用类型,也就是说当我们需要装载的数据是诸如int、float等基本数据类型的时候,必须把它们转换成对应的包装类。
ArrayList的底层实现是一个Object数组:
既然它是基于数组实现的,数组在内存空间中是连续分配的,那必然查询速率非常快,不过当然也肯定逃不过增删效率低的缺陷。
另外,和ArrayList一样同样实现了List接口的、我们比较常用的还有LinkedList。LinkedList比较特殊,它不仅实现了List接口,还实现了Queue接口,所以你可以看见LinkedList经常被当作队列使用:QueueIntegerqueuenewLinkedList();
LinkedList人如其名,它的底层自然是基于链表的,而且还是个双向链表。链表的特性和数组正好是反的,由于没有索引,所以查询效率低,但是增删速度快。
2。ArrayList如何指定底层数组大小的?
OK,首先,既然咱真正存储数据的地方是数组,那我们初始化ArrayList的时候自然要给数组分配一个大小,开辟一个内存空间。我们先来看看ArrayList的无参构造函数:
可以看到,它为底层的Object数组也就是elementData赋值了一个默认的空数组DEFAULTCAPACITYEMPTYELEMENTDATA。也就是说,使用无参构造函数初始化ArrayList后,它当时的数组容量为0。
这给咱初始化一个容量为0的数组有啥用?啥也存不了啊?别急,如果使用了无参构造函数来初始化ArrayList,只有当我们真正对数据进行添加操作add时,才会给数组分配一个默认的初始容量DEFAULTCAPACITY10。看下图:
说完了无参构造,ArrayList的有参构造函数就是中规中矩了,按照用户传入的大小开辟数组空间:
3。数组的大小一旦被规定就无法改变,那ArrayList是怎么对底层数组进行扩容的?
ArrayList的底层实现是Object数组,我们知道,数组的大小一旦被规定就无法改变。那如果我们不断的往里面添加数据的话,ArrayList是如何进行扩容的呢?或者说ArrayList是如何实现存放任意数量对象的呢?
OK,扩容发生在啥时候?那肯定是我们往数组中新加入一个元素但是发现数组满了的时候。没错,我们去add方法中看看ArrayList是怎么做扩容的:
ensureExplicitCapacity判断是否需要进行扩容,很显然,grow方法是扩容的关键:
说实话,别的都不用看了,看上面图中的黄色框框就知道ArrayList是怎么扩容的了:扩容后的数组长度当前数组长度当前数组长度2。最后使用Arrays。copyOf方法直接把原数组中的数组copy过来,需要注意的是,Arrays。copyOf方法会创建一个新数组然后再进行拷贝。
举个例子画个图来演示一下:
4。既然扩容发生在添加数据的时候,讲讲ArrayList具体是怎么添加数据的
OK,add方法我们刚刚讲了一半,添加数据前会先判断一下是否需要扩容,真正的添加数据的操作在下半部分:
先讲下add(intindex,Eelement)这个方法的含义,就是在指定索引index处插入元素element。比如说ArrayList。add(0,3),意思就是在头部插入元素3。
再来看看add方法的核心System。arraycopy,这个方法有5个参数:elementData:源数组index:从源数组中的哪个位置开始复制elementData:目标数组index1:复制到目标数组中的哪个位置sizeindex:要复制的源数组中数组元素的数量
解释一下上面代码中arraycopy的意思,举个例子,我们想要在index5的位置插入元素,首先,我们会复制一遍源数组elementData(这里我们称复制的数组为新数组吧),然后把源数组中从index5的位置开始到数组末尾的元素,放到新数组的index16的位置上:
于是,这就给我们要新增的元素腾出了位置,然后在新数组index5的位置放入元素element就完成了添加的操作:
显然,不用多说,ArrayList的将数据插入到指定位置的操作性能非常低下,因为要开辟新数组复制元素啊,要是涉及到扩容那就更慢了。
另外,ArrayList还内置了一个直接在末尾添加元素的add方法,不用复制数组,直接size就好,这个方法应该是我们最常使用的:
5。ArrayList又是如何删除数据的呢?
CtrlF找到remove方法,就这?和添加一个道理,也是复制数组
举个例子,假设我们要删除数组的index5的元素,首先,我们会复制一遍源数组,然后把源数组中从index16的位置开始到数组末尾的元素,放到新数组的index5的位置上:
也就是说index5的元素直接被覆盖掉了,给了你被删除的感觉。同样的,它的效率自然也是十分低下的6。ArrayList是线程安全的吗?不安全的表现
ArrayList和LinkedList都不是线程安全的,我们以在末尾添加元素的add方法为例,来看看ArrayList线程不安全的表现是啥:
黄色框里的并不是一个原子操作,它由两步操作构成:elementData〔size〕e;sizesize1;
在单线程执行这两条代码时,那当然没有任何问题,但是当多线程环境下执行时,可能就会发生一个线程添加的值覆盖另一个线程添加的值。举个例子:假设size0,我们要往这个数组的末尾添加元素线程A开始添加一个元素,值为A。此时它执行第一条操作,将A放在了数组elementData下标为0的位置上接着线程B刚好也要开始添加一个值为B的元素,且走到了第一步操作。此时线程B获取到的size值依然为0,于是它将B也放在了elementData下标为0的位置上线程A开始增加size的值,size1线程B开始增加size的值,size2
这样,线程A、B都执行完毕后,理想的情况应该是size2,elementData〔0〕A,elementData〔1〕B。而实际情况变成了size2,elementData〔0〕B(线程B覆盖了线程A的操作),下标1的位置上什么都没有。并且后续除非我们使用set方法修改下标为1的值,否则这个位置上将一直为null,因为在末尾添加元素时将会从size2的位置上开始。
上段代码验证下:
结果和我们分析的一样:
ArrayList的线程安全版本是Vector,它的实现很简单,就是把所有的方法统统加上synchronized:
既然它需要额外的开销来维持同步锁,所以理论上来说它要比ArrayList要慢。7。为什么线程不安全还要用它呢?
因为在大多数场景中,查询的情况居多,不会涉及太频繁的增删。那如果真的涉及频繁的增删,可以使用LinkedList,底层链表实现,为增删而生。而如果你非得保证线程安全那就使用Vector。当然实际开发中使用最多的还是ArrayList,虽然线程不安全、增删效率低,但是查询效率高啊。