ArrayList是我们开发中最常用到的集合,但是很多人对它的源码并不了解,导致面试时,面试官问的稍微深入的问题,就无法作答,今天我们一起来探究一下ArrayList源码。

1. 简介

  • ArrayList底层是数组,允许元素是null,能够动态扩容
  • size、isEmpty、get、set、add 等方法时间复杂度都是 O (1)
  • 非线程安全,并发修改时,会抛出ConcurrentModificationException

2. 初始化

// 初始容量
private static final int DEFAULT_CAPACITY = 10; // 空数组
private static final Object[] EMPTY_ELEMENTDATA = {}; // 默认空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 存储元素的数组
transient Object[] elementData; // 无参初始化,默认是空数组
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);
}
}

切记:无参初始化的时候,默认是空数组,并没有初始化容量大小,容量是在第一次添加元素的才进行初始化。

3. 添加元素

public boolean add(E e) {
// 确保数组容量够用,size是元素个数
ensureCapacityInternal(size + 1);
// 直接赋值
elementData[size++] = e;
return true;
} // 确保数组容量够用
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
} // 计算所需最小容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果数组等于空数组,最小容量为10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return 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,意思是1.5倍扩容
int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果扩容后的容量小于最小容量,扩容后的容量就等于最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity; // 如果扩容后的容量大于Integer的最大值,就用Integer最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity); // 扩容并赋值给原数组
elementData = Arrays.copyOf(elementData, newCapacity);
}

可以看到:

  • 扩容是以原容量的1.5倍扩容,并不是翻倍扩容
  • 最大容量是Integer的最大值
  • 添加元素时,没有对元素校验,可以是null

再看一下数组拷贝的逻辑,这里都是Arrays类里面的方法了:

/**
* @param original 原数组
* @param newLength 新的容量大小
*/
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
} public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
// 创建一个新数组,容量是新的容量大小
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
// 把原数组的元素拷贝到新数组
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}

最终调用了System类的数组拷贝方法,是native方法:

/**
* @param src 原数组
* @param srcPos 原数组的开始位置
* @param dest 目标数组
* @param destPos 目标数组的开始位置
* @param length 被拷贝的长度
*/
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);

4. 删除单个元素

public boolean remove(Object o) {
// 判断要删除的元素是否为null
if (o == null) {
// 遍历数组
for (int index = 0; index < size; index++)
// 如果和当前位置上的元素相等,就删除当前位置上的元素
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
// 遍历数组
for (int index = 0; index < size; index++)
// 如果和当前位置上的元素相等,就删除当前位置上的元素
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
} // 删除该位置上的元素
private void fastRemove(int index) {
modCount++;
// 计算需要移动的元素的个数
int numMoved = size - index - 1;
if (numMoved > 0)
// 从index+1位置开始拷贝,也就是后面的元素整体向左移动一个位置
System.arraycopy(elementData, index+1, elementData, index, numMoved);
// 数组最后一个元素赋值为null,防止会导致内存泄漏
elementData[--size] = null;
}

可以知道,删除元素,就是遍历数组,循环比较是否等于目标值。如果相等,就把该位置后面的元素整体向左移动一个位置,再把数组最后一个元素赋值为null。

5. 批量删除

// 批量删除ArrayList和集合c都存在的元素
public boolean removeAll(Collection<?> c) {
// 非空校验
Objects.requireNonNull(c);
// 批量删除
return batchRemove(c, false);
} private boolean batchRemove(Collection<?> c, boolean complement){
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
// 把需要保留的元素左移
elementData[w++] = elementData[r];
} finally {
// 当出现异常的时候,可能不相等
if (r != size) {
// 1:可能是上面的for循环出现了异常
// 2:可能是其它线程添加了元素
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
// 把不需要保留的元素赋值为null
if (w != size) {
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}

可以知道,批量删除的时候,只会进行一次数组拷贝,比用for循环单个删除效率更高,所以删除一批元素的时候,尽量用removeAll()方法。

5. 总结

本文分析了ArrayList的初始化、put、add、remove、动态扩容等方法的底层源码,相信大家对于ArrayList有了更深层次的了解,下篇一块学习一下LinkedList的源码。

竟然还有人说ArrayList是2倍扩容,今天带你手撕ArrayList源码的更多相关文章

  1. 刨死你系列——手撕ArrayList

    不多BB,直接上代码: public class MyArrayList { //创建数组对象 private Object[] elements; //已使用数组长度 private int siz ...

  2. Java - ArrayList源码分析

    java提高篇(二一)-----ArrayList 一.ArrayList概述 ArrayList是实现List接口的动态数组,所谓动态就是它的大小是可变的.实现了所有可选列表操作,并允许包括 nul ...

  3. Java泛型底层源码解析-ArrayList,LinkedList,HashSet和HashMap

    声明:以下源代码使用的都是基于JDK1.8_112版本 1. ArrayList源码解析 <1. 集合中存放的依然是对象的引用而不是对象本身,且无法放置原生数据类型,我们需要使用原生数据类型的包 ...

  4. Java小白集合源码的学习系列:ArrayList

    ArrayList源码学习 本文基于JDK1.8版本,对集合中的巨头ArrayList做一定的源码学习,将会参考大量资料,在文章后面都将会给出参考文章链接,本文用以巩固学习知识. ArrayList的 ...

  5. Java8集合框架——ArrayList源码分析

    java.util.ArrayList 以下为主要介绍要点,从 Java 8 出发: 一.ArrayList的特点概述 二.ArrayList的内部实现:从内部属性和构造函数说起 三.ArrayLis ...

  6. 集合之ArrayList(含JDK1.8源码分析)

    一.ArrayList的数据结构 ArrayList底层的数据结构就是数组,数组元素类型为Object类型,即可以存放所有类型数据.我们对ArrayList类的实例的所有的操作(增删改查等),其底层都 ...

  7. JAVA面试题 手写ArrayList的实现,在笔试中过关斩将?

    面试官Q1:可以手写一个ArrayList的简单实现吗? 我们都知道ArrayList是基于数组实现,如果让你实现JDK源码ArrayList中add().remove().get()方法,你知道如何 ...

  8. (一)ArrayList集合源码解析

    一.ArrayList的集合特点 问题 结      论 ArrayList是否允许空 允许 ArrayList是否允许重复数据 允许 ArrayList是否有序 有序 ArrayList是否线程安全 ...

  9. 源码分析二(ArrayList与LinkedList的区别)

    一:首先看一下ArrayList类的结构体系: public class ArrayList<E> extends AbstractList<E> implements Lis ...

随机推荐

  1. audio微信自动播放以及自定义样式

    audio标签如下: <audio id="audioTag" src="" autoplay="autoplay" controls ...

  2. Python入门-初识变量类型

    上一篇我们学习了第一行代码,我们print()了很多代码,我们可以print哪些东西呢,这一篇来讲. print()括号里面可以放哪些东西呢?..可以放很多东西,只要是Python的全部数据类型都可以 ...

  3. Go 1.18泛型的局限性初探

    前言 Go 1.18 版本之后正式引入泛型,它被称作类型参数(type parameters),本文初步介绍 Go 中泛型的使用.长期以来 go 都没有泛型的概念,只有接口 interface 偶尔类 ...

  4. 从实例学习 Go 语言、"基础与进阶" 学习笔记及心得体会、Go指南

    第一轮学习 golang "基础与进阶"学习笔记,Go指南练习题目解析.使用学习资料 <Go-zh/tour tour>.记录我认为会比较容易忘记的知识点,进行补充,整 ...

  5. 快速排序算法 - go实现

    在分析redis集群中大Key的时候,通常都采用分析rdb文件的方式:但是这种方式需要在每一台redis服务器上部署分析程序及分析脚本,而像salt之类的工具运维没有开放给我们使用,一台一台部署不好管 ...

  6. SMTP协议解读以及如何使用SMTP协议发送电子邮件

    电子邮件协议中POP3协议用于接收邮件,SMTP协议用于发送邮件.SMTP的全称为Simple Mail Transfer Protocol,也就是简单邮件传输协议,字如其名.   相较于POP3而言 ...

  7. [AcWing 822] 走方格

    点击查看代码 #include<iostream> using namespace std; int n, m, ans = 0; void dfs(int x, int y) { if ...

  8. 虚拟机VMware 安装centos、常规配置、共享文件等

    安装centos7[通过vm来安装运行centos7] 一.准备工作 1.centos7 的安装镜像下载链接:http://isoredirect.centos.org/centos/7/isos/x ...

  9. dfs深搜

    一.01背包dfs //回溯法,01背包 #include<iostream> #include<algorithm> using namespace std; const i ...

  10. 超越OpenCV速度的MorphologyEx函数实现(特别是对于二值图,速度是CV的4倍左右)。

    最近研究了一下opencv的 MorphologyEx这个函数的替代功能, 他主要的特点是支持任意形状的腐蚀膨胀,对于灰度图,速度基本和CV的一致,但是 CV没有针对二值图做特殊处理,因此,这个函数对 ...