概述

ArrayList底层是基于数组实现的,并且支持动态扩容的动态数组(变长的集合类)。ArrayList允许空值和重复的元素,当向ArrayList中添加元素数量大于其底层数组容量时,会通过扩容机制重新生成一个容量更大的数组。另外,由于ArrayList底层数据结构是数组,所以保证了在O(1)复杂度下完成随机查找操作。ArrayList是非线程安全的,在并发环境下,多个线程同时操作ArrayList会引发不可预知的错误。

从上面的类图可以看出,ArrayList实现了4个接口和继承了1个抽象类,分别是:

  • List接口:主要提供了数组的添加、删除、修改、迭代遍历等操作;
  • Cloneable接口:标识克隆操作;
  • Serializable接口:标识可序列列化操作;
  • RandomAccess接口:标识可随机访问操作;
  • 继承AbstractList抽象类,主要提供迭代遍历相关操作。

属性

ArrayList的属性比较少,只有两个属性:elementData和size。

1 private static final int DEFAULT_CAPACITY = 10;
2
3 private static final Object[] EMPTY_ELEMENTDATA = {};
4
5 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
6 // 底层存放元素的数组
7 transient Object[] elementData;
8 // elementData数组中元素的数量
9 private int size;

构造方法

ArrayList有3个构造方法,分别如下:

  • ArrayList(int initialCapcity):根据传入的initialCapacity初始化容量来创建elementData数组;
  • ArrayList():无参构造方法;
  • ArrayList(Collection<? extends E> c):使用传入的集合C作为ArrayList的elementData;
// 1. ArrayList(int initialCapacity)构造方法
// 需要注意:当初始化容量initialCapacity为0时,使用EMPTY_ELEMENTDATA对象创建一个空数组,在添加元素的时候,会进行扩容创建需要的数组
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
       // 初始化容量大于0,创建Object数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
       // 初始化容量为0时,使用EMPTY_ELEMENTDATA对象
this.elementData = EMPTY_ELEMENTDATA;
} else {
       // 参数异常
throw new IllegalArgumentException("Illegal Capacity:" + initialCapacity);
}
} // 2. ArrayList()无参构造方法
public ArrayList() {
     // 实际创建的时候是空数组,在首次添加元素的时候,才会初始化容量为10的数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
} // 3. ArrayList(Collection<? extends E> c)构造方法
public ArrayList(Collection<? extends E> c) {
     // elementData指向c.toArray()
elementData = c.toArray();
if ((size = elementData.length) != 0) {
       // 如果集合元素的类型不是Object.class类型,则拷贝成Object.class类型
// 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插入方法有4个:

  • public boolean add(E e):在数组尾部插入元素;
  • public void add(int index, E element):在数组指定index位置插入元素;
  • public boolean addAll(Collection<? extends E> c):在数组尾部插入多个元素;
  • public boolean addAll(int index, Collection<? extends E> c):在数组指定index位置插入多个元素;
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

主要关注ensureCapacityInternal方法具体做了什么事情:现有的数组容量是否支持新增元素的需求,如果不满足,则考虑扩容操作

通过底层源码,我们看看JDK的设计者是如何考虑的:

STEP-1:首先是ensureCapacityInternal(int minCapacity)方法,该方法的主要功能是 确保数组内部容量足以满足本次插入操作

该方法实际是首先调用calculateCapacity(Object[] elementData, int minCapacity)方法,计算满足本次插入操作所需要的容量,然后调用ensureExplicitCapacity(int minCapacity)方法,保证容量足够

/**
* 确保数组容量足够添加元素
*
* @param minCapacity 最小容量
*/
private void ensureCapacityInternal(int minCapacity) {
// 计算容量
int capacity = calculateCapacity(elementData, minCapacity);
// 确保容量足够 -- 如不足够则触发扩容
ensureExplicitCapacity(capacity);
}

STEP-2:考量calculateCapacity(Object[] elementData, int minCapacity)方法是如何计算本次插入操作所需容量的。

如果elementData数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA:没有指定初始化容量时;则会判断是最小容量minCapacity和DEFAULT_CAPACITY的大小,取两者中较大者。

/**
* 计算容量
* @param elementData 元素数组
* @param minCapacity 最小容量
* @return int 计算后的数组容量大小
*/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果是DEFAULT_CAPACITY ,那么就会从10开
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// DEFAULT_CAPACITY 定义:private static final int DEFAULT_CAPACITY = 10;
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 如果不是则直接返回需要的最小容量
return minCapacity;
}

STEP-3:考量ensureExplicitCapacity 如何确保数组容量足够本次新增操作的:当所需的最小容量minCapacity大于elementData.length数组长度时,则触发grow()执行扩容。

/**
* 确保数组剩余容量足够
* @param minCapacity 最小容量
*/
private void ensureExplicitCapacity(int minCapacity) {
// 在父类 AbstractList中定义了 用以记录数组修改的次数(注意是次数)
modCount++; // minCapacity代表的是本次(新增)操作所需要的最小容量, elementData.length代表的是当前元素数组的长度
// 可能写成 minCapacity > elementData.length 更好理解
if (minCapacity - elementData.length > 0) {
// 扩容可指定最小的容量(满足当前需求)
grow(minCapacity);
}
}
/**
* 扩容机制
* 旧容量经过运算扩展为1.5倍后与最小容量minCapacity进行比较
* 如果大于则采用旧容量扩展1.5倍后的大小,否则采用最小容量minCapacity
* @param minCapacity 新增操作所需最小容量
*/
private void grow(int minCapacity) {
// 旧容量大小 elementData数组的长度
int oldCapacity = elementData.length;
// 新容量大小 == 旧 + 旧的位运算 (向右移动1位) 大致上是原大小的1.5倍
// ArrayList扩容机制
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果计算出来的新容量小于 指定的最小容量,则创建指定的最小容量大小的新数组
// 可理解为 newCapacity < minCapacity 不知道为啥会喜欢写成减法的形式。。。
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
// 如果新容量大于最大值(会出现内存不足的情况)
// private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
if (newCapacity - MAX_ARRAY_SIZE > 0) {
// hugeCapicaty方法用以当 newCapacity超过指定范围的时候,确保创建的数组不会溢出
newCapacity = hugeCapacity(minCapacity);
}
// 数组拷贝,将原有的数据搬到新的数组上
elementData = Arrays.copyOf(elementData, newCapacity);
}

综述:经过上述过程的研讨,我们知道ArrayList插入操作的整个过程:首先计算插入元素所需的最小容量;然后判断当前elementData数组是否支持本次插入操作,当容量不足时则触发扩容机制,将原有数组的元素拷贝到新数组上,然后往后追加新的元素。【备注,其他3个插入操作过程基本类似,可自行分析源码】

查找元素

public int indexOf(Object o):查找首个为指定元素o的位置,源码如下:

/**
* 查找首个为指定元素 o 的位置
* @param o 被查找元素
* @return int 如果存在则返回对应index 不存在返回-1
*/
@Override
public int indexOf(Object o) {
// 从这里我们其实也可以看出,ArrayList是支持存储null值的
if (o == null) {
for (int i = 0; i < size; i++) {
if (elementData[i]==null) {
return i;
}
}
} else {
for (int i = 0; i < size; i++) {
if (o.equals(elementData[i])) {
return i;
}
}
}
// 不存在则返回 -1
return -1;
}

删除元素

删除元素的方法主要有4个,分别如下:

  • public E remove(int index):移除指定位置的元素,并返回该位置的原元素;
  • public boolean remove(Object o):移除首个为o的元素,并返回是否移除成功;
  • protected void removeRange(int fromIndex, int toIndex):批量移除[fromIndex, toIndex)内的多个元素,注意包左不含右
  • public boolean removeAll(Collection<?> c):批量移除指定的多个元素;

public E remove(int index) 继承于 AbstractList 抽象类,删除指定位置的元素,并返回该位置上的原元素。

/**
* 删除指定位置index的元素,并返回该元素
* @param index
* @return E
*/
public E remove(int index) {
// index 合法性校验,不合法则抛出相关异常
rangeCheck(index);
// 修改数组的次数 + 1
modCount++;
// 获取index下标对应的value,elementData方法其实就是 return (E) elementData[index]
E oldValue = elementData(index); // 每删除一个元素,都需要对原有的数组进行移动,所以从这里也可以看出ArrayList并不是特别适合于删除操作比较多的场景~
// 需要移动的元素的数量
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
// 数组的最后一个位置设置为null 帮助GC
elementData[--size] = null; return oldValue;
} private void rangeCheck(int index) {
// index 合法性校验
if (index >= size) {
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
}

【注意】:删除源码是比较简单易读的,注意一点:凡是涉及到index的操作首先需要参数合法性检查,然后再获取index对应下标的元素,再对index位置后的元素向前移动,最终返回被删除的元素值。

转换成数组

  • public Object[] toArray():将ArrayList转换为Object[]数组;
  • public <T> T[] toArray(T[] a):将ArrayList转换为指定T泛型的数组;
/**
* 将ArrayList转换成Object类型数组
* @return Object[]
*/
@Override
public Object[] toArray() {
// 返回的是Object[] 类型,需要注意;转换成数组就相当于是将 ArrayList的底层elementData暴露出去而已
return Arrays.copyOf(elementData, size);
} public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
/**
* 将ArrayList转换成指定泛型的数组
* @param a
* @return T[]
*/
@Override
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
// 《1》如果传入的数组小于 size 的大小,直接拷贝一个新的数组返回
if (a.length < size) {
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
}
// 《2》否则直接拷贝数组即可
System.arraycopy(elementData, 0, a, 0, size);
// 额 这个的目的是? 有点没看懂
if (a.length > size) {
a[size] = null;
}
// 返回传入的a,但是考虑到《1》的时候会返回新的数组,所以即时《2》返回a数组,最好还是按照 a = list.toArray(a); 来使用
return a;
}

【注意】:我们在调用toArray()方法时,可能会遇到异常java.lang.ClassCastException:这是因为toArray()方法返回的类型是Object[],如果我们将其转换为其他类型,可能排除异常。这是因为Java并不支持向下转型,所以当我们需要将ArrayList转换成数组并且指定类型的时候,应该使用指定泛型的方法。

其他方法

/**
* 判断集合中是否含有元素 o
* 可以看出,contains方法其实就是依赖的 indexOf方法
* @param o
* @return boolean
*/
@Override
public boolean contains(Object o) {
return indexOf(o) >= 0;
} /**
* 获取指定index位的元素
* @param index
* @return E
*/
@Override
public E get(int index) {
// 参数检查
rangeCheck(index); // 相当于直接返回 elementData[index]
return elementData(index);
} /**
* 设置指定index位置的元素,并且返回该index位置旧元素值
* 并返回旧元素的值(相当于替换)
* @param index
* @param element
* @return E
*/
@Override
public E set(int index, E element) {
// 参数检查
rangeCheck(index); E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
} /**
* 清空集合
*/
@Override
public void clear() {
// 数组操作次数 + 1,虽然不知道记录这个东东有啥子用
modCount++;
// 遍历数组,全部设置为null,同时更新size值
for (int i = 0; i < size; i++) {
elementData[i] = null;
} size = 0;
} /**
* 创建子数组
* 注意subList并不是只读数组,而是和父数组共享相同的 elementData 的数组,换句话说,对subList的操作会影响到 父数组
* 只不过是fromIndex 和 toIndex限制了查看的范围
* @param fromIndex 开始下标
* @param toIndex 结束下标
* 是一个 [fromIndex, toIndex)的效果
*/
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}

总结

本文从源码的角度,主要总结了ArrayList的属性、构造方法、核心方法。有不准确之处,望指正!!!

ArrayList源码分析(JDK1.8)的更多相关文章

  1. ArrayList源码分析--jdk1.8

    ArrayList概述   1. ArrayList是可以动态扩容和动态删除冗余容量的索引序列,基于数组实现的集合.  2. ArrayList支持随机访问.克隆.序列化,元素有序且可以重复.  3. ...

  2. Java基础 ArrayList源码分析 JDK1.8

    一.概述 本篇文章记录通过阅读JDK1.8 ArrayList源码,结合自身理解分析其实现原理. ArrayList容器类的使用频率十分频繁,它具有以下特性: 其本质是一个数组,因此它是有序集合 通过 ...

  3. ReentrantLock源码分析--jdk1.8

    JDK1.8 ArrayList源码分析--jdk1.8LinkedList源码分析--jdk1.8HashMap源码分析--jdk1.8AQS源码分析--jdk1.8ReentrantLock源码分 ...

  4. java集合系列之ArrayList源码分析

    java集合系列之ArrayList源码分析(基于jdk1.8) ArrayList简介 ArrayList时List接口的一个非常重要的实现子类,它的底层是通过动态数组实现的,因此它具备查询速度快, ...

  5. Java集合干货——ArrayList源码分析

    ArrayList源码分析 前言 在之前的文章中我们提到过ArrayList,ArrayList可以说是每一个学java的人使用最多最熟练的集合了,但是知其然不知其所以然.关于ArrayList的具体 ...

  6. ArrayList 源码分析

    ArrayList 源码分析 1. 结构   首先我们需要对 ArrayList 有一个大致的了解就从结构来看看吧. 1. 继承   该类继承自 AbstractList 这个比较好说 2. 实现 这 ...

  7. ArrayList源码学习----JDK1.7

    什么是ArrayList? ArrayList是存储一组数据的集合,底层也是基于数组的方式实现,实际上也是对数组元素的增删改查:它的主要特点是: 有序:(基于数组实现) 随机访问速度快:(进行随机访问 ...

  8. ArrayList源码分析超详细

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

  9. Java - ArrayList源码分析

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

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

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

随机推荐

  1. hive 将hive表数据查询出来转为json对象和json数组输出

    一.将hive表数据查询出来转为json对象输出 1.将查询出来的数据转为一行一行,并指定分割符的数据 2.使用UDF函数,将每一行数据作为string传入UDF函数中转换为json再返回 1.准备数 ...

  2. 文档驱动开发模式在 AIMS 中的应用与实践

    摘要:程序员常会说:我最讨厌别人写的代码没有文档,我也最讨厌自己需要写文档. 有一个很老的梗: 我最讨厌别人写的代码没有文档,我也最讨厌自己需要写文档. 有这种想法的程序员应该算是一个老鸟了,对于大多 ...

  3. 首秀:Markdown学习笔记

    # Markdown学习 ## 标题 ### 三级标题 #### 四级标题 ## 字体 **Hello,World!** *Hello,World!* ***Hello,World!*** ~~Hel ...

  4. ios iphone 崩溃字符记录

    如题,近日iphone被爆出有一串字符可引发系统错误 (بٍٍٍٍََُُُِّّّْرٍٍٍٍََُُِِّّّْآٍٍٍَُّ بٍٍٍٍََُُُِّّّْرٍٍٍٍََُُِِّّّْآٍٍٍ ...

  5. 关于.NET中的控制反转(三)- 依赖注入之 Autofac

    一.Autofac简介 Autofac和其他容器的不同之处是它和C#语言的结合非常紧密,在使用过程中对你的应用的侵入性几乎为零,更容易与第三方的组件集成.Autofac的主要特性如下: 组件侵入性为零 ...

  6. 【JDBC核心】commons-dbutils

    commons-dbutils 简介 commons-dbutils 是 Apache 组织提供的一个开源 JDBC 工具类库,它是对 JDBC 的简单封装,学习成本极低,并且使用 commons-d ...

  7. 【MyBatis】MyBatis 缓存

    MyBatis 缓存 文章源码 什么是缓存 像大多数的持久化框架一样,MyBatis 也提供了缓存策略,通过缓存策略来减少数据库的查询次数,从而提高性能. Mybatis 中缓存分为一级缓存,二级缓存 ...

  8. Java API 操作HBase Shell

    HBase Shell API 操作 创建工程 本实验的环境实在ubuntu18.04下完成,首先在改虚拟机中安装开发工具eclipse. 然后创建Java项目名字叫hbase-test 配置运行环境 ...

  9. iostat的输出

    第一行显示的时子系统启动以来的平均值,接下来的报告显示了增量的平均值,每个设备一行 Device:         rrqm/s   wrqm/s     r/s     w/s   rsec/s   ...

  10. LeetCode559.N 叉树的最大深度

    题目 法一.层序遍历 1 class Solution { 2 public: 3 int maxDepth(Node* root) { 4 if(root== NULL) return 0; 5 q ...