ArrayList源码学习
1、ArrayList:基于数据实现,允许出现空值和重复元素,当ArrayList中添加的元素数量大于底层数组容量是,会通过扩容机制重新生成一个更大的数组。(非线程安全)
2、源码分析
构造函数
/**
* 初始化容量
*/
private static final int DEFAULT_CAPACITY = 10;
//有参构造函数时使用,当初始化容量=0时,默认是一个空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//无参构造时默认空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//ArrayList中基础的数组对象
transient Object[] elementData;
//数组大小
private int size;
//指定容量的构造函数
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() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
} //Collection类型的集合 , 集合为空时,ArrayList默认空数组
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;
}
}
插入:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!! 检测是否需要扩容
elementData[size++] = e; //将新增的元素添加到数组的末尾
return true;
}
//在指定索引础添加新的元素
public void add(int index, E element) {
rangeCheckForAdd(index); //检测给出的索引是否合法 ensureCapacityInternal(size + 1); // Increments modCount!! 检测是否需要扩容
//将index和之后的元素整体向后移动一位
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//上一步移动后 index位置的元素和index+1位置的元素相同(index+1位置的元素就是未移动之前的index位置的元素),将新元素插入到index位置
elementData[index] = element;
size++;
} //计算容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
//检测是否需要扩容
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
对于在元素序列尾部插入,这种情况比较简单,只需两个步骤即可:
- 检测数组是否有足够的空间插入
- 将新元素插入至序列尾部
如下图:
如果是在元素序列指定位置(假设该位置合理)插入,则情况稍微复杂一点,需要三个步骤:
- 检测数组是否有足够的空间
- 将 index 及其之后的所有元素向后移一位
- 将新元素插入至 index 处
如下图:
从上图可以看出,将新元素插入至序列指定位置,需要先将该位置及其之后的元素都向后移动一位,为新元素腾出位置。这个操作的时间复杂度为O(N)
,频繁移动元素可能会导致效率问题,特别是集合中元素数量较多时。在日常开发中,若非所需,我们应当尽量避免在大集合中调用第二个插入方法。
扩容:(扩容比例 原数组大小的1.5倍)
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); //原数组大小的1.5倍
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win: 新容量计算出来之后 复制原数组中的元素到新数组中
elementData = Arrays.copyOf(elementData, newCapacity);//方法内部会新建一个newCapacity大小的新数组 然后调用System.arrcopy()方法进行复制
} private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
删除:
//删除指定索引的元素
public E remove(int index) {
rangeCheck(index);//校验index是否合法 modCount++;//该字段存储arraylist的修改次数
E oldValue = elementData(index);//暂存要删除索引位置的元素 int numMoved = size - index - 1;//index位置到数组最后的长度 为了system.arraycopy复制方法使用,将index+1位置和之后的元素 整体向前移动一位
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work 将数组中最后位置置空 return oldValue;
} //删除指定元素 (删除ArrayList中索引最小的指定元素)
public boolean remove(Object o) {
//元素为空的话 循环删除ArrayList中索引最小的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;
} //同remove(index)方法
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
上面的删除方法并不复杂,这里以第一个删除方法为例,删除一个元素步骤如下:
- 获取指定位置 index 处的元素值
- 将 index + 1 及之后的元素向前移动一位
- 将最后一个元素置空,并将 size 值减 1
- 返回被删除值,完成删除操作
如下图:
上面就是删除指定位置元素的分析,并不是很复杂。
特殊情况:
往ArrayList中插入大量的元素,然后又删除很多元素,此时ArrayList底层的数组占用了很多空间没有释放,因为ArrayList没有自动缩容机制,导致底层空间不能被释放,造成浪费
此时调用ArrayList的方法:
//size ArrayList的大小 elementData底层数组的长度
public void trimToSize() {
modCount++;
if (size < elementData.length) {//如果size小于elementData 代表数组中空着很多位置,需要进行缩容
elementData = (size == 0) //size == 0时代表,ArrayList中没有元素,可以将element置为空数组
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
通过上面的方法,我们可以手动触发 ArrayList 的缩容机制。这样就可以释放多余的空间,提高空间利用率。
遍历:
ArrayList 实现了 RandomAccess 接口(该接口是个标志性接口),表明它具有随机访问的能力。ArrayList 底层基于数组实现,所以它可在常数阶的时间内完成随机访问,效率很高。对 ArrayList 进行遍历时,一般情况下,我们喜欢使用 foreach 循环遍历,但这并不是推荐的遍历方式。ArrayList 具有随机访问的能力,如果在一些效率要求比较高的场景下,更推荐下面这种方式:
for (int i = 0; i < list.size(); i++) {
list.get(i);
}
至于原因也不难理解,foreach 最终会被转换成迭代器遍历的形式,效率不如上面的遍历方式
判断ArrayList是否有指定元素:
//有指定元素返回true 也就是indexOf()方法返回值大于等于0
public boolean contains(Object o) {
return indexOf(o) >= 0;
} //类似remove和fastremove方法
public int indexOf(Object o) {
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;
}
return -1;
}
清空ArrayList:
//地层elementData中的元素全部置null size(ArrayList)长度置0
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null; size = 0;
}
其他:
关于遍历时删除
遍历时删除是一个不正确的操作,即使有时候代码不出现异常,但执行逻辑也会出现问题。关于这个问题,阿里巴巴 Java 开发手册里也有所提及。这里引用一下:
【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
相关代码(稍作修改)如下:
List<String> a = new ArrayList<String>();
a.add("1");
a.add("2");
for (String temp : a) {
System.out.println(temp);
if("1".equals(temp)){
a.remove(temp);
}
}
}
相信有些朋友应该看过这个,并且也执行过上面的程序。上面的程序执行起来不会虽不会出现异常,但代码执行逻辑上却有问题,只不过这个问题隐藏的比较深。我们把 temp 变量打印出来,会发现只打印了数字1
,2
没打印出来。初看这个执行结果确实很让人诧异,不明原因。如果死抠上面的代码,我们很难找出原因,此时需要稍微转换一下思路。我们都知道 Java 中的 foreach 是个语法糖,编译成字节码后会被转成用迭代器遍历的方式。所以我们可以把上面的代码转换一下,等价于下面形式:
List<String> a = new ArrayList<>();
a.add("1");
a.add("2");
Iterator<String> it = a.iterator();
while (it.hasNext()) {
String temp = it.next();
System.out.println("temp: " + temp);
if("1".equals(temp)){
a.remove(temp);
}
}
这个时候,我们再去分析一下 ArrayList 的迭代器源码就能找出原因。
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount; public boolean hasNext() {
return cursor != size;
} @SuppressWarnings("unchecked")
public E next() {
// 并发修改检测,检测不通过则抛出异常
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
} final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
} // 省略不相关的代码
}
我们一步一步执行一下上面的代码,第一次进入 while 循环时,一切正常,元素 1 也被删除了。但删除元素 1 后,就无法再进入 while 循环,此时 it.hasNext() 为 false。原因是删除元素 1 后,元素计数器 size = 1,而迭代器中的 cursor 也等于 1,从而导致 it.hasNext() 返回false。归根结底,上面的代码段没抛异常的原因是,循环提前结束,导致 next 方法没有机会抛异常。不信的话,大家可以把代码稍微修改一下,即可发现问题:
List<String> a = new ArrayList<>();
a.add("1");
a.add("2");
a.add("3");
Iterator<String> it = a.iterator();
while (it.hasNext()) {
String temp = it.next();
System.out.println("temp: " + temp);
if("1".equals(temp)){
a.remove(temp);
}
}
以上是关于遍历时删除的分析,在日常开发中,我们要避免上面的做法。正确的做法使用迭代器提供的删除方法,而不是直接删除。
本文内容引自:该博客 建议读原博客
ArrayList源码学习的更多相关文章
- ArrayList源码学习----JDK1.7
什么是ArrayList? ArrayList是存储一组数据的集合,底层也是基于数组的方式实现,实际上也是对数组元素的增删改查:它的主要特点是: 有序:(基于数组实现) 随机访问速度快:(进行随机访问 ...
- Java API学习(一) ArrayList源码学习
ArrayList在平常用的还挺多的,用起来十分舒服,顺手.这里来学习一下它的源码. 类定义 下面是类的定义: public class ArrayList<E> extends Abst ...
- 由JDK源码学习ArrayList
ArrayList是实现了List接口的动态数组.与java中的数组相比,它的容量能动态增长.ArrayList的三大特点: ① 底层采用数组结构 ② 有序 ③ 非同步 下面我们从ArrayList的 ...
- 集合框架源码学习之ArrayList
目录: 0-0-1. 前言 0-0-2. 集合框架知识回顾 0-0-3. ArrayList简介 0-0-4. ArrayList核心源码 0-0-5. ArrayList源码剖析 0-0-6. Ar ...
- 从JDK源码学习Arraylist
从今天开始从源码去学习一些Java的常用数据结构,打好基础:) Arraylist源码阅读: jdk版本:1.8.0 首先看其构造方法: 构造方法一: 第一种支持初始化容量大小,其中声明一个对象数组, ...
- JDK1.8源码学习-ArrayList
JDK1.8源码学习-ArrayList 目录 一.ArrayList简介 为了弥补普通数组无法自动扩容的不足,Java提供了集合类,其中ArrayList对数组进行了封装,使其可以自动的扩容或缩小长 ...
- [数据结构1.2-线性表] 动态数组ArrayList(.NET源码学习)
[数据结构1.2-线性表] 动态数组ArrayList(.NET源码学习) 在C#中,存在常见的九种集合类型:动态数组ArrayList.列表List.排序列表SortedList.哈希表HashTa ...
- java源码学习(四)ArrayList
ArrayList ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下, ...
- JDK源码学习笔记——ArrayList/Vector
一.类定义 public class ArrayList<E> extends AbstractList<E> implements List<E>, Random ...
随机推荐
- 用标准3层神经网络实现MNIST识别
一.MINIST数据集下载 1.https://pjreddie.com/projects/mnist-in-csv/ 此网站提供了mnist_train.csv和mnist_test.cs ...
- AttributeError: 'NoneType' object has no attribute 'split' 报错处理
报错场景 social_django 组件对原生 django 的支持较好, 但是因为 在此DRF进行的验证为 JWT 方式 和 django 的验证存在区别, 因此需要进行更改自行支持 JWT 方式 ...
- python在windows(双版本)及linux环境下安装
python下载 下载地址:https://www.python.org/downloads/ 可以下载需要的版本,这里选择2.7.12和3.6.2 下面第一个是linux版本,第二个是windows ...
- Spring Bean初始化之后执行指定方法
转: Spring Bean初始化之后执行指定方法 2017年07月31日 15:59:33 vircens 阅读数:24807 Spring Bean初始化之后执行指定方法 在运用Spring进 ...
- django 2.接口之工作原理
1.创建应用程序有两种方法,第一种就是在新建项目的时候,在最初使的时填入应用程序名称,第二种就是进入目录下面,输入 python manage.py startapp appName 就会自动生成一个 ...
- python 正则指北之我的总结
本文经本人搜索网络加上个人理解整理而成,如有侵权,请告知,会立即删除! 正则引擎大体上可分为不同的两类:DFA和NFA,而NFA又基本上可以分为传统型NFA和POSIX NFA. DFA Determ ...
- 基于jeesite的cms系统(一):开发环境搭建
基于jeesite的cms系统系列,是对基于jeesite进行二次开发的博客模块开发过程的总结.涉及入门安装,二次开发,部署等 一.概况: JeeSite 是一个 Java 企业级快速开发平台,基于经 ...
- Knockoutjs 响应式计算研究
reactive programming https://en.wikipedia.org/wiki/Reactive_programming In computing, reactive progr ...
- Mybatis-Plus入门
1 Mybatis-Plus简介 1.1 什么是Mybatis-Plus MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化 ...
- winform 以不规则图形背景显示窗体
一:创建一个winform窗体,把BackgroundImage引入一个不规则的图片,设置属性BackgroundImageLayout为Stretch 二:主要代码 using System; us ...