看完这篇ArrayList,工资直接+1000

这是我参与更文挑战的第 19 天,活动详情查看:更文挑战

作者:JavaGieGie

微信公众号:Java开发零到壹

前言

ArrayList花Gie猜想应该所有Java的小伙伴都用过,如果还有小伙伴没用过,请文末留言,你放学给我留下来我给你补习补习。本文是集合类讲解的第一篇,选择了一个相对比较简单、大家又比较熟悉的ArrayList开篇。集合是Java中非常重要而且基础的内容,因为任何数据必不可少的就是该数据是如何存储的,集合的作用就是以一定的方式组织、存储数据。

正文

狗剩子:花Gie,新系列开启,还有点小激动呀!

毕竟同时肝几个系列,也是有点要老命的,你看我这日渐光亮的头顶,哎,啥也不说了。

狗剩子:….以后省洗发水了你,憋扯犊子了,给我说说学习集合我要注意哪几点吧!

(摸了摸光滑的头顶,若有所思)对于集合,我认为关注的点主要有以下四点

  • 是否允许空
  • 是否允许重复数据
  • 是否有序,有序的意思是读取数据的顺序和存放数据的顺序是否一致
  • 是否线程安全

狗剩子:可以说下ArrayList怎么用的吗?

首先看一下他的新增元素的方法add(),非常简单,代码如下:

1
2
3
java复制代码List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);

狗剩子:看着是挺简单的,那就说下新增在底层是怎么实现的吧!

….这么突然的吗,直接肝到原理了,好吧,来来来

add方法的源码来看一下:

image.png

第2行的ensureCapacity方法是扩容用的,占时先不看。底层实际上在调用add方法的时候只是给elementData的某个位置添加了一个数据而已,用一张图表示的话是这样的:

图片

这里需要提醒一下,elementData中存储的应该是堆内存中元素的引用,而不是实际的元素,花Gie这么画图主要是为了小伙伴们理解,只要知道这个问题就好了。

狗剩子:那ensureCapacity这个扩容方法是什么原理呢?

哟呼,狗子今天当了面试官,还知道追问了。那我们先看一下,构造ArrayList的时候,默认的底层数组大小是10:

image.png

既然固定了大小,那底层数组的大小不够了怎么办?狗子都知道那就是扩容,这也就是为什么一直说ArrayList的底层是基于动态数组实现的原因,动态数组的意思就是指底层的数组大小并不是固定的,而是根据添加的元素大小进行一个判断,不够的话就动态扩容,扩容的代码就在ensureCapacity里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码 private void grow(int minCapacity) {
//1. 获取数组长度
int oldCapacity = elementData.length;

//oldCapacity >> 1 相当于除以2
//2. 新数组容量=原数组容量 * 1.5。
int newCapacity = oldCapacity + (oldCapacity >> 1);

//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

1
2
3
4
5
6
7
java复制代码private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}

狗剩子:感觉很简单的扩容方式,那为什么要使用这种方式扩容呢?

大佬们定的,我能回答出来怎么扩容的还不够强么,还问为啥,你是十万个为什么嘛。

狗剩子:回答对了工资加五百

这样呀,那这个就好办了。

image.png

我们可以想:

1、如果一次性扩容扩得太大,必然造成内存空间的浪费

2、如果一次性扩容扩得不够,那么下一次扩容的操作必然比较快地会到来,这会降低程序运行效率,要知道扩容还是比较耗费性能的一个操作

所以扩容扩多少,是JDK开发人员在时间、空间上做的一个权衡,提供出来的一个比较合理的数值。最后调用到的是Arrays的copyOf方法,将元素组里面的内容复制到新的数组里面去:

image.png

用一张图来表示就是这样的:

图片

狗剩子:带图示的就很棒,给你一个么么哒,除了顺序添加元素,肯定有按照下标插入的咯

你可真是个机灵鬼,ArrayList的插入操作调用的也是add方法,比如:

1
2
3
4
5
6
7
8
9
10
11
java复制代码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)。看一下运行结果也证明了这一点:

1
csharp复制代码[111, 222, 000, 333, 444, 555, 666, 777, 888]

还是看一下插入的时候做了什么:

image.png

​ 可以看到插入的时候,先用ensureCapacity方法进行判断是否扩容按照指定位置,然后利用System.arraycopy方法从指定位置开始的所有元素做一个整体的复制,向后移动一个位置,然后指定位置的元素设置为需要插入的元素,完成了一次插入的操作。用图表示这个过程是这样的:

图片

狗剩子:哟~不错哦,那再说说删除元素吧!

ArrayList支持以下两种删除方式:

  • 按照下标删除,即list.remove(1)
  • 按照元素删除,使用方式为list.remove(“111”),如果有多个等值的元素111,也只是会删除匹配的第一个元素

从代码来看,这两种删除方法在ArrayList的实现原理差不多,都是调用的下面一段代码:

image.png

简单概括其实做的事情只有两件:

  1. 把指定元素后面位置的所有元素,利用System.arraycopy方法整体向前移动一个位置
  2. 最后一个位置的元素指定为null,这样让gc可以去回收它

比方现在操作这一段代码:

1
2
3
4
5
6
7
8
9
10
java复制代码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的优点如下:

  1. ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快
  2. ArrayList在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已

不过ArrayList的缺点也十分明显:

  1. 删除元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能
  2. 插入元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能

因此,简单总结就是:ArrayList比较适合顺序添加、随机访问的场景

拓展部分

狗剩子:这里有个疑问困扰了我好久了呢,为什么ArrayList的elementData是用transient修饰的?

image.png

这个差点超出了我的认知,还好花Gie昨晚看过秘籍宝典。

image.png

我们看一下ArrayList的定义:

image.png

ArrayList实现了Serializable接口,也就是说ArrayList是可以被序列化的,而用transient修饰elementData意味着我不希望elementData数组被序列化。

这一万个草泥马奔腾而过,不能慌?因为序列化ArrayList的时候,elementData未必是满的,比方说elementData有10的大小,但是我只用了其中的3个,那么是否有必要序列化整个elementData呢?显然没有这个必要,因此ArrayList中重写了writeObject方法:

image.png

每次序列化的时候调用这个方法,先调用defaultWriteObject()方法序列化ArrayList中的非transient元素,elementData不去序列化它,然后遍历elementData,只序列化那些有的元素,这样就会有两点好处:

  1. 加快了序列化的速度
  2. 减小了序列化之后的文件大小

总结

ArrayList的重要性大家应该都懂,这里也就不啰嗦了。我们在读源码的时候,其实有很多可以值得我们借鉴,比如elementData使用transient来修饰,学习中需要多思考,把学习到的技术和思想运用到自己实际开发中,学以致用,才能不断强大。

点关注,防走丢

以上就是本期全部内容,如有纰漏之处,请留言指教,非常感谢。我是花GieGie ,有问题大家随时留言讨论 ,我们下期见🦮。

+++

文章持续更新,可以微信搜一搜 Java开发零到壹 第一时间阅读,并且可以获取面试资料学习视频等,有兴趣的小伙伴欢迎关注,一起学习,一起哈🐮🥃。

原创不易,你怎忍心白嫖,如果你觉得这篇文章对你有点用的话,感谢老铁为本文点个赞、评论或转发一下,因为这将是我输出更多优质文章的动力,感谢!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%