ArrayList源码阅读笔记

简介

ArrayList是基于数组实现的一种列表。

ArrayList继承体系如下:

图一:ArrayList继承体系

在这里插入图片描述

  • ArrayList实现了List,RandomAccess,Cloneable,java.io.Serializable等接口。

  • ArrayList实现了List,提供了基础的添加、删除、遍历等操作。

  • ArrayList实现了RandomAccess,提供了随机访问的能力。

  • ArrayList实现了Cloneable,可以被克隆。

  • ArrayList实现了Serializable,可以被序列化。


源码分析

属性

首先看看ArrayList的属性。

    /**
     * 默认初始化容量
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 空数组,如果传入的容量为0时使用
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     *空数组,传传入容量时使用,添加第一个元素的时候会重新初始为默认容量大小
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     *存储数据元素的数组
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * ArrayList的大小(包含数据元素的个数)
     */
    private int size;

构造方法

  • 无参构造方法
    /**
     * 创建一个初始容量为10的空列表
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
   
  • 指定初始容量的构造方法
  /**
  * 创建一个指定容量的list
  */
    public ArrayList(int initialCapacity) {
       // 如果传入的初始容量大于0,就新建一个数组存储元素
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            // 如果传入的初始容量等于0,使用空数组EMPTY_ELEMENTDATA
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
  • 从其它集合构造
   /**
   * 把传入集合的元素初始化到ArrayList中
   */
    public ArrayList(Collection<? extends E> c) {
       //把集合转为数组
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            //  检查c.toArray()返回的是不是Object[]类型,如果不是,重新拷贝成Object[].class类型
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData,size,Object[].class);
        } else {
            // 如果是空集合,则初始化为空数组EMPTY_ELEMENTDATA
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

add(E e)

添加元素到末尾,平均时间复杂度为O(1)。

在这里插入图片描述

    /**
    * 在末尾添加元素
    */
    public boolean add(E e) {
         // 检查是否需要扩容
        ensureCapacityInternal(size + 1);  
        // 把元素插入到末尾
        elementData[size++] = e;
        return true;
    }

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData,minCapacity));
    }

    private static int calculateCapacity(Object[] elementData,int minCapacity) {
        // 如果是空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA,就初始化为默认大小10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY,minCapacity);
        }
        return minCapacity;
    }

    private void ensureExplicitCapacity(int minCapacity) {
       // 修改次数 +1,用于 fail-fast 处理
        modCount++;

        // 如果 minCapacity 大于 elementData 的长度,则进行扩容处理
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    } 
    
    /**
    *  扩容
    */
     private void grow(int minCapacity) {
        // 有整形溢出风险的代码
        int oldCapacity = elementData.length;
        //新容量=旧容量+(旧容量右移1位(除以2)),新容量为原来的1.5倍
        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);
    }

这里我们稍微看一下Arrays.copyOf的源码:

    public static <T> T[] copyOf(T[] original,int newLength) {
        return (T[]) copyOf(original,newLength,original.getClass());
    }
    public static <T,U> T[] copyOf(U[] original,int newLength,Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(),newLength);
        System.arraycopy(original,copy,Math.min(original.length,newLength));
        return copy;
    }

最终调用的是System.arraycopy方法,这是一个Native方法:

    public static native void arraycopy(Object src,int  srcPos,Object dest,int destPos,int length);

关于数组拷贝,谷歌了一下,说法不一,有说是深拷贝的,有说是浅拷贝的。暂时先放下,未来有机会再研究。


add(int index,E element)

add(int index,E element)在特定位置插入元素,时间复杂度为O(n)。

    public void add(int index,E element) {
        // 检查是否越界
        rangeCheckForAdd(index);
        //检查是否需要扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //// 将 elementData 中位置为 index 位置及其后面的元素都向后移动一个下标(底层是 native 方法,使用 cpp 直接操作内存。)
        System.arraycopy(elementData,index,elementData,index + 1,size - index);
        // 将元素插入到index的位置                 
        elementData[index] = element;
        //// 大小增1
        size++;
    }

addAll

addAll用于批量添加。

    public boolean addAll(Collection<? extends E> c) {
       // 集合转化成数组
        Object[] a = c.toArray();
        
        int numNew = a.length;
        //检查是否需要扩容
        ensureCapacityInternal(size + numNew);  // Increments modCount
        //将集合内的元素复制到 elementData 中,覆盖 [size,size+numNew) 的元素
        System.arraycopy(a,numNew);
        //数组大小增加numNew
        size += numNew;
        return numNew != 0;
    }


    public boolean addAll(int index,Collection<? extends E> c) {
       //检查下标是否越界
        rangeCheckForAdd(index);
        //转换为数组
        Object[] a = c.toArray();
        int numNew = a.length;
        //检查是否需要扩容
        ensureCapacityInternal(size + numNew);  // Increments modCount
        int numMoved = size - index;
        if (numMoved > 0)
           // 将 elementData 中位置为 index 及其以后的元素都向后移动 numNew 个位置
            System.arraycopy(elementData,index + numNew,numMoved);
        // 将集合内的元素复制到 elementData 中,覆盖 [index,index+numNew) 的元素                      
        System.arraycopy(a,numNew);
        size += numNew;
        return numNew != 0;
    }

get(int index)

获取指定位置元素,时间复杂度为O(1)。

    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

remove(int index)

删除指定索引位置的元素,时间复杂度为O(n)。

    public E remove(int index) {
       //检查下标是否越界
        rangeCheck(index);

        modCount++;
        //获取指定索引处元素
        E oldValue = elementData(index);
        //// 如果index不是最后一位,则将index之后的元素往前挪一位
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData,index+1,numMoved);
        //将最后一个元素删除,帮助GC                     
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

remove(Object o)

删除指定元素,时间复杂度为O(n²)。

    public boolean remove(Object o) {
       //元素为null
        if (o == null) {
           //遍历数组
            for (int index = 0; index < size; index++)
               //快速删除所有为null的元素
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            //元素不为null,遍历数组
            for (int index = 0; index < size; index++)
               //找到对应元素,快读删除
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

    //和remove(int index)基本相同,少了检查越界的方法,无返回值
    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        // 如果index不是最后一位,则将index之后的元素往前挪一位
        if (numMoved > 0)
            System.arraycopy(elementData,numMoved);
        // 将最后一个元素删除,帮助GC                     
        elementData[--size] = null; // clear to let GC do its work
    }

removeAll

用于批量删除元素。

   //批量删除 ArrayList 和集合 c 都存在的元素
    public boolean removeAll(Collection<?> c) {
       //非空校验
        Objects.requireNonNull(c);
       //批量删除 
        return batchRemove(c,false);
    }

    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 {
            if (r != size) {
                System.arraycopy(elementData,r,w,size - r);
                w += size - r;
            }
            if (w != size) {
                // 跟 fastRemove(int index) 里面的操作类似,防止内存泄漏
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }

set

用于更改特定下标的值,时间复杂度为O(1)。

    public E set(int index,E element) {
        
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

序列化

注意观察,ArrayList 有两个属性被 transient 关键字 修饰,transient 关键字的作用:让某些被修饰的成员属性变量不被序列化

transient Object[] elementData;
protected transient int modCount = 0;

为什么最重要的元素数组要被transient 修饰呢?

因为ArrayList 并没有用Java序列化机制的默认处理来序列化 elementData 数组,而是通过 readObject、writeObject 方法自定义序列化和反序列化策略。

之所以要用自定义的序列化和反序列化策略,是因为效率的问题。如果用默认处理来序列化的话,如果 elementData 的长度有100,但是实际只用了50,其实剩余的50是可以不用序列化的,这样可以提高序列化和反序列化的效率,节省空间。

   /**
   *  自定义序列化
   */
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // fail-fast,后续判断是否有并发处理
        int expectedModCount = modCount;
         // 序列化没有标记为 static、transient 的字段,包括 size 等。
        s.defaultWriteObject();

        
        s.writeInt(size);

        // 序列化数组的前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;

        //// 反序列化没有标记为 static、transient 的字段,包括 size 等
        s.defaultReadObject();

       
        s.readInt(); // ignored

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



线程安全

在上面的源码里我们看到很多方法里有快速失败的机制,例如判断扩容的方法:

private void ensureExplicitCapacity(int minCapacity) {
    // 修改次数 +1,用于 fail-fast 处理
    modCount++;

这种快读失败的机制一定程度上避免了线程安全问题。

"快速失败”即fail-fast,它是java集合的一种错误检测机制。当多钱程对集合进行结构上的改变或者集合在迭代元素时直接调用自身方法改变集合结构而没有通知迭代器时,有可能会触发fast-fail机制并抛出异常。

当然,fail-fast机制只是可能触发,实际上,ArrayList的线程安全还是没有保证的。一般,保证ArrayList的线程安全可以通过这些方案:

  • 使用 Vector 代替 ArrayList。(不推荐,Vector是一个历史遗留类)
  • 使用 Collections.synchronizedList 包装 ArrayList,然后操作包装后的 list 即可。
  • 使用 CopyOnWriteArrayList 代替 ArrayList。
  • 在使用 ArrayList 时,应用程序通过同步机制去控制 ArrayList 的读写(不推荐)。



纸上得来终觉浅,绝知此事要躬行。



参考:

【1】:【死磕 Java 集合】— ArrayList源码分析
【2】:java 集合之 ArrayList 源码解读
【3】:ArrayList 源码分析

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


@ 注解能被用来为程序元素( 类、 方法、 成员变量等) 设置元数据。 值得指出的是, 注解不影响程序代码的执行, 无论增加、 删除注解, 代码都始终如一地执行。 如果希望让程序中的注解在运行时起一定
@ 1、线性表的概念 线性表是最常见也是最简单的一种数据结构。简言之, 线性表是n个数据元素的有限序列。 其一般描述为: A={a1,a2,……an) 一个数据元素通常包含多个数据项, 此时每个数据元
简介 ArrayList是开发中使用比较多的集合,它不是线程安全的,CopyOnWriteArrayList就是线程安全版本的ArrayList。CopyOnWriteArrayList同样是通过数组
在 Java String类源码阅读笔记 里学习了String类的源码,StringBuilder、StringBuffer是经常拿来和String类做对比的两个类,可谓是“爱恨纠缠” ,这里我们继续
话不多说,先上图。 1、基本概念 欲说线程,必先说进程。 进程:进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。 线程:线程是进程的一个执行路径,一个进程中至少有一个线程,进
@ 网络基础 计算机网络是指两台或更多的计算机组成的网络,在同一个网络中,任意两台计算机都可以直接通信,因为所有计算机都需要遵循同一种网络协议。 那什么是互联网呢?互联网是网络的网络(internet
JVM是面试中必问的部分,本文通过思维导图以面向面试的角度整理JVM中不可不知的知识。 先上图: 1、JVM基本概念 1.1、JVM是什么 JVM 的全称是 「Java Virtual Machine
@ 本文基于jdk1.8 HashMap采用 key/value 存储结构,每个key对应唯一的value。 在jdk1.7之前,HashMap 的内部存储结构是数组+链表。 在jdk1.8中 Has
@ Eclipse是很多Java开发者的第一个开发工具,尽管开源的Eclipse在一后起之秀的对比下,显得有些颓势,但是,Eclipse有着丰富的插件支持。选择合适的插件,Eclipse表示:老夫也能
@ 准备 LinkedList是基于双向链表数据结构实现的Java集合(jdk1.8以前基于双向循环链表),在阅读源码之前,有必要简单了解一下链表。 先了解一下链表的概念:链表是由一系列非连续的节点组
@ 写博客哪有刷逼乎有意思 1 写博客哪有刷逼乎有意思 2 写博客哪有刷逼乎有意思 3 类的加载、 连接和初始化 系统可能在第一次使用某个类时加载该类, 也可能采用预加载机制来加载某个类。 JVM 和
树结构是一类重要的非线性数据结构。直观来看,树是以分支关系定义的层次结构。树结构在客观世界广泛存在,如人类社会的族谱和各种社会组织机构都可用树来形象表示。 树在计算机领域中也得到广泛应用,尤以二叉树最
@ 本文基于jdk1.8 String类可谓是我们开发中使用最多的一个类了。对于它的了解,仅仅限于API的了解是不够的,必须对它的源码进行一定的学习。 一、前置 String类是Java中非常特别的一
随便打开一个招聘网站,看看对高级Java工程师的技能要求。 抛开其它的经验能力等等,单纯从技术,或者说知识上来讲,可以发现一些共通的地方。 Java基础 计算机基础 数据库,SQL/NoSQL 常用开
@ JDBC指Java 数据库连接,是一种标准Java应用编程接口( JAVA API),用来连接 Java 编程语言和广泛的数据库。 1、JDBC典型用法 1.1、JDBC 4.2 常用接口和类简介
简介 ArrayList是基于数组实现的一种列表。 ArrayList继承体系如下: 图一:ArrayList继承体系 ArrayList实现了List, RandomAccess, Cloneabl
@ Java 的 IO 通过 java.io 包下的类和接口来支持, 在 java.io 包下主要包括输入、 输出两种 10 流, 每种输入、 输出流又可分为字节流和字符流两大类。 其中字节流以字节为
@ 使用断言 断言(Assertion)是一种调试程序的方式。在Java中,使用assert关键字来实现断言。 断言的概念 假设确信某个属性符合要求, 并且代码的执行依赖于这个属性。例如, 需要计算:
@ Java 程序在不同操作系统上运行时,可能需要取得平台相关的属性,或者调用平台命令来完成特定功能。 Java 提供了 System 类和 Runtime 类来与程序的运行平台进行交互。 Syste
@ Java 提供了一个操作 Set 、 List 和 Map等集合的类:Collections , 该工具类里提供了大量方法对集合元素进行排序、 查询和修改等操作,还提供了将集合对象设置为不可变、对