• Java知识树 集合 ArrayList


    知识简述

    ArrayList,实现了List接口,它是一个有序集合,即元素排列的顺序和添加元素的顺序一致,我们可以通过下面的示例代码和结构图来理解刚刚这句话。

    示例代码:

    List<Integer> list = new ArrayList<Integer>(); 
    list.add(
    6);
    list.add(
    4);
    list.add(
    7);
    list.add(
    6);
    list.add(
    1);

    结构图:

    通过上面的结构图我们可以知道ArrayList的底层是由数组来实现的,但它与数组的区别在于ArrayList的容量会动态增长,这意味着因存储元素导致容量不足时ArrayList会自动扩大数组的容量。这一过程的细节会在源码分析阶段进行说明。

     

    我们在创建ArrayList时若不指定初始容量大小,则初始容量大小会默认为10,即一个能存储10个元素的数组。(注①)当然我们也可以根据实际的存储需要手动指定初始容量大小,见如下示例代码:

    List<Integer> list = new ArrayList<Integer>(5);

    请记住,即便指定了初始容量大小,当因存储元素导致容量不足时,ArrayList依然会自动扩容。那么这个扩容是否会有一个上限呢?答案是肯定的,具体细节会在源码分析阶段说明。

    注①:在jdk1.6版本中这句话是没有问题的,但在jdk1.7版本中ArrayList()这一构造器的内部实现做了改进。可见如下源码对比:

    //jdk1.6的实现
    public ArrayList() {
        this(10);
    }
    
    //this(10)是调用了另外一个构造器ArrayList(int initialCapacity)
    public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }
    
    //this.elementData就是一个用来存储元素的数组
    private transient Object[] elementData;
    
    
    
    //jdk1.7的实现
    public ArrayList() {
         super();
         this.elementData = EMPTY_ELEMENTDATA;
    }
    
    //EMPTY_ELEMENTDATA是一个已经初始化了的数组,不过它的容量为0
    private static final Object[] EMPTY_ELEMENTDATA = {};

    我们可以对jdk1.7的改进稍作分析:当调用ArrayList()时,仅初始化了数组但并未指定容量,故此时数组的长度为0,这样做也许是为了避免存储空间的浪费。若按照之前jdk1.6的做法,只要通过ArrayList()这一构造器来创建ArrayList,那么就会初始化一个容量为10的数组,如果这个ArrayList不被使用,那么这10个容量的数组就浪费了,这样的ArrayList越多,浪费的数组也就越多。采用jdk1.7的做法虽然一样初始化了数组,但并未指定容量,这样即便创建出的ArrayList不被使用,也不至于造成存储空间的浪费。对于jdk1.7的改进我们可以提出一个疑问?通过ArrayList()这一构造器来创建ArrayList,它是在什么时候指定数组容量的呢?答案是在调用add(E e)或add(int index, E element)方法时,具体的细节会在源码分析阶段进行说明。

     

    ArrayList实现了RandomAccess接口,提供了随机访问功能,即可以通过索引快速访问该索引所对应的元素。那么这个随机访问又是一个怎样的过程?这个问题也会在接下来的源码分析阶段得到解答。

     

    最后需要注意的是ArrayList是非线程安全的,这意味着在多线程环境下使用它是有很大风险的。因此在多线程环境下建议使用Vector来替代它。

     

    源码分析(基于jdk1.6.0_45

    在源码分析阶段我们通过一个个问题来进行探索。

    1)创建一个ArrayList的过程是怎样的?

    在创建一个ArrayList时,我们需要调用它的构造器,在不指定初始容量的情况下我们可以调用ArrayList(),如果明确了要存储的元素容量,可以调用ArrayList(int initialCapacity)来创建一个带有指定容量的ArrayList。

    //用于存储元素的数组,当ArrayList的构造器被调用时进行初始化
    private transient Object[] elementData;
    
    //数组中包含的元素数量,这里注意要与capacity区分,在调用ArrayList()后,capacity是10,但size是0
    private int size;
    
    
    //这虽然也是ArrayList的一个构造器,但从内部实现来看,它是调用了ArrayList(int initialCapacity)这一构造器。当我们不指定数组初始容量大小时,默认传入的initialCapacity的值是10,即一个容量为10的数组。
    public ArrayList() {
        this(10);
    }
    
    public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

    2)往ArrayList中添加元素时,add(E e)方法内部都做了什么?

    /*
    首先是要确保容量足够存储被添加的元素,因此调用ensureCapacity(int minCapacity)方法,注意这个方法的形参minCapacity,它的实参是size + 1, 这就是说在每次添加元素时,size都会自增1,并且会用size和capacity进行对比。 当添加第11个元素时,if (minCapacity > oldCapacity) 的条件就成立了, 这时就需要对数组进行扩容,扩大多少容量呢? int newCapacity = (oldCapacity * 3) / 2 + 1通过这个公式可以知道扩容了大约60%。
    扩容的过程就是创建一个新的数组,其容量为newCapacity,再将原本数组中的元素都copy到新数组中,最后将这个新数组的引用给到elementData,这样就完了一次ArrayList的动态扩容。
    */ public boolean add(E e) { ensureCapacity(size + 1); elementData[size++] = e; return true; } public void ensureCapacity(int minCapacity) { modCount++; int oldCapacity = elementData.length; if (minCapacity > oldCapacity) { Object oldData[] = elementData; int newCapacity = (oldCapacity * 3)/2 + 1; if (newCapacity < minCapacity) newCapacity = minCapacity; elementData = Arrays.copyOf(elementData, newCapacity); } }

    3)删除ArrayList中存储的元素时发生了什么?

    /*
    对于remove(int index)方法来说,在进行删除操作前需要先判断索引(index)是否大于等于size,这样做是为了避免下标越界。
    接着获取索引对应的元素,在完成这一步操作后,进行了一次运算:int numMoved = size - index - 1,这个numMoved有什么用呢?
    首先我们需要明白当删除数组中的一个元素时,数组中其它元素是可能需要移动的。假设一个size为6的数组,当我们删除index为3对应的元素后, index为4、5对应的元素需要向前移动,原本index为4的元素移动到3进行存储,index为5的元素移动到4进行存储,此时的数组就是在删除指定索引对应的元素之后得到的新数组。
    remove(int index)方法图示为我们展现了这一过程。
    正是由于这种删除元素后,其它元素可能需要移动的特性,导致当数组中元素较多时,一旦删除某个元素,则其它元素进行移动就需要付出较大代价。 注意:如果是删除数组中最后一个元素,则不存在上述问题。但若是删除数组中间的一个元素,则需要考虑这个代价。如果数组本身存储的元素较少则不必为这个代价而苦恼,
    如果存储的元素较多又想获取较高的添加、删除性能呢?那么我会推荐你使用LinkedList(想进一步学习?进入这个链接吧:http://www.cnblogs.com/seker/p/6921511.html)
    这里也介绍下arraycopy(Object src, int srcPos, Object dest, int destPos, int length)这个方法。src是指源数组,srcPos表示从源数组的哪个位置开始复制, dest是指目标数组,destPos表示目标数组的起始位置(从这个位置开始接收要复制的内容),length表示要复制的元素数量。 知道了从源数组哪个位置开始复制,那么又是到哪里结束呢?这其实是有一个计算公式的:对源数组来说,就是从srcPos开始,到srcPos + length - 1结束, 对目标数组来说,就是从destPos开始,到destPos + length - 1结束。
    */
    public E remove(int index) { RangeCheck(index); modCount++; E oldValue = (E) elementData[index]; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; return oldValue; } private void RangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException( "Index: "+index+", Size: "+size); }
    remove(int index)方法图示:

    /*
    remove(Object o)也是用来删除ArrayList中元素的方法,它和remove(int index)有什么差异呢?首先我们来看一下它的实现,它传入的是元素对象而非索引,
    通过for循环遍历数组中的元素,若匹配到则进行删除,我们可以看到fastRemove(int index)这个方法中的代码就是把remove(int index)方法中的部分代码进行了封装。
    需要注意的是如果数组中有多个一样的元素,则只会删除第一个匹配到的元素,这也是remove(Object o)这个方法的特点。
     */
    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; } 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; }

    4)add(int index, E element)、set(int index, E element)方法的区别是什么?

    --------------------未完,待更新--------------------

  • 相关阅读:
    四,redis6版本的使用部署
    记录篇-浪潮服务器raid卡
    sudo漏洞解决方案--源码转rpm包(spec文件编写)
    关闭 Chrome 浏览器阅读清单功能
    【转译】如何成为一个数据工程师?
    Python 用最小堆获取大量元素 topk 大个元素
    Python 实现二分查找
    Python 排序算法之堆排序,使用 heapq 实现
    Python 排序算法之归并排序
    Python 排序算法之快速排序
  • 原文地址:https://www.cnblogs.com/seker/p/6986668.html
Copyright © 2020-2023  润新知