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中pywin32 常用窗口操作的实现

《Python中pywin32常用窗口操作的实现》本文主要介绍了Python中pywin32常用窗口操作的实现,pywin32主要的作用是供Python开发者快速调用WindowsAPI的一个... 目录获取窗口句柄获取最前端窗口句柄获取指定坐标处的窗口根据窗口的完整标题匹配获取句柄根据窗口的类别匹配获取句

Java 中的 @SneakyThrows 注解使用方法(简化异常处理的利与弊)

《Java中的@SneakyThrows注解使用方法(简化异常处理的利与弊)》为了简化异常处理,Lombok提供了一个强大的注解@SneakyThrows,本文将详细介绍@SneakyThro... 目录1. @SneakyThrows 简介 1.1 什么是 Lombok?2. @SneakyThrows

SpringBoot3.4配置校验新特性的用法详解

《SpringBoot3.4配置校验新特性的用法详解》SpringBoot3.4对配置校验支持进行了全面升级,这篇文章为大家详细介绍了一下它们的具体使用,文中的示例代码讲解详细,感兴趣的小伙伴可以参考... 目录基本用法示例定义配置类配置 application.yml注入使用嵌套对象与集合元素深度校验开发

判断PyTorch是GPU版还是CPU版的方法小结

《判断PyTorch是GPU版还是CPU版的方法小结》PyTorch作为当前最流行的深度学习框架之一,支持在CPU和GPU(NVIDIACUDA)上运行,所以对于深度学习开发者来说,正确识别PyTor... 目录前言为什么需要区分GPU和CPU版本?性能差异硬件要求如何检查PyTorch版本?方法1:使用命

Qt实现网络数据解析的方法总结

《Qt实现网络数据解析的方法总结》在Qt中解析网络数据通常涉及接收原始字节流,并将其转换为有意义的应用层数据,这篇文章为大家介绍了详细步骤和示例,感兴趣的小伙伴可以了解下... 目录1. 网络数据接收2. 缓冲区管理(处理粘包/拆包)3. 常见数据格式解析3.1 jsON解析3.2 XML解析3.3 自定义

SpringMVC 通过ajax 前后端数据交互的实现方法

《SpringMVC通过ajax前后端数据交互的实现方法》:本文主要介绍SpringMVC通过ajax前后端数据交互的实现方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价... 在前端的开发过程中,经常在html页面通过AJAX进行前后端数据的交互,SpringMVC的controll

Java中的工具类命名方法

《Java中的工具类命名方法》:本文主要介绍Java中的工具类究竟如何命名,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录Java中的工具类究竟如何命名?先来几个例子几种命名方式的比较到底如何命名 ?总结Java中的工具类究竟如何命名?先来几个例子JD

Spring Security自定义身份认证的实现方法

《SpringSecurity自定义身份认证的实现方法》:本文主要介绍SpringSecurity自定义身份认证的实现方法,下面对SpringSecurity的这三种自定义身份认证进行详细讲解,... 目录1.内存身份认证(1)创建配置类(2)验证内存身份认证2.JDBC身份认证(1)数据准备 (2)配置依

python获取网页表格的多种方法汇总

《python获取网页表格的多种方法汇总》我们在网页上看到很多的表格,如果要获取里面的数据或者转化成其他格式,就需要将表格获取下来并进行整理,在Python中,获取网页表格的方法有多种,下面就跟随小编... 目录1. 使用Pandas的read_html2. 使用BeautifulSoup和pandas3.

Spring 中的循环引用问题解决方法

《Spring中的循环引用问题解决方法》:本文主要介绍Spring中的循环引用问题解决方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录什么是循环引用?循环依赖三级缓存解决循环依赖二级缓存三级缓存本章来聊聊Spring 中的循环引用问题该如何解决。这里聊