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

相关文章

Python的time模块一些常用功能(各种与时间相关的函数)

《Python的time模块一些常用功能(各种与时间相关的函数)》Python的time模块提供了各种与时间相关的函数,包括获取当前时间、处理时间间隔、执行时间测量等,:本文主要介绍Python的... 目录1. 获取当前时间2. 时间格式化3. 延时执行4. 时间戳运算5. 计算代码执行时间6. 转换为指

Windows 上如果忘记了 MySQL 密码 重置密码的两种方法

《Windows上如果忘记了MySQL密码重置密码的两种方法》:本文主要介绍Windows上如果忘记了MySQL密码重置密码的两种方法,本文通过两种方法结合实例代码给大家介绍的非常详细,感... 目录方法 1:以跳过权限验证模式启动 mysql 并重置密码方法 2:使用 my.ini 文件的临时配置在 Wi

MySQL重复数据处理的七种高效方法

《MySQL重复数据处理的七种高效方法》你是不是也曾遇到过这样的烦恼:明明系统测试时一切正常,上线后却频频出现重复数据,大批量导数据时,总有那么几条不听话的记录导致整个事务莫名回滚,今天,我就跟大家分... 目录1. 重复数据插入问题分析1.1 问题本质1.2 常见场景图2. 基础解决方案:使用异常捕获3.

最详细安装 PostgreSQL方法及常见问题解决

《最详细安装PostgreSQL方法及常见问题解决》:本文主要介绍最详细安装PostgreSQL方法及常见问题解决,介绍了在Windows系统上安装PostgreSQL及Linux系统上安装Po... 目录一、在 Windows 系统上安装 PostgreSQL1. 下载 PostgreSQL 安装包2.

Python正则表达式语法及re模块中的常用函数详解

《Python正则表达式语法及re模块中的常用函数详解》这篇文章主要给大家介绍了关于Python正则表达式语法及re模块中常用函数的相关资料,正则表达式是一种强大的字符串处理工具,可以用于匹配、切分、... 目录概念、作用和步骤语法re模块中的常用函数总结 概念、作用和步骤概念: 本身也是一个字符串,其中

Python使用getopt处理命令行参数示例解析(最佳实践)

《Python使用getopt处理命令行参数示例解析(最佳实践)》getopt模块是Python标准库中一个简单但强大的命令行参数处理工具,它特别适合那些需要快速实现基本命令行参数解析的场景,或者需要... 目录为什么需要处理命令行参数?getopt模块基础实际应用示例与其他参数处理方式的比较常见问http

SQL中redo log 刷⼊磁盘的常见方法

《SQL中redolog刷⼊磁盘的常见方法》本文主要介绍了SQL中redolog刷⼊磁盘的常见方法,将redolog刷入磁盘的方法确保了数据的持久性和一致性,下面就来具体介绍一下,感兴趣的可以了解... 目录Redo Log 刷入磁盘的方法Redo Log 刷入磁盘的过程代码示例(伪代码)在数据库系统中,r

Python利用ElementTree实现快速解析XML文件

《Python利用ElementTree实现快速解析XML文件》ElementTree是Python标准库的一部分,而且是Python标准库中用于解析和操作XML数据的模块,下面小编就来和大家详细讲讲... 目录一、XML文件解析到底有多重要二、ElementTree快速入门1. 加载XML的两种方式2.

Java的栈与队列实现代码解析

《Java的栈与队列实现代码解析》栈是常见的线性数据结构,栈的特点是以先进后出的形式,后进先出,先进后出,分为栈底和栈顶,栈应用于内存的分配,表达式求值,存储临时的数据和方法的调用等,本文给大家介绍J... 目录栈的概念(Stack)栈的实现代码队列(Queue)模拟实现队列(双链表实现)循环队列(循环数组

redis过期key的删除策略介绍

《redis过期key的删除策略介绍》:本文主要介绍redis过期key的删除策略,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录第一种策略:被动删除第二种策略:定期删除第三种策略:强制删除关于big key的清理UNLINK命令FLUSHALL/FLUSHDB命