Arraylist源码分析

ArrayList 我们几乎每天都会使用到,但是通常情况下我们只是知道如何去使用,至于其内部是怎么实现的我们不关心,但是有些时候面试官就喜欢问与ArrayList 的源码相关的问题,今天我们就来看看和ArrayList 源码相关的问题。

一:整体架构

1.1、ArrayList 结构

ArrayList 整体架构比较简单,就是一个数组结构,比较简单,如下图:

图中展示是长度为 n 的数组,index 表示数组的下标,从 0 开始计数,elementData 表示数组本身,源码中除了这两个概念,还有以下三个基本概念:

  1. DEFAULT_CAPACITY ,表示数组的初始大小,默认是 10;
  2. size ,当前数组的大小,没有使用 volatile 修饰,非线程安全的;
  3. modCount ,统计当前数组被修改的版本次数;

1.2、ArrayList 类注释

看源码,首先要看类注释,我们看看类注释上面都说了什么,部分截图如下图所示:

类注释主要讲了以下四点

  1. 允许put null 值,会自动扩容 ;
  2. size、isEmpty、get、set、add 等方法时间复杂度都是 O (1) ;
  3. 是非线程安全的,多线程情况下,推荐使用线程安全类:Collections#synchronizedList ;
  4. 增强 for 循环,或者使用迭代器迭代过程中,如果数组大小被改变,会快速失败,抛出异常

二:源码解析

2.1、初始化

ArrayList 有三种初始化办法:无参数直接初始化、指定大小初始化、指定初始数据初始化,源码如下:

// 无参数直接初始化,数组大小为空
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
} // 指定大小初始化
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
} // 指定初始数据初始化
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class) {
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}

注意: ArrayList 无参构造器初始化时,默认大小是空数组,并不是大家常说的 10,10 是在第一次 add 的时候扩容的数组值。

2.2、新增和扩容实现

新增方法主要分成两步:首先判断数组是否需要扩容,如果需要,执行扩容操作,否则,直接赋值。新增方法源码如下,其中ensureCapacityInternal()方法就是扩容操作。

public boolean add(E e) {
//确保数组大小是否足够,不够执行扩容,size 为当前数组的大小
ensureCapacityInternal(size + 1); // Increments modCount!!
//直接赋值,线程不安全的
elementData[size++] = e;
return true;
}

扩容(ensureCapacityInternal)的源码:

private void ensureCapacityInternal(int minCapacity) {
// 如果初始化数组大小时,有给定初始值,以给定的大小为准,不走 if 逻辑
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 确保容积足够
ensureExplicitCapacity(minCapacity);
} private void ensureExplicitCapacity(int minCapacity) {
// 记录数组被修改
modCount++;
// 如果我们期望的最小容量大于目前数组的长度,那么就扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
} // 扩容,并把现有数据拷贝到新的数组里面去
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// oldCapacity >> 1 是把 oldCapacity 除以 2 的意思
int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果扩容后的值 < 我们的期望值,扩容后的值就等于我们的期望值
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity; // 如果扩容后的值 > jvm 所能分配的数组的最大值,那么就用 Integer 的最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity); // 通过复制进行扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}

从扩容的源码我们可以看出:

  1. 扩容的规则并不是翻倍,而是原来容量的 1.5 倍 ;
  2. ArrayList 中的数组容量的最大值是 Integer.MAX_VALUE,超过这个值,JVM 就不会给数组分配内存空间了 ;

2.3、删除

ArrayList 删除元素有很多种方式,比如根据数组索引删除、根据值删除或批量删除等等,原理和思路都差不多,我们选取根据值删除方式来进行源码说明:

public boolean remove(Object o) {
// 如果要删除的值是 null,找到第一个值是 null 的删除
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
// 如果要删除的值不为 null,找到第一个和要删除的值相等的删除
for (int index = 0; index < size; index++)
// 这里是根据 equals 来判断值相等的,相等后再根据索引位置进行删除
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}

从上面的源码中我们可以看出:

  1. 由于新增的时候没有对 null 值做出判断,所以是可以删除 null 值的 ;
  2. 找到值在数组中的索引位置,是通过 equals 来判断的,如果数组元素不是基本类型,我们需要关注 equals 的具体实现;

删除的具体逻辑是在fastRemove()中实现的,源码如下所示

private void fastRemove(int index) {
// 记录数组的结构要发生变动了
modCount++;
// numMoved 表示删除 index 位置的元素后,需要从 index 后移动多少个元素到前面去
// 减 1 的原因,是因为 size 从 1 开始算起,index 从 0开始算起
int numMoved = size - index - 1;
if (numMoved > 0)
// 从 index +1 位置开始被拷贝,拷贝的起始位置是 index,长度是 numMoved
System.arraycopy(elementData, index+1, elementData, index, numMoved);
//数组最后一个位置赋值 null,帮助 GC
elementData[--size] = null;
}

2.4、复杂度分析

操作 时间复杂度
get() 根据下标直接查询 O(1)
add(E e) 直接尾部添加 O(1)
add(int index, E e) 插入后元素需要往后移动一个单位 O(n)
remove(E e) 删除后元素需要往前移动一个单位 O(n)

2.5、线程安全

我们需要强调的是,只有当 ArrayList 作为共享变量时,才会有线程安全问题,当 ArrayList 是方法内的局部变量时,是没有线程安全的问题的。

ArrayList 有线程安全问题的本质,是因为 ArrayList 自身的 elementData、size、modConut 在进行各种操作时,都没有加锁,而且这些变量的类型并非是可见(volatile)的,所以如果多个线程对这些变量进行操作时,可能会有值被覆盖的情况。

类注释中推荐我们使用 Collections#synchronizedList 来保证线程安全,SynchronizedList 是通过在每个方法上面加上锁来实现,虽然实现了线程安全,但是性能大大降低。

三:总结

从上面 ArrayList 的部分源码的分析中,我们可以发现 ArrayList 底层其实就是围绕数组结构,各个 API 都是对数组的操作进行封装,让使用者无需感知底层实现,只需关注如何使用即可。

最后

感谢你看到这里,看完有什么的不懂的可以在评论区问我,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!

面试官:你说你精通源码,那你知道ArrayList 源码的设计思路吗?的更多相关文章

  1. 一个HashMap能跟面试官扯上半个小时

    一个HashMap能跟面试官扯上半个小时 <安琪拉与面试官二三事>系列文章 一个HashMap能跟面试官扯上半个小时 一个synchronized跟面试官扯了半个小时 一个volatile ...

  2. ArrayList源码分析超详细(转载)

    ArrayList源码分析超详细   ArrayList源码分析超详解 想要分析下源码是件好事,但是如何去进行分析呢?以我的例子来说,我进行源码分析的过程如下几步: 找到类:利用 IDEA 找到所需要 ...

  3. 厉害!这份阿里面试官 甩出的Spring源码笔记,GitHub上已经爆火

    前言 时至今日,Spring 在 Java 生态系统与就业市场上,面试出镜率之高,投产规模之广,无出其右.随着技术的发展,Spring 从往日的 IoC 框架,已发展成 Cloud Native 基础 ...

  4. 面试官:小伙子,听说你看过ThreadLocal源码?(万字图文深度解析ThreadLocal)

    前言 Ym8V9H.png (高清无损原图.pdf关注公众号后回复 ThreadLocal 获取,文末有公众号链接) 前几天写了一篇AQS相关的文章:我画了35张图就是为了让你深入 AQS,反响不错, ...

  5. 面试官问我:看过sharding-jdbc的源码吗?我吧啦吧啦说了一通!!

    写在前面 在产品初期快速迭代的过程中,往往为了快速上线而占据市场,在后端开发的过程中往往不会过多的考虑分布式和微服务,往往会将后端服务做成一个单体应用,而数据库也是一样,最初会把所有的业务数据都放到一 ...

  6. 面试官:讲讲Redis的五大数据类型?如何使用?(内含完整测试源码)

    写在前面 最近面试跳槽的小伙伴有点多,给我反馈的面试情况更是千差万别,不过很多小伙伴反馈说:面试中的大部分问题都能够在我的公众号[冰河技术]中找到答案,面试过程还是挺轻松的,最终也是轻松的拿到了Off ...

  7. 【笔记0-开篇】面试官系统精讲Java源码及大厂真题

    背景 开始阅读 Java 源码的契机,还是在第一年换工作的时候,被大厂的技术面虐的体无完肤,后来总结大厂的面试套路,发现很喜欢问 Java 底层实现,即 Java 源码,于是我花了半年时间,啃下了 J ...

  8. 深度分析:面试腾讯,阿里面试官都喜欢问的String源码,看完你学会了吗?

    前言 最近花了两天时间,整理了一下String的源码.这个整理并不全面但是也涵盖了大部分Spring源码中的方法.后续如果有时间还会将剩余的未整理的方法更新到这篇文章中.方便以后的复习和面试使用.如果 ...

  9. 面试官就是要问我SpringMVC的源码,差点顶不住!

    <对线面试官>系列目前已经连载22篇啦!有深度风趣的系列! [对线面试官]Java注解 [对线面试官]Java泛型 [对线面试官] Java NIO [对线面试官]Java反射 & ...

随机推荐

  1. ES5新增数组的方法

    ES5新增数组的方法 ES5新增数组常见方法(indexOf/forEach/map/filter/some/every) .indexOf( data , start)  检测数组中是否存在指定数据 ...

  2. STL: set和map的区别、联系、使用

    set是一种关联式容器,其特性如下: set以RBTree作为底层容器 所得元素的只有key(键)没有value(值) 不允许出现键重复 所有的元素都会被自动排序 不能通过迭代器来改变set的值,因为 ...

  3. Spring之AOP(面向切面编程)_入门Demo

    AOP是OOP的延续,是Aspect Oriented Programming的缩写,意思是面向切面编程.AOP实际是GoF设计模式的延续,设计模式孜孜不倦追求的是调用者和被调用者之间的解耦,AOP可 ...

  4. linux 安装配置zerotier

    1.在线安装zerotier curl -s https://install.zerotier.com/ | sudo bash 2.添加开机自启 $ sudo systemctl enable ze ...

  5. VirtualXposed结合justTrustMe 模块傻瓜式破解app没法抓包问题

    一.首先就是按照这两个apk 声明仅供学习 justTrustMe 链接:https://pan.baidu.com/s/1av3oaez4y4n6a9C1I0VsAg 提取码:mjqg Virtua ...

  6. centos7安装ifconfig

    1,找出哪个包提供了ifconfig命令 yum provides ifconfig 2,通过输出,发现是net-tools提供了ifconfig命令 yum install net-tools

  7. Zookeeper(2)---节点属性、监听和权限

    之前通过客户端连接之后我们已经知道了zk相关的很多命令(Zookeeper(1)---初识). 节点属性: 现在我们就通过stat指令来看看节点都有哪些属性,或者使用get 指令和-s参数来查看节点数 ...

  8. JavaScript 正则表达式:字符串中查找数字

    以下代码是在一段字符串中,用正则表达式找到数字,使用 replace() 方法,用找到的数字的两倍值替换原数字.replace() 方法的第二个参数为一个函数,返回找到数字的两倍值. <scri ...

  9. Map 和 Json 互转

    Json 转 Map<String,String> String query = "{tableName:\"iov_biz_car_info\",selec ...

  10. java开发-前后端分离

    众所周知,做java开发是后端的开发,我们时常与前端打交道,但更加注重后端代码的实现,前台的页面都是由前端开发人员做的,那么,是怎么做到前后端分离的呢? 首先,是后端的开发, 在mapper层:Stu ...