Java源码解析ArrayList及ConcurrentModificationException

本文基于jdk1.8来分析ArrayList的源码

首先是主要的成员变量。

  /**
   * Default initial capacity.
   **/
  private static final int DEFAULT_CAPACITY = 10;
  /**
   * Shared empty array instance used for empty instances.
   **/
  private static final Object[] EMPTY_ELEMENTDATA = {};
  /**
   * Shared empty array instance used for default sized empty instances. We
   * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
   * first element is added.
   **/
  private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  /**
   * The array buffer into which the elements of the ArrayList are stored.
   * The capacity of the ArrayList is the length of this array buffer. Any
   * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
   * will be expanded to DEFAULT_CAPACITY when the first element is added.
   **/
  transient Object[] elementData; // non-private to simplify nested class access
  /**
   * The size of the ArrayList (the number of elements it contains).
   *
   * @serial
   **/
  private int size;

其中初始大小为10,size表示集合中元素的个数。此外,还有两个空数组EMPTY_ELEMENTDATA,和DEFAULTCAPACITY_EMPTY_ELEMENTDATA。通过DEFAULTCAPACITY_EMPTY_ELEMENTDATA的注释,我们可以了解到,这个变量区别于EMPTY_ELEMENTDATA,主要是为了决定第一个元素插入时,扩容多大的问题。从这里的描述可以理解到,ArrayList创建好后,其实并没有真正分配数组空间,而是在第一个元素插入时,才分配的空间。这一点是区别于jdk1.6的。在jdk1.6中,ArrayList一创建,数据空间就默认分配好了,10个或指定的空间。jdk1.8这么做,可以做到空间延迟分配,提高程序性能。

接下来看一下构造函数。

/**
   * Constructs an empty list with an initial capacity of ten.
   **/
  public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
  }
/**
   * Constructs an empty list with the specified initial capacity.
   *
   * @param initialCapacity the initial capacity of the list
   * @throws IllegalArgumentException if the specified initial capacity
   *     is negative
   **/
  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);
    }
  }

无参构造函数,将创建一个长度为0的空数组。

有参构造函数,参数大于0时正常创建数组,参数为0时,也是创建长度为0的数组。但它和无参构造函数创建的空数组是可以区别开的,它们使用了不同的对象。

接下来是插入元素add。

/**
   * Appends the specified element to the end of this list.
   *
   * @param e element to be appended to this list
   * @return <tt>true</tt> (as specified by {@link Collection#add})
   **/
  public boolean add(E e) {
    ensureCapacityInternal(size + 1); // Increments modCount!!
    elementData[size++] = e;
    return true;
  }
 private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
  }
 private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
      return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
  }
 private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
      grow(minCapacity);
  }

通过calculateCapacity函数,我们可以知道,如果是用new ArrayList()创建的list,第一次add元素,计算得minCapacity = 1。如果是new ArrayList(0)创建的list,计算得minCapacity = 10. 然后再根据minCapacity去grow。

get方法比较简单,这里不再分析。

ArrayList的一个常见问题是ConcurrentModificationException,同步修改异常,也称为快速失败,fast-fail。当我们以foreach方式遍历ArrayList时,如果在遍历过程中删除ArrayList的元素,或者别的线程往ArrayList中添加元素,就会抛出该异常。这里需要注意,以for(int i = 0; i < list.size(); i++)的方式遍历ArrayList时,是不会抛出同步修改异常的,但用这种方式遍历,需要处理好i的前进速度。

那么,用foreach方式遍历ArrayList为什么会抛出同步修改异常呢?

foreach代码的底层实现,是用iterator对ArrayList进行遍历,在遍历过程中,会持续调用next获取下一个元素。next方法中,会首先checkForComodification(),它的作用是检查modCount和expectedModCount是否相等。不相等时,则抛出同步修改异常。那么什么情况下修改次数和期望修改次数不相等呢?这里需要首先弄明白,modCount和expectedModCount是什么东西?modCount是ArrayList从它的父类继承来的属性,记录了集合的修改次数,add,remove时都会给modCount加1. expectedModCount是迭代器的成员变量,它是在创建迭代器时,取的modCount的值,并且,在遍历过程中不再改变。那么就清楚了,expectedModCount其实是开始遍历时modCount的值,如果在遍历过程中,ArrayList进行了add或remove操作,那么必然导致expectedModCount和modCount不相等,于是就抛出了同步修改异常。

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

那么,同步修改异常如何避免呢?或者说,我们如何遍历集合并把其中的某些元素删除呢?

答案是使用迭代器的remove方法删除元素。在迭代器的remove方法中,删除元素后,会重新把modCount赋值给expectedModCount,所以,它不会抛出同步修改异常。

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

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。如果你想了解更多相关内容请查看下面相关链接

(0)

相关推荐

  • Java实现对两个List快速去重并排序操作示例

    本文实例讲述了Java实现对两个List快速去重并排序操作.分享给大家供大家参考,具体如下: 1:去重并排序 package twolist; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.uti

  • Java中List根据map的某个key去重的代码

    话不多说,看代码和效果 /** * 根据map中的某个key 去除List中重复的map * @author shijing * @param list * @param mapKey * @return */ public static List<Map<String, Object>> removeRepeatMapByKey(List<Map<String, Object>> list, String mapKey){ if (CollectionUt

  • java正则表达式实现提取需要的字符并放入数组【ArrayList数组去重复功能】

    本文实例讲述了java正则表达式实现提取需要的字符并放入数组.分享给大家供大家参考,具体如下: 这里演示Java正则表达式提取需要的字符并放入数组,即ArrayList数组去重复功能. 具体代码如下: package com.test.tool; import java.util.ArrayList; import java.util.HashSet; import java.util.regex.*; public class MatchTest { public static void ma

  • Java源码解析LinkedList

    本文基于jdk1.8进行分析. LinkedList和ArrayList都是常用的java集合.ArrayList是数组,Linkedlist是链表,是双向链表.它的节点的数据结构如下. private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = elemen

  • Java LinkedList的实现原理图文详解

    一.概述 先来看看源码中的这一段注释,我们先尝试从中提取一些信息: Doubly-linked list implementation of the List and Deque interfaces. Implements all optional list operations, and permits all elements (including null).All of the operations perform as could be expected for a doubly-l

  • java中List对象列表实现去重或取出及排序的方法

    前言 因为在面试的时候碰到几次list的去重和排序,觉着有必要给大家总结一下具体的方法,分享出来供大家学习参考,话不多说了,来一起看看下面介绍的一种做法: 一.list去重 1.1 实体类Student List<Student>容量10k以上,要求去重复.这里Student的重复标准是属性相同,因此需要重写equals和hashcode方法,不知道有几个可以手写出来. student的equals方法: public void equals(Object o){ if(this == o)

  • Java List中数据的去重

    list中数据的去重,通常使用将list转换为set,简单直接,因为set集合的特点就是没有重复的元素.需要考虑一下两种情况: 1.List集合中的数据类型是基本数据类型 可以直接将list集合转换成set,就会自动去除重复的元素. 如下示例: public class Test { public static void main(String[] args) { List list = new ArrayList(); list.add(11); list.add(12); list.add(

  • Java中对List去重 Stream去重的解决方法

    问题 当下互联网技术成熟,越来越多的趋向去中心化.分布式.流计算,使得很多以前在数据库侧做的事情放到了Java端.今天有人问道,如果数据库字段没有索引,那么应该如何根据该字段去重?大家都一致认为用Java来做,但怎么做呢? 解答 忽然想起以前写过list去重的文章,找出来一看.做法就是将list中对象的hashcode和equals方法重写,然后丢到HashSet里,然后取出来.这是最初刚学Java的时候像被字典一样背写出来的答案.就比如面试,面过号称做了3年Java的人,问Set和HashMa

  • Java源码解析CopyOnWriteArrayList的讲解

    本文基于jdk1.8进行分析. ArrayList和HashMap是我们经常使用的集合,它们不是线程安全的.我们一般都知道HashMap的线程安全版本为ConcurrentHashMap,那么ArrayList有没有类似的线程安全的版本呢?还真有,它就是CopyOnWriteArrayList. CopyOnWrite这个短语,还有一个专门的称谓COW. COW不仅仅是java实现集合框架时专用的机制,它在计算机中被广泛使用. 首先看一下什么是CopyOnWriteArrayList,它的类前面

  • Java源码解析ArrayList及ConcurrentModificationException

    本文基于jdk1.8来分析ArrayList的源码 首先是主要的成员变量. /** * Default initial capacity. **/ private static final int DEFAULT_CAPACITY = 10; /** * Shared empty array instance used for empty instances. **/ private static final Object[] EMPTY_ELEMENTDATA = {}; /** * Shar

  • Java源码解析之HashMap的put、resize方法详解

    一.HashMap 简介 HashMap 底层采用哈希表结构 数组加链表加红黑树实现,允许储存null键和null值 数组优点:通过数组下标可以快速实现对数组元素的访问,效率高 链表优点:插入或删除数据不需要移动元素,只需要修改节点引用效率高 二.源码分析 2.1 继承和实现 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {

  • Java源码解析之ClassLoader

    一.前言 一个完整的Java应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的class文件当中,所以经常要从这个class文件中要调用另外一个class文件中的方法,如果另外一个文件不存在的,则会引发系统异常.而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它cl

  • Java源码解析之Gateway请求转发

    Gateway请求转发 本期我们主要还是讲解一下Gateway,上一期我们讲解了一下Gateway中进行路由转发的关键角色,过滤器和断言是如何被加载的,上期链接://www.jb51.net/article/211824.htm 好了我们废话不多说,开始今天的Gateway请求转发流程讲解,为了在讲解源码的时候,以防止大家可能会迷糊,博主专门画了一下源码流程图,链接地址://www.jb51.net/article/211824.htm 上一期我们已经知道了相关类的加载,今天直接从源码开始,大家

  • Java源码解析之TypeVariable详解

    TypeVariable,类型变量,描述类型,表示泛指任意或相关一类类型,也可以说狭义上的泛型(泛指某一类类型),一般用大写字母作为变量,比如K.V.E等. 源码 public interface TypeVariable<D extends GenericDeclaration> extends Type { //获得泛型的上限,若未明确声明上边界则默认为Object Type[] getBounds(); //获取声明该类型变量实体(即获得类.方法或构造器名) D getGenericDe

  • Java源码解析之GenericDeclaration详解

    学习别人实现某个功能的设计思路,来提高自己的编程水平.话不多说,下面进入正题. GenericDeclaration 可以声明类型变量的实体的公共接口,也就是说,只有实现了该接口才能在对应的实体上声明(定义)类型变量,这些实体目前只有三个:Class(类).Construstor(构造器).Method(方法)(详见:Java源码解析之TypeVariable详解 源码 public interface GenericDeclaration { //获得声明列表上的类型变量数组 public T

  • Java源码解析之object类

    在源码的阅读过程中,可以了解别人实现某个功能的涉及思路,看看他们是怎么想,怎么做的.接下来,我们看看这篇Java源码解析之object的详细内容. Java基类Object java.lang.Object,Java所有类的父类,在你编写一个类的时候,若无指定父类(没有显式extends一个父类)编译器(一般编译器完成该步骤)会默认的添加Object为该类的父类(可以将该类反编译看其字节码,不过貌似Java7自带的反编译javap现在看不到了). 再说的详细点:假如类A,没有显式继承其他类,编译

  • Java源码解析之接口Collection

    一.图示 二.方法定义 我们先想一想,公司如果要我们自己去封装一些操作数组或者链表的工具类,我么需要封装哪些功能呢?不妨就是统计其 大小,增删改查.清空或者是查看否含有某条数据等等.而collection接口就是把这些通常操作提取出来,使其更全面.更通用,那现在我们就来看看其源码都有哪些方法. //返回集合的长度,如果长度大于Integer.MAX_VALUE,返回Integer.MAX_VALUE int size(); //如果集合元素总数为0,返回true boolean isEmpty(

  • Java源码解析之平衡二叉树

    一.平衡二叉树的定义 平衡二叉树是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1 .它是一种高度平衡的二叉排序树.意思是说,要么它是一棵空树,要么它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1 .我们将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF (Balance Factor),那么平衡二叉树上所有结点的平衡因子只可能是-1 .0 和1. 这里举个栗子: 仔细看图中值为18的节点,18的节点的深度为2 .而它的右子树的深度为0,其差

  • Java源码解析之接口List

    前言 List接口是Collection接口的三大接口之一,其中的数据可以通过位置检索,用户可以在指定位置插入数据.List的数据可以为空,可以重复.我们来看看api文档是怎么说的: 一.List特有的方法 我们这里就只关注和Collection不同的方法,主要有以下这些: //在指定位置,将指定的集合插入到当前的集合中 boolean addAll(int index, Collection<? extends E> c); //这是一个默认实现的方法,会通过Iterator的方式对每个元素

随机推荐