Array List

[TOC]

ArrayList

类图

image-20200804154736870

数据结构

数组:Object[] elementData

原理

扩容

知识了解

minCapacity:最小扩容量,分析源码不难知道,等于当前数组的size+1。

首次扩容

当我们使用ArrayList()无参构造器初始化对象的时候,默认不扩容,将默认的空数组赋值给数组。后续,在新增数据的时候,触发了扩容机制。

其中,扩容的大小取默认容量、最小扩容量的最大值,之后扩容逻辑同非首次扩容。

非首次扩容

源码处理流程:查看大图

源码分析:

JDK版本区别

1.7和1.8最大的区别就是:1.7是饿汉式创建集合容量,1.8是懒汉式创建集合容量

优缺点

优点

1、因为是数组,所以根据脚标取数据很快

缺点

1、数组的新增、删除操作效率不高,涉及到数组的扩容、复制等操作 2、线程不安全

拓展

以下代码,均基于jdk1.8

如何证明线程不安全?

在不运行之前,你觉得结果是如何?list.size()为10000吗。实际运行结果,每次都是不一样的, 且会出现ArrayIndexOutOfBoundsException异常,正是因为ArrayList没有保证线程安全,在并发访问时存在线程安全问题。

那么为什么会存在线程安全问题呢?这还得归根到源码里:

size++是非原子操作,在并发时,会有线程安全。

size++可拆解为size=size+1,细分为:第一步,计算size + 1 ,第二步,把size + 1的结果赋值给size,在执行第二步的时候,可能size的值已经发生了变化。

那么怎么简单解决呢?使用synchronized + volatile即可,当然了还有其他的方式,本篇文章不在赘述,在并发编程相关内容,将会详细研讨。

扩容了几次?

看看以下代码,猜猜一共扩容了几次?

为了方便了解到具体扩容了几次,我们使用反射的机制获取ArrayList中的elementData数组一看便知:

运行结果:

结合结果,可见一共扩容了7次,分别是添加1、2、3、4、5、7、10的时候,elementData数组大小变化为:0(new结束)、1、2、3、4、6、9、13。

没猜对?没关系,咱们在稍微变化下题目:

这次是几次呢?答案:稍后揭晓。

首先我们来回顾下两种情况的不同点——构造器:

很明显针对构造器来讲,区别就是初始赋值不一样,至于为啥不一样,源码解释了:

  • EMPTY_ELEMENTDATA:Shared empty array instance used for empty instances.

  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA:Shared empty array instance used for default sized empty instances. We distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when first element is added.

我们暂且将这两种情况称为实际空实例(即人为设置size=0)、默认空实例(即使用空造器)。看完了构造器,我们在来回顾下add的扩容机制:

也就是说,针对默认空实例、实际空实例处理是不一样的,默认空实例扩容直接使用默认的大小10,两者后续扩容都按照1.5倍的机制来扩容。

说到这里,第二种的情况,答案也有了:一共扩容了1次,添加1的时候,elementData数组大小变化为:0(new结束)、10。

至于为什么要谈这两道题目,一方便,是为了更好的理解jdk为ArrayList做的优化;另一方面,是避免小伙伴有new ArrayList<>(0);的习惯,以为节省了内存,实则可能会产生多次扩容的时间浪费。

Last updated