Java中Arraylist源码分析

Posted Yrion

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中Arraylist源码分析相关的知识,希望对你有一定的参考价值。

 前言:ArrayList作为我们常用的一个集合数据类型,我们在代码中经常用它来装载数据,可谓是和HashMap一样常用的集合类型了。我们经常用它,那么就有必须知道它的内部工作原理,比如它是如何添加进去数据的,它内部的数据结构是怎样的,当我们做一个remove操作,它又做了哪些工作。了解这些内部工作的原理能够帮助我们更好的理解Arraylist,什么时候使用它和不使用它,如何提升它的效率,等等。那么本篇博文就来聚焦Arraylist,走进它的内部源码,来一探究竟吧。

本篇博客的目录

一:Arraylist简介

二:Arrsylist的构造方法

三:Arraylist的add和remove方法分析

四:Arraylist的常用方法源码分析

五:Arrraylist与linkedlist的区别

六:总结

一:Arraylist简介:

     首先是Arraylist一种java的数据容器,作为数据容器,我们在程序中经常使用它,比如Map、Collection、HashMap、TreeSet等等。如它的名字一样,它的内部结构是数组,而这个数组是可以动态扩容的,就犹如hashMap一样。它的所有操作都是基于其内部的动态数组来进行操作,list接口的大小可变数组的实现、如果1-1所示(其中index表示的是数组的元素的位置,但注意其数组下标实际是从0开始的,而不是1),源码中多次涉及到index这个变量,在这里可以先理解一下。其次它本质上一个类,实现了list接口,继承了AbstractList,和Cloneable接口、Serializable表明其继承于抽象的list,并且可以进行浅复制和序列化写入到数据文件中。同时它在构建的时候就说明了其类型泛型E,所以Arraylist是一种在新建时就声明其包含类型的集合,并且可支持复制、序列化、克隆等特性。

public class ArrayList<E> extends AbstractList<E>   implements List<E>, RandomAccess, Cloneable, java.io.Serializable

 

 二:Arrsylist的成员变量和构造方法

我们先来看一看Arraylist的成员变量,从中可以看到Arraylist维护着的数组和一些基本的数值表示出它的大小和数据类型,之所以声明为Object类型的,是因为要定义一个适用于所有java数据类型的,这样它就可以向下转型为所有基本类型了。至于元素容量为什么是10,而不是11或者12,这个可能是jdk的作者经过计算、根据使用习惯计算出来的,有一定的科学依据。这些都是全局变量,也就是说下面的方法都可以访问这些变量,并修改它的值。

    private static final long serialVersionUID = 8683452581122892189L; //序列版本号

    
    private static final int DEFAULT_CAPACITY = 10;//默认数组元素的初始容量为10

    
    private static final Object[] EMPTY_ELEMENTDATA = {};//空元素对象数组

    
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//默认容量的空数据数组

    
    transient Object[] elementData; // 数组元素对象,list中的元素就存在这里

    
    private int size;//添加的元素的数量

看完了成员变量,我们来看一下Arraylist的3个构造函数,其中我们用到的最多的也就是第二个无参的构造函数,平时代码里我们经常这样写:List<String>   list= new Arraylist()<String>;这就是调用了其第二个构造函数,在构造函数里面,我们可以看到它对空数组进行了赋值,将维护着的数组的大小设为10个容量,然后我们就可以使用这个list了。接下来我们就讲一讲如何往这个list里面增添元素和移除元素,这是我们使用list最常用的两个方法。

public ArrayList(int initialCapacity) {   //初始化容量 
        if (initialCapacity > 0) { //判断传入的参数值
            this.elementData = new Object[initialCapacity];//把传入的容量作为数组的初始容量
        } else if (initialCapacity == 0) {//如果传入的是0
            this.elementData = EMPTY_ELEMENTDATA;//初始化为空数组
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+ //传入的如果小于0,就抛出参数违规异常
                                               initialCapacity);
        }
    }
    
    public ArrayList() {  //空构造方法
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;//空对象数组初始化,其容量为10
    }
    
    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;
        }
    }

 三:Arraylist的add和remove方法分析

 1.add方法的解析:

我们先来看一下add方法,这其中又涉及到了rangeCheckForAdd方法和ensureCapacityInternal、System.arraycopy方法,我们来依次分析一下其中的源码,看看究竟在我们add一个元素的时候,Arraylist做了什么:

    public void add(int index, E element) {//根据指定元素添加指定元素
        rangeCheckForAdd(index);//通过位置进行范围检查

        ensureCapacityInternal(size + 1);  //
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;//元素赋值给指定的位置
        size++;//数组大小增加
    }
 private void rangeCheckForAdd(int index) {  //专为add方法增加的返回检查
            if (index < 0 || index > this.size)//如果传入的参数小于0或者大于数组已添加元素的个数
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));//抛出异常
        }
 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;//定义数组最大的值为Integer最大值-8
private void ensureCapacityInternal(int minCapacity) {//确保容量 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //如果数组元素等于默认的空数组容量 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);//取默认的容量和指定的容量的较大值 } ensureExplicitCapacity(minCapacity);//传入 ensureExplicitCapacity方法确保扩展的容量 }
    private void ensureExplicitCapacity(int minCapacity) {/确保扩展容量
        modCount++;//修改次数+1

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)//如果指定的容量大于数组容量
            grow(minCapacity);//调用gorw方法
    }
 
    private void grow(int minCapacity) { //传入指定的容量
        // overflow-conscious code
        int oldCapacity = elementData.length;//取旧的数组元素的长度
        int newCapacity = oldCapacity + (oldCapacity >> 1);//旧长度除以2+长度赋予新长度(相当于扩容1.5倍)
        if (newCapacity - minCapacity < 0)//如果新长度小于指定的容量
            newCapacity = minCapacity;//把新计算的长度赋予指定的容量
        if (newCapacity - MAX_ARRAY_SIZE > 0)//如果新容量>最大数组大小
            newCapacity = hugeCapacity(minCapacity);//调用bugeCapcaity方法
        elementData = Arrays.copyOf(elementData, newCapacity);//复制数组
    }
  private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // 如果指定容量小于0
            throw new OutOfMemoryError();//抛出异常内存错误
        return (minCapacity > MAX_ARRAY_SIZE) ?//如果指定容量大于最大数组大小返回int的最大值
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;//否则返回最小数组大小
    }

    通过源码我们可以看出:Arraylist在添加元素的时候,首先进行的是范围检查,防止其传入的参数小于0或者超过数组的size大小,再是和默认分配的大小值进行比较,如果大于默认大小就要进行扩容。扩容的时候首先把旧数组的大小提升1.5倍,成为新数组的大小值。同时也可以看到Arrylist的大小边界是Interger的最大值,这个数字是很大的:2147483647,也就是它的最大值是这么多,这个值足以我们程序员平常进行使用!在检查完毕和扩容完毕之后,就是要进行数组的拷贝了,我们开看一下System.arrayCopy()方法的源码:

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

其中可以看出,它是native修饰的,也就是说它是一个引用本地其他语言的代码库(windows就是.dll库)的方法,具体的源码我们不深究了,这里解释一下它的参数,src:你要从哪个数组复制,srcpos:要复制的位置起点 dest:要复制要哪个数组,destPos:要复制到的数组起始位置 lengh:复制的长度

System.arraycopy(elementData, index, elementData, index + 1,  size - index);

代入add方法里,也就是说把elementData的从index开始的元素复制size-index长度到elementData中,从index+1开始。这是一个自我复制的过程,为了保证数据的完整。然后把指定的元素值放入到指定的位置中。我们来用图解释一下这个复制的过程:首先是图1-1表示的是一共有5个元素,那么这个数组的size就是5.我们现在要在index=2的位置上插入元素六。我们来看看通过arraycopy方法之后的数组是什么样的:如图1-2。然后调用lementData[index] = element方法,之后数组就会变成图1-3:可见我们要插入的元素六就这样插入到指定位置上了,从中也可以看出自我复制也就是数组的元素从指定的位置向后移动,这也间接说明了Arraylist插入数据的效率比较低,因为它要移动数据。

         

 

 

             

                                               图 1-3

2:remove方法

我们看romove方法首先还是进行范围检查,然后用elementData()方法去找到指定数组元素中的值,再用size-index-1转为numMoved,接着进行再次自我复制。然后把size-1甚至设置为null。这样这个数组中元素就是null了。

    
    public E remove(int index) {//根据指定位置移除元素
        rangeCheck(index);//范围检查

        modCount++;//修改次数+1
        E oldValue = elementData(index);//根据数组元素的位置找到旧值

        int numMoved = size - index - 1;
        if (numMoved > 0)//判断是否大于0
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // 

        return oldValue;//返回旧值
    }

    为了更好的理解remove方法,我们来用画图的方式理解一下:其中一二三到十使我们存放的值,如图2-1.那么这个数组的size=10,假如我们移除remove(3),则index=3,我们可以看到index=2对应的值是四,然后numMoved的值是7,大于0。然后进行自我复制。从elementData的index=3开始,复制之后的结果如图2-2,可以看出从index开始的位置所有的元素往前移动了一位,这就是arraycopy方法的根本目的,然后再把  elementData[--size] = null; 也就是最后一个元素十置为null,这样数组的大小就自然减-1,并且顺利的移出了指定位置的元素。这就是remove方法原理

 

 

                                                                图 2-1

 

                                                                      图 2-2

 

 四:Arraylist的常用方法源码分析

我们再来看一下Arraylist中一些常见方法的源码,其中包括返回其大小的方法,判断是否为空的方法。是否包含某个元素的方法等等,这些方法比较简单,我只列出了基本的注释。

 public int size() {//取其大小
        return size;//返回size的数组
    }

    
    public boolean isEmpty() {//判断list是否为空
        return size == 0;//用其size和0去比较
    }

    
    public boolean contains(Object o) {//判断指定元素是否包含
        return indexOf(o) >= 0;//返回indexof方法的返回结果是否大于0。大于0表示含有,如果小于0表示没有
    }

    
    public int indexOf(Object o) {//检索指定元素是否存在
        if (o == null) {//如果元素为null
            for (int i = 0; i < size; i++)//遍历循环整个数组
                if (elementData[i]==null)//如果找到元素为null
                    return i;//返回元素中的位置
        } else {//如果不为null
            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) {//如果元素为null
            for (int i = size-1; i >= 0; i--)//按照元素从大到小
                if (elementData[i]==null)//如果元素为null
                    return i;//返回数组中的位置
        } else {//如果元素不为null
            for (int i = size-1; i >= 0; i--)//按照元素从大到小
                if (o.equals(elementData[i]))//如果元素等于数组中的元素
                    return i;//返回数组中的位置
        }
        return -1;//否则返回-1
    }
  public boolean remove(Object o) {//判断元素是否能够移除
        if (o == null) {//如果元素为null
            for (int index = 0; index < size; index++)//
                if (elementData[index] == null) {//如果元素值为null
                    fastRemove(index);//快速移除
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

    
    private void fastRemove(int index) {//根据指定的位置移除元素
        modCount++;//修改次数+1
        int numMoved = size - index - 1;//从指定元素的下一个开始
        if (numMoved > 0)//如果元素的下一个大于0
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);//拷贝数组
        elementData[--size] = null; //数组对象的最后一个值赋值为null 
    }

    
    public void clear() { //清除元素
        modCount++;//修改次数+1
        for (int i = 0; i < size; i++)//遍历循环整个数组
            elementData[i] = null;//将其值设为null,便于GC回收

        size = 0;//size等于0
    }

 

五:Arraylist与linkedlist的区别

     1.从本篇博文中可以看出ArrayList基于动态数组的数据结构,但是LinkedList基于链表的数据结构。
     2:Arraylist是有序的,元素它是按照固定的顺序排列,每次都往后移动一位,并且很容易可以看出它是允许值为null。而linkedlist是无序的。

     3.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。 我们在这里看一下get方法的源码,其中可以看出其只需要做个范围检查,然后就可以通过数组的位置计算         处其位置上的值,这也是数组的特点,获取数组很快。但是linkedlist是链表结构的,所谓结构决定功能,链表的获取元素时候需要一层层去节点里面遍历,这无疑增加了工作量,所以linked的随机     访 问要落后与Arraylist
    

  public E get(int index) {//根据指定位置返回对应的值
        rangeCheck(index);//调用范围检查的方法,防止其越界
        
        return elementData(index);//把参数传入,调用elementData()方法
    }

 

    4.对于新增和删除操作add和remove,LinkedList有很大的优势,因为ArrayList要移动数据

       我们通过比对发现add和remove方法,它们都要经过System.arrayCopy()方法,进行数据的移动,比较麻烦,而基于链表的结构的LinkedList则只需要进行值和位置的封装,然后放入链表即           可。

    5:Arraylist是非线程安全的,其中我们可以看出它并没有同步机制的实现,也没有synchronize等关键字的修饰。所以在多线程的环境下慎用Arraylist。同时linkedList也并非线程安全的!

六:总结

   本篇博文介绍了Arraylist的源码,从其中可以看出jdk设计者的巧妙,包括其add方法的校验,防止校验,还有动态扩容的特性,它作为一个我们在实际开发中常用的数据容器,按照从全局变量到构造方法再到其add、remove方法的学习,我们对arraylist也有了进一步的认知,同时与linkedlist进行了对比,以便于我们更好的掌握Arraylist!好了,本次博客就到这里结束了。

 

以上是关于Java中Arraylist源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Java中arraylist和linkedlist源码分析与性能比较

[Java源码分析]ArrayList源码分析

ArrayList精讲(源码分析)---Java集合

ArrayList精讲(源码分析)---Java集合

java集合中的ArrayList源码分析

Java集合源码分析——ArrayList