ArrayList源码解析(空间结构和特性、常用方法、遍历时删除元素注意事项等)

本文主要是介绍ArrayList源码解析(空间结构和特性、常用方法、遍历时删除元素注意事项等),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1、概念

ArrayList继承了AbstractList,实现了 List ,RandomAccess, Cloneable, Serializable接口,所以他有List的相关功能同时还有动态随机访问、复制和序列化等功能。他的底层是使用数组实现的,所以查询起来相对较快,而插入删除时相对较慢。由于ArrayList里面的方法没有使用synchronized修饰,所以不是线程安全的。

2、空间结构

对象序列化的UID,类序列化时会传入一个serialVersionUID。在反序列化时,JVM会把传来的字节流中的serialVersionUID于本地相应实体类的serialVersionUID进行比较。如果相同说明是一致的,可以进行反序列化,否则会出现反序列化版本一致的异常,即是InvalidCastException。

private static final long serialVersionUID = 8683452581122892189L;

默认的初始容量大小为10。

private static final int DEFAULT_CAPACITY = 10;

一个空的数组实例,如果传进来的size是空,则使用这个空数组

private static final Object[] EMPTY_ELEMENTDATA = {};

默认大小为空的数组实例,如果没传进来初始化size就是用这个空数组。

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

用来装ArrayList元素的数组,所以他底层是使用数组实现。

transient Object[] elementData

集合实际的元素个数,与容量不同。

private int size;

计数器,用于记录对List的修改次数,这里涉及到一个fail-fast快速失败机制,单线程时迭代器遍历集合中remove元素和多线程中一个线程遍历另一个线程remove元素时都会抛出ConcurrentModificationException。因为迭代器遍历时候,内部会将modCount赋给expectedModCount ,当集合结构改变时,modCount会被修改,迭代器每次next()和hahNext()方法都会检查expectedModCount与modCount是否相等,被改变时,抛出ConcurrentModificationException。所以在对集合遍历操作时,建议大家在迭代器中增删元素,不要直接操作集合的remove。引出fail-safe是快速安全机制,在多线程中使用(ConcurrentHashMap中)。任何对集合结构的修改都会在一个复制的集合上进行,因此不会抛出ConcurrentModificationException,但无法保证最终读取的数据是正确的元素,且需要复制集合,开销大。(这个modCount方法中都在用,翻了一会子类父类源码没发现,然后藏在601行)

protected transient int modCount = 0;

三个构造方法:

  1. 初始化一个指定容量的构造器,直接指定elementData数组的容量,如果容量<0,默认还是用空的实例。
  2. 初始化一个空的数组,在添加元素时候再对数组elementData扩容。
  3. 初始化一个集合参数的数组,把集合通过Arrays.copyOf拷到elementData中,容量和指定集合容量相同,如果容量<0,默认还是用空的实例。
    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;}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;}}

3、常用方法

扩容

大概增加元素时候先判断是否要扩容,然后再判断扩容的大小,默认是扩容1.5倍,但是如果新的长度扩容1.5倍也不能容纳或者扩容得太大,就扩容到需要的容量。

	//其他方法先调用此方法看看是否需要扩容private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}//如果是初始化时没有指定大小,然后调用add方法,默认返回默认大小和当前size+1的最大值private static int calculateCapacity(Object[] elementData, int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;}//如果添加后的实际容量大于当前容量则需要调用grow方法扩容private void ensureExplicitCapacity(int minCapacity) {modCount++;if (minCapacity - elementData.length > 0)grow(minCapacity);}private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;//扩容1.5倍后的容量和传入的容量取大值进行扩容,如果新的容量特别大时给Interger最大值或者Integer.MAX_VALUE - 8(搞不懂为啥是-8这样设计)private void grow(int minCapacity) {int oldCapacity = elementData.length;int newCapacity = oldCapacity + (oldCapacity >> 1);if (newCapacity - minCapacity < 0)newCapacity = minCapacity;if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);elementData = Arrays.copyOf(elementData, newCapacity);}//判断容量大时的情况private static int hugeCapacity(int minCapacity) {if (minCapacity < 0) throw new OutOfMemoryError();return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;}//这个方法是专为为DenseIntMapImpl类的extend方法提供。public void ensureCapacity(int minCapacity) {int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)table? 0: DEFAULT_CAPACITY;if (minCapacity > minExpand) {ensureExplicitCapacity(minCapacity);}}

数组元素数量

    public int size() {return size;}

数组元素数量是否为0

    public boolean isEmpty() {return size == 0;}

添加元素
传入元素,先判断扩容,然后添加到数组中。

    public boolean add(E e) {ensureCapacityInternal(size + 1); elementData[size++] = e;return true;}

传入index和元素,先判断index在数组中是否存在,然后判断扩容,然后将原数组复制一份,在中间index位置加入element。

    public void add(int index, E element) {rangeCheckForAdd(index);ensureCapacityInternal(size + 1);  //复制一个数组,把index后得一段整体后移一位。System.arraycopy(elementData, index, elementData, index + 1,size - index);elementData[index] = element;size++;}/**
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
代码解释:Object src : 原数组int srcPos : 从元数据的起始位置开始Object dest : 目标数组int destPos : 目标数组的开始起始位置int length  : 要copy的数组的长度
*/

传入集合,先判断容量,然后复制一个数组,把这个集合整体加到原集合后。

    public boolean addAll(Collection<? extends E> c) {Object[] a = c.toArray();int numNew = a.length;ensureCapacityInternal(size + numNew);  // Increments modCountSystem.arraycopy(a, 0, elementData, size, numNew);size += numNew;return numNew != 0;}

获取元素
先判断index是否存在,然后返回数组对应下标,可以看出直接从数组随机访问,速度比较快。

    public E get(int index) {rangeCheck(index);return elementData(index);}

获取元素索引
返回给定元素第一次出现的位置,没有则返回-1。

    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;}

返回给定元素最后一次出现的位置,没有则返回-1。

    public int lastIndexOf(Object o) {if (o == null) {for (int i = size-1; i >= 0; i--)if (elementData[i]==null)return i;} else {for (int i = size-1; i >= 0; i--)if (o.equals(elementData[i]))return i;}return -1;}

是否包含指定元素
使用indexOf判断是否包含元素

    public boolean contains(Object o) {return indexOf(o) >= 0;}

删除元素
传入index,先判断index是否存在,然后判断是否是最后一个元素,如果不是就重新拷贝一个数组,将index后的数组元素都向前移动一个位置,然后把数组得最后一位内存释放出来。

    public E remove(int index) {rangeCheck(index);modCount++;E oldValue = elementData(index);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 workreturn oldValue;}

传入对象,如果o为空就,就删除所有null的元素,否则删除下标为index的元素。

    public boolean remove(Object o) {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方法类似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}

传入索引的一段范围,删除这段范围内的所有元素。

    protected void removeRange(int fromIndex, int toIndex) {modCount++;int numMoved = size - toIndex;System.arraycopy(elementData, toIndex, elementData, fromIndex,numMoved);// clear to let GC do its workint newSize = size - (toIndex-fromIndex);for (int i = newSize; i < size; i++) {elementData[i] = null;}size = newSize;}

删除指定元素removeAll和保留指定元素retainAll,都调用的batchRemove函数,通过传入true或者false执行不同的功能(JDK源码有点子巧妙~)。

	//删除集合中所有在集合c中出现过的元素。public boolean removeAll(Collection<?> c) {Objects.requireNonNull(c);return batchRemove(c, false);}//删除集合中所有没有在c集合中出现过的元素。public boolean retainAll(Collection<?> c) {Objects.requireNonNull(c);return batchRemove(c, true);}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 {//正常情况下r=size是成立的,这里稍微有点不明白,自己理解大概就是出异常的时候将未判断的直接赋值给数组,凑齐数组长度,让w==size,好让后续if (w != size) 不执行,返回false,if (r != size) {System.arraycopy(elementData, r,elementData, w,size - r);w += size - r;}//如果方法最终w=size,证明没有删除过元素或者出现异常执行了if (r != size),所以返回false并释放内存if (w != size) {// clear to let GC do its workfor (int i = w; i < size; i++)elementData[i] = null;modCount += size - w;size = w;modified = true;}}return modified;}

复制集合
调用Arrays.copyOf方法进行元素复制。

    public Object clone() {try {ArrayList<?> v = (ArrayList<?>) super.clone();v.elementData = Arrays.copyOf(elementData, size);v.modCount = 0;return v;} catch (CloneNotSupportedException e) {// this shouldn't happen, since we are Cloneablethrow new InternalError(e);}}

将集合转化为数组

	public Object[] toArray() {return Arrays.copyOf(elementData, size);}

清空集合
把所有值赋为null让gc回收,并把size变为0。

    public void clear() {modCount++;// clear to let GC do its workfor (int i = 0; i < size; i++)elementData[i] = null;size = 0;}

序列化和反序列化
通过writeObject方法和readObject方法来遍历elementData数组把数组中的元素写入ObjectOutputStream ,ObjectInputStream 中的。因为ArrayList实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为100,而实际只放了一个元素,那就会序列化99个null元素。为了保证在序列化的时候不会将这么多null同时进行序列化,ArrayList把元素数组设置为transient。

    private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException{int expectedModCount = modCount;s.defaultWriteObject();s.writeInt(size);for (int i=0; i<size; i++) {s.writeObject(elementData[i]);}if (modCount != expectedModCount) {throw new ConcurrentModificationException();}}private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException {elementData = EMPTY_ELEMENTDATA;s.defaultReadObject();s.readInt(); if (size > 0) {int capacity = calculateCapacity(elementData, size);SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);ensureCapacityInternal(size);Object[] a = elementData;for (int i=0; i<size; i++) {a[i] = s.readObject();}}}

迭代器
ArrayList的listIterator和iterator实际上是返回ListItr和Itr对象,他是ArrayList通过内部类的方式来实现的。listIterator有previous方法支持前后遍历、迭代过程添加元素等操作。for-each就是Java提供的语法糖,实际上也是通过iterator遍历元素。
    在用 for 遍历集合的时候是不可以对集合进行 remove操作的,因为 remove 操作会改变集合的大小。从而容易造成结果不准确甚至数组下标越界,更严重者还会抛出 ConcurrentModificationException。因为在创建迭代器时候会有这么一句代码:

int expectedModCount = modCount;

每当操作集合时,modCount都会自增,而每次iterator调用next方法时都会调用一下代码:

    final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException();}

判断expectedModCount和modCount是否相等,不相等所以抛出ConcurrentModificationException异常。而iterator自带的remove方法每次都会将modCount重新赋给expectedModCount,所以遍历时删除元素需要使用集合的iterator的remove,以下是ListItr 和Itr的源码:

    public ListIterator<E> listIterator(int index) {if (index < 0 || index > size)throw new IndexOutOfBoundsException("Index: "+index);return new ListItr(index);}public ListIterator<E> listIterator() {return new ListItr(0);}public Iterator<E> iterator() {return new Itr();}private class Itr implements Iterator<E> {int cursor;     int lastRet = -1; int expectedModCount = modCount;Itr() {}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];}public void remove() {if (lastRet < 0)throw new IllegalStateException();checkForComodification();try {ArrayList.this.remove(lastRet);cursor = lastRet;lastRet = -1;expectedModCount = modCount;} catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException();}}@Override@SuppressWarnings("unchecked")public void forEachRemaining(Consumer<? super E> consumer) {Objects.requireNonNull(consumer);final int size = ArrayList.this.size;int i = cursor;if (i >= size) {return;}final Object[] elementData = ArrayList.this.elementData;if (i >= elementData.length) {throw new ConcurrentModificationException();}while (i != size && modCount == expectedModCount) {consumer.accept((E) elementData[i++]);}// update once at end of iteration to reduce heap write trafficcursor = i;lastRet = i - 1;checkForComodification();}final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException();}}private class ListItr extends Itr implements ListIterator<E> {ListItr(int index) {super();cursor = index;}public boolean hasPrevious() {return cursor != 0;}public int nextIndex() {return cursor;}public int previousIndex() {return cursor - 1;}@SuppressWarnings("unchecked")public E previous() {checkForComodification();int i = cursor - 1;if (i < 0)throw new NoSuchElementException();Object[] elementData = ArrayList.this.elementData;if (i >= elementData.length)throw new ConcurrentModificationException();cursor = i;return (E) elementData[lastRet = i];}public void set(E e) {if (lastRet < 0)throw new IllegalStateException();checkForComodification();try {ArrayList.this.set(lastRet, e);} catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException();}}public void add(E e) {checkForComodification();try {int i = cursor;ArrayList.this.add(i, e);cursor = i + 1;lastRet = -1;expectedModCount = modCount;} catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException();}}}

4、总结

  1. ArrayList 底层基于数组实现的,所以插入或删除元素时,需要移动插入位置之后的所有元素,开销较大,所以适合随机查询,不适合增删多的操作。
  2. ArrayList初始容量默认为10。扩容机制为首先扩容为原始容量的 1.5 倍再与当前size+1取较大值成为新的数组长度。由于扩容是通过Arrays.copyOf每次创建新的数组,所以创建ArrayList时候尽量传入合适的容量,减小内存开销。
  3. ArrayList不是线程安全的,所以有modCount的存在,直接修改集合时都会使modCount++,所以遍历ArrayList时删除元素时使用iterator的remove方法,该方法会同步modCount值,避免出现ConcurrentModificationException异常。

这篇关于ArrayList源码解析(空间结构和特性、常用方法、遍历时删除元素注意事项等)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/826941

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

JS常用组件收集

收集了一些平时遇到的前端比较优秀的组件,方便以后开发的时候查找!!! 函数工具: Lodash 页面固定: stickUp、jQuery.Pin 轮播: unslider、swiper 开关: switch 复选框: icheck 气泡: grumble 隐藏元素: Headroom

电脑桌面文件删除了怎么找回来?别急,快速恢复攻略在此

在日常使用电脑的过程中,我们经常会遇到这样的情况:一不小心,桌面上的某个重要文件被删除了。这时,大多数人可能会感到惊慌失措,不知所措。 其实,不必过于担心,因为有很多方法可以帮助我们找回被删除的桌面文件。下面,就让我们一起来了解一下这些恢复桌面文件的方法吧。 一、使用撤销操作 如果我们刚刚删除了桌面上的文件,并且还没有进行其他操作,那么可以尝试使用撤销操作来恢复文件。在键盘上同时按下“C

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

常用的jdk下载地址

jdk下载地址 安装方式可以看之前的博客: mac安装jdk oracle 版本:https://www.oracle.com/java/technologies/downloads/ Eclipse Temurin版本:https://adoptium.net/zh-CN/temurin/releases/ 阿里版本: github:https://github.com/

浅谈主机加固,六种有效的主机加固方法

在数字化时代,数据的价值不言而喻,但随之而来的安全威胁也日益严峻。从勒索病毒到内部泄露,企业的数据安全面临着前所未有的挑战。为了应对这些挑战,一种全新的主机加固解决方案应运而生。 MCK主机加固解决方案,采用先进的安全容器中间件技术,构建起一套内核级的纵深立体防护体系。这一体系突破了传统安全防护的局限,即使在管理员权限被恶意利用的情况下,也能确保服务器的安全稳定运行。 普适主机加固措施:

30常用 Maven 命令

Maven 是一个强大的项目管理和构建工具,它广泛用于 Java 项目的依赖管理、构建流程和插件集成。Maven 的命令行工具提供了大量的命令来帮助开发人员管理项目的生命周期、依赖和插件。以下是 常用 Maven 命令的使用场景及其详细解释。 1. mvn clean 使用场景:清理项目的生成目录,通常用于删除项目中自动生成的文件(如 target/ 目录)。共性规律:清理操作

webm怎么转换成mp4?这几种方法超多人在用!

webm怎么转换成mp4?WebM作为一种新兴的视频编码格式,近年来逐渐进入大众视野,其背后承载着诸多优势,但同时也伴随着不容忽视的局限性,首要挑战在于其兼容性边界,尽管WebM已广泛适应于众多网站与软件平台,但在特定应用环境或老旧设备上,其兼容难题依旧凸显,为用户体验带来不便,再者,WebM格式的非普适性也体现在编辑流程上,由于它并非行业内的通用标准,编辑过程中可能会遭遇格式不兼容的障碍,导致操