前言
ArrayList
猜想应该所有Java的小伙伴都用过,如果还有小伙伴没用过,请三联,你放学给我留下来我给你补习补习。本文是集合类讲解的第一篇,选择了一个相对比较简单、大家又比较熟悉的ArrayList开篇。集合是Java中非常重要而且基础的内容,因为任何数据必不可少的就是该数据是如何存储的,集合的作用就是以一定的方式组织、存储数据。
憋扯犊子了,说说学习集合我要注意哪几点吧!
对于集合,我认为关注的点主要有以下四点:
- 是否允许空
- 是否允许重复数据
- 是否有序,有序的意思是读取数据的顺序和存放数据的顺序是否一致
- 是否线程安全
可以说下ArrayList怎么用的吗?
首先看一下他的新增元素的方法add()
,非常简单,代码如下:
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
看着是挺简单的,那就说下新增在底层是怎么实现的吧!
…这么突然的吗,直接肝到原理了,好吧,来来来
add方法的源码来看一下:
第2行的ensureCapacity方法是扩容用的,占时先不看。底层实际上在调用add方法的时候只是给elementData的某个位置添加了一个数据而已,用一张图表示的话是这样的:【领取资料】
这里需要提醒一下,elementData中存储的应该是堆内存中元素的引用,而不是实际的元素,花Gie这么画图主要是为了小伙伴们理解,只要知道这个问题就好了。
狗剩子:那ensureCapacity这个扩容方法是什么原理呢?
哟呼,狗子今天当了面试官,还知道追问了。那我们先看一下,构造ArrayList的时候,默认的底层数组大小是10:
既然固定了大小,那底层数组的大小不够了怎么办?狗子都知道那就是扩容,这也就是为什么一直说ArrayList的底层是基于动态数组实现的原因,动态数组的意思就是指底层的数组大小并不是固定的,而是根据添加的元素大小进行一个判断,不够的话就动态扩容,扩容的代码就在ensureCapacity里面:
private void grow(int minCapacity) {
//1\. 获取数组长度
int oldCapacity = elementData.length;
//oldCapacity >> 1 相当于除以2
//2\. 新数组容量=原数组容量 * 1.5。
int newCapacity = oldCapacity + (oldCapacity >> 1);
//vx小助手:xiehuangbao1123 领取资料面试真题
//3\. 如果新的数组容量小于传入的参数要求的最小容量minCapacity,那么新的数组容量以传入的容量参数为准。
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//4\. 判断新的数组容量newCapacity是否大于数组能容纳的最大元素个数 MAX_ARRAY_SIZE
if (newCapacity - MAX_ARRAY_SIZE > 0)
//5.
newCapacity = hugeCapacity(minCapacity);
//6\. 将扩容前数组放进新的扩容后的数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
其中第 5 步hugeCapacity(minCapacity)
用于判断传入的参数minCapacity
是否大于MAX_ARRAY_SIZE
,如果minCapacity
大于MAX_ARRAY_SIZE
,那么newCapacity
等于Integer.MAX_VALUE
,否者newCapacity
等于MAX_ARRAY_SIZE
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//vx小助手:xiehuangbao1123 领取资料面试真题
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
感觉很简单的扩容方式,那为什么要使用这种方式扩容呢?
大佬们定的,我能回答出来怎么扩容的还不够强么,还问为啥,你是十万个为什么嘛。
回答对了工资加五百【领取资料】
这样呀,那这个就好办了。
我们可以想:
1、如果一次性扩容扩得太大,必然造成内存空间的浪费
2、如果一次性扩容扩得不够,那么下一次扩容的操作必然比较快地会到来,这会降低程序运行效率,要知道扩容还是比较耗费性能的一个操作
所以扩容扩多少,是JDK开发人员在时间、空间上做的一个权衡,提供出来的一个比较合理的数值。最后调用到的是Arrays的copyOf方法,将元素组里面的内容复制到新的数组里面去:
用一张图来表示就是这样的:
带图示的就很棒,给你一个么么哒,除了顺序添加元素,肯定有按照下标插入的咯
你可真是个机灵鬼,ArrayList的插入操作调用的也是add方法,比如:
List<String> list = new ArrayList<>();
list.add("111");
list.add("222");
list.add("333");
list.add("444");
list.add("555");
list.add("666");
list.add("777");
list.add("888");
list.add(2,"000");
System.out.println(list);
有一个地方不要搞错了,第10行的add方法的意思是,往第几个下标插入数据,像第10行就是在下标为2的位置插入数据000(注意ArrayList下标从0开始,即list.get(0)的值是111)。看一下运行结果也证明了这一点:
[111, 222, 000, 333, 444, 555, 666, 777, 888]
还是看一下插入的时候做了什么:【领取资料】
可以看到插入的时候,先用ensureCapacity方法进行判断是否扩容按照指定位置,然后利用System.arraycopy方法从指定位置开始的所有元素做一个整体的复制,向后移动一个位置,然后指定位置的元素设置为需要插入的元素,完成了一次插入的操作。用图表示这个过程是这样的:
:哟~不错哦,那再说说删除元素吧!
ArrayList支持以下两种删除方式:
-
按照下标删除,即list.remove(1)
-
按照元素删除,使用方式为list.remove(“111”),如果有多个等值的元素
111
,也只是会删除匹配的第一个元素
从代码来看,这两种删除方法在ArrayList的实现原理差不多,都是调用的下面一段代码:
简单概括其实做的事情只有两件:
-
把指定元素后面位置的所有元素,利用System.arraycopy方法整体向前移动一个位置
-
最后一个位置的元素指定为null,这样让gc可以去回收它
比方现在操作这一段代码:
List<String> list = new ArrayList<>();
list.add("111");
list.add("222");
list.add("333");
list.add("444");
list.add("555");
list.add("666");
list.add("777");
list.add("888");
list.remove("3333");
用图表示是为:
那你总结一下ArrayList特点呗!
ArrayList应该是小白到大佬都非常常用的集合类,它是一个以数组形式实现的集合,花Gie这里用一张表格先来看一下ArrayList里面有哪些基本的元素:【领取资料】
元 素 | 作 用 |
---|---|
private transient Object[] elementData; | ArrayList是基于数组的一个实现,elementData就是底层的数组 |
private int size; | ArrayList里面元素的个数,这里要注意一下,size是按照调用add、remove方法的次数进行自增或者自减的,所以add了一个null进入ArrayList,size也会加1 |
ArrayList属性一览:
属 性 | 结 论 |
---|---|
ArrayL ist是否允许空 | 允许 |
ArrayList是否允许重复数据 | 允许 |
ArrayList是否有序 | 有序 |
ArrayList是否线程安全 | 非线程安全 |
说了那么多,那ArrayList有啥优缺点呀
任何事物都不是完美无瑕的,我们要结合场景合理使用,ArrayList的优点如下:
- ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快
- ArrayList在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已
不过ArrayList的缺点也十分明显:
-
删除元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能
-
插入元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能
因此,简单总结就是:ArrayList比较适合顺序添加、随机访问的场景。
拓展部分
这里有个疑问困扰了我好久了呢,为什么ArrayList的elementData是用transient修饰的?
这个差点超出了我的认知,还好昨晚看过秘籍宝典。
我们看一下ArrayList的定义:
ArrayList实现了Serializable接口,也就是说ArrayList是可以被序列化的,而用transient修饰elementData意味着我不希望elementData数组被序列化。
这一万个草泥马奔腾而过,不能慌?因为序列化ArrayList的时候,elementData未必是满的,比方说elementData有10的大小,但是我只用了其中的3个,那么是否有必要序列化整个elementData呢?显然没有这个必要,因此ArrayList中重写了writeObject方法:
【领取资料】
每次序列化的时候调用这个方法,先调用defaultWriteObject()
方法序列化ArrayList中的非transient元素,elementData不去序列化它,然后遍历elementData,只序列化那些有的元素,这样就会有两点好处:
-
加快了序列化的速度
-
减小了序列化之后的文件大小
总结
ArrayList的重要性大家应该都懂,这里也就不啰嗦了。我们在读源码的时候,其实有很多可以值得我们借鉴,比如elementData使用transient来修饰,学习中需要多思考,把学习到的技术和思想运用到自己实际开发中,学以致用,才能不断强大。