ArrayList为什么可以扩容?它的扩容机制是什么?

ArrayList了解吗,它是什么?有什么作用?

众所周知,Java 集合框架拥有两大接口 Collection 和 Map,其中,Collection 麾下三生子 List、Set 和 Queue。ArrayList 就实现了 List 接口,其实就是一个数组列表,不过作为 Java 的集合框架,它只能存储对象引用类型,也就是说当我们需要装载的数据是诸如 int、float 等基本数据类型的时候,必须把它们转换成对应的包装类。

ArrayList的底层实现是数组(Object []数组)
在这里插入图片描述
既然它是数组实现的,那么ArrayList的查询速度肯定是很快的,但是增删效率会很低

还有另一个实现了List接口的LinkedList,它同时也实现了Queue接口,所以它也经常当作队列来使用

Queue<Integer> queue = new LinkedList<>();

而且LinkedList人如其名,底层实现那肯定是链表了,而且还是双向链表,链表的特性和数组正好相反,由于没有索引,所以查询速度较慢,但是增删的效率高
在这里插入图片描述

ArrayList如何指定大小

既然ArrayList要用来存储数据,而且真正存储的地方是数组,那么创建时肯定要为其分配内存,开辟空间,我们先来看一下它的构造函数
在这里插入图片描述
可以看到,它为底层的 Object 数组也就是 elementData 赋值了一个默认的空数组 DEFAULTCAPACITY_EMPTY_ELEMENTDATA。也就是说,使用无参构造函数初始化 ArrayList 后,它当时的数组容量为 0 。
**一个容量为0的数组怎么存储数组?有什么用?**不用担心,如果我们调用了无参构造创建一个ArrayList,那么只有在我们第一次调用add方法时才会为数组赋予一个默认容量
DEFAULT_CAPACITY=10
在这里插入图片描述
既然无参构造是在第一次调用add方法时给定一个默认容量,那么有参构造是怎么工作的呢?
答案很简单,在构造的时候传入一个整数即可,整数大小就是我们的数组大小
在这里插入图片描述

数组的大小是固定的,那ArrayList是怎么扩容的?

ArrayList 的底层实现是 Object 数组,我们知道,数组的大小一旦被规定就无法改变。那如果我们不断的往里面添加数据的话,ArrayList 是如何进行扩容的呢?或者说 ArrayList 是如何实现存放任意数量对象的呢?
那我们添加元素是使用add方法,那么当数组元素已经加满的时候,肯定会在add方法里有对应的扩容方法,我们看一下add方法的源码
在这里插入图片描述
ensureExplicitCapacity 判断是否需要进行扩容,很显然,grow 方法是扩容的关键:
在这里插入图片描述
只要看上述黄色框里的代码就可知,扩容后的数组长度 = 当前数组长度 + 当前数组长度 / 2。最后使用 Arrays.copyOf 方法直接把原数组中的数组 copy 过来,需要注意的是,Arrays.copyOf 方法会创建一个新数组然后再进行拷贝。(所以扩容后的Array List不是原来的)
举个例子画个图来演示一下:
在这里插入图片描述

扩容发生在调用add方法的时候,那么add方法是怎么添加数据的?

add方法其实在每次添加元素之前都会判断是否需要扩容,然后再对数据进行添加操作
在这里插入图片描述
先讲下 add(int index, E element) 这个方法的含义,就是在指定索引 index 处插入元素 element。比如说 ArrayList.add(0, 3),意思就是在头部插入元素 3。
再来看看 add 方法的核心 System.arraycopy,这个方法有 5 个参数:
elementData源数组
index从源数组中的哪个位置开始复制
elementData目标数组
index + 1复制到目标数组中的哪个位置
size - index要复制的源数组中数组元素的数量
解释一下上面代码中 arraycopy 的意思,举个例子,我们想要在 index = 5 的位置插入元素,首先,我们会复制一遍源数组 elementData(这里我们称复制的数组为新数组吧),然后把源数组中从 index = 5 的位置开始到数组末尾的元素,放到新数组的 index + 1 = 6 的位置上:
在这里插入图片描述
这样的话我们就给要插入元素的索引处腾出了位置
在这里插入图片描述
显然,不用多说,ArrayList 的将数据插入到指定位置的操作性能非常低下,因为要开辟新数组复制元素啊,要是涉及到扩容那就更慢了。
另外ArrayList还内置了一个将元素直接添加到末尾处的add方法,这应该是我们最常用到的。
在这里插入图片描述

ArrayList是如何删除数据的呢?

其实和add方法是差不多的道理
在这里插入图片描述
举个例子,比如我们要将索引位置index=5的元素删除,其实也是复制一遍原数组,然后把从index+1位置处到末尾处的元素添加到新数组的index=5位置上
在这里插入图片描述
也就是说 index = 5 的元素直接被覆盖掉了,给了你被删除的感觉。同样的,它的效率自然也是十分低下的

ArrayList是线程安全的吗?

大家应该都知道是不安全的,而且ArrayList和LinkedList都是线程不安全的,我们以add方法举例子,看一下不安全是体现在何处
在这里插入图片描述
我们可以看到黄色框的代码其实并不是原子性的,它是分两步走的

elementData[size] = e;
size = size + 1;

在单线程执行这两条代码时,那当然没有任何问题,但是当多线程环境下执行时,可能就会发生一个线程添加的值覆盖另一个线程添加的值。举个例子:
假设 size = 0,我们要往这个数组的末尾添加元素
线程 A 开始添加一个元素,值为 A。此时它执行第一条操作,将 A 放在了数组 elementData 下标为 0 的位置上
接着线程 B 刚好也要开始添加一个值为 B 的元素,且走到了第一步操作。此时线程 B 获取到的 size 值依然为 0,于是它将 B 也放在了 elementData 下标为 0 的位置上
1)线程 A 开始增加 size 的值,size = 1
2)线程 B 开始增加 size 的值,size = 2
这样,线程 A、B 都执行完毕后,理想的情况应该是 size = 2,elementData[0] = A,elementData[1] = B。而实际情况变成了 size = 2,elementData[0] = B(线程 B 覆盖了线程 A 的操作),下标 1 的位置上什么都没有。并且后续除非我们使用 set 方法修改下标为 1 的值,否则这个位置上将一直为 null,因为在末尾添加元素时将会从 size = 2 的位置上开始。
我们来验证一下
在这里插入图片描述可以看到并不是多线程情况下A、B的元素是穿插在数组中的
在这里插入图片描述
ArrayList 的线程安全版本是 Vector,它的实现很简单,就是把所有的方法统统加上 synchronized :
在这里插入图片描述
但是加锁就会需要额外的开销来维持,所以Vector的性能不及ArrayList

那为什么线程不安全还那么多人使用?

因为大部分情况下,查询的情况居多,不会涉及太频繁的增删。那如果真的涉及频繁的增删,可以使用LinkedList,底层链表实现,为增删而生。而如果你非得保证线程安全那就使用 Vector。当然实际开发中使用最多的还是 ArrayList,虽然线程不安全、增删效率低,但是查询效率高啊

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值