zoukankan      html  css  js  c++  java
  • ArrayList源码学习

    1、ArrayList:基于数据实现,允许出现空值和重复元素,当ArrayList中添加的元素数量大于底层数组容量是,会通过扩容机制重新生成一个更大的数组。(非线程安全)

    2、源码分析

    构造函数

        /**
         * 初始化容量
         */
        private static final int DEFAULT_CAPACITY = 10;
      //有参构造函数时使用,当初始化容量=0时,默认是一个空数组
        private static final Object[] EMPTY_ELEMENTDATA = {};
      //无参构造时默认空数组
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
      //ArrayList中基础的数组对象
        transient Object[] elementData; 
      //数组大小
        private int size;
      //指定容量的构造函数
        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;
        }
    
      //Collection类型的集合 , 集合为空时,ArrayList默认空数组
        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;
            }
        }

    插入:

    public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!! 检测是否需要扩容
            elementData[size++] = e; //将新增的元素添加到数组的末尾
            return true;
    }
      //在指定索引础添加新的元素
        public void add(int index, E element) {
            rangeCheckForAdd(index); //检测给出的索引是否合法
    
            ensureCapacityInternal(size + 1);  // Increments modCount!! 检测是否需要扩容
         //将index和之后的元素整体向后移动一位
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
         //上一步移动后 index位置的元素和index+1位置的元素相同(index+1位置的元素就是未移动之前的index位置的元素),将新元素插入到index位置 elementData[index]
    = element; size++; }   //计算容量 private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; }   //检测是否需要扩容 private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); }   //扩容 private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }

    对于在元素序列尾部插入,这种情况比较简单,只需两个步骤即可:

    1. 检测数组是否有足够的空间插入
    2. 将新元素插入至序列尾部

    如下图:

    如果是在元素序列指定位置(假设该位置合理)插入,则情况稍微复杂一点,需要三个步骤:

    1. 检测数组是否有足够的空间
    2. 将 index 及其之后的所有元素向后移一位
    3. 将新元素插入至 index 处

    如下图:

    从上图可以看出,将新元素插入至序列指定位置,需要先将该位置及其之后的元素都向后移动一位,为新元素腾出位置。这个操作的时间复杂度为O(N)频繁移动元素可能会导致效率问题,特别是集合中元素数量较多时。在日常开发中,若非所需,我们应当尽量避免在大集合中调用第二个插入方法

    扩容:(扩容比例  原数组大小的1.5倍)

       private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);  //原数组大小的1.5倍
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // minCapacity is usually close to size, so this is a win: 新容量计算出来之后 复制原数组中的元素到新数组中
            elementData = Arrays.copyOf(elementData, newCapacity);//方法内部会新建一个newCapacity大小的新数组  然后调用System.arrcopy()方法进行复制
        }
    
        private static int hugeCapacity(int minCapacity) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
        }

    删除:

       
      //删除指定索引的元素
      public E remove(int index) { rangeCheck(index);//校验index是否合法 modCount++;//该字段存储arraylist的修改次数 E oldValue = elementData(index);//暂存要删除索引位置的元素 int numMoved = size - index - 1;//index位置到数组最后的长度 为了system.arraycopy复制方法使用,将index+1位置和之后的元素 整体向前移动一位 if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work 将数组中最后位置置空 return oldValue; }   //删除指定元素 (删除ArrayList中索引最小的指定元素) public boolean remove(Object o) {
         //元素为空的话 循环删除ArrayList中索引最小的null值
    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(index)方法 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 }

    上面的删除方法并不复杂,这里以第一个删除方法为例,删除一个元素步骤如下:

    1. 获取指定位置 index 处的元素值
    2. 将 index + 1 及之后的元素向前移动一位
    3. 将最后一个元素置空,并将 size 值减 1
    4. 返回被删除值,完成删除操作

    如下图:

    上面就是删除指定位置元素的分析,并不是很复杂。

    特殊情况:

    往ArrayList中插入大量的元素,然后又删除很多元素,此时ArrayList底层的数组占用了很多空间没有释放,因为ArrayList没有自动缩容机制,导致底层空间不能被释放,造成浪费

    此时调用ArrayList的方法:

        
    //size ArrayList的大小 elementData底层数组的长度
    public void trimToSize() { modCount++; if (size < elementData.length) {//如果size小于elementData 代表数组中空着很多位置,需要进行缩容 elementData = (size == 0) //size == 0时代表,ArrayList中没有元素,可以将element置为空数组 ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } }

    通过上面的方法,我们可以手动触发 ArrayList 的缩容机制。这样就可以释放多余的空间,提高空间利用率。

    遍历:

    ArrayList 实现了 RandomAccess 接口(该接口是个标志性接口),表明它具有随机访问的能力。ArrayList 底层基于数组实现,所以它可在常数阶的时间内完成随机访问,效率很高。对 ArrayList 进行遍历时,一般情况下,我们喜欢使用 foreach 循环遍历,但这并不是推荐的遍历方式。ArrayList 具有随机访问的能力,如果在一些效率要求比较高的场景下,更推荐下面这种方式:

    for (int i = 0; i < list.size(); i++) {
        list.get(i);
    }

    至于原因也不难理解,foreach 最终会被转换成迭代器遍历的形式,效率不如上面的遍历方式

    判断ArrayList是否有指定元素:

       
      //有指定元素返回true 也就是indexOf()方法返回值大于等于0
      public boolean contains(Object o) { return indexOf(o) >= 0; } //类似remove和fastremove方法 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; }

    清空ArrayList:

        
      //地层elementData中的元素全部置null size(ArrayList)长度置0
      public void clear() { modCount++;        // clear to let GC do its work for (int i = 0; i < size; i++) elementData[i] = null; size = 0; }

    其他:

    关于遍历时删除

    遍历时删除是一个不正确的操作,即使有时候代码不出现异常,但执行逻辑也会出现问题。关于这个问题,阿里巴巴 Java 开发手册里也有所提及。这里引用一下:

    【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。

    相关代码(稍作修改)如下:

    List<String> a = new ArrayList<String>();
        a.add("1");
        a.add("2");
        for (String temp : a) {
            System.out.println(temp);
            if("1".equals(temp)){
                a.remove(temp);
            }
        }
    }

    相信有些朋友应该看过这个,并且也执行过上面的程序。上面的程序执行起来不会虽不会出现异常,但代码执行逻辑上却有问题,只不过这个问题隐藏的比较深。我们把 temp 变量打印出来,会发现只打印了数字12没打印出来。初看这个执行结果确实很让人诧异,不明原因。如果死抠上面的代码,我们很难找出原因,此时需要稍微转换一下思路。我们都知道 Java 中的 foreach 是个语法糖,编译成字节码后会被转成用迭代器遍历的方式。所以我们可以把上面的代码转换一下,等价于下面形式:

    List<String> a = new ArrayList<>();
    a.add("1");
    a.add("2");
    Iterator<String> it = a.iterator();
    while (it.hasNext()) {
        String temp = it.next();
        System.out.println("temp: " + temp);
        if("1".equals(temp)){
            a.remove(temp);
        }
    }

    这个时候,我们再去分析一下 ArrayList 的迭代器源码就能找出原因。

    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;
    
        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];
        }
        
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
        
        // 省略不相关的代码
    }

    我们一步一步执行一下上面的代码,第一次进入 while 循环时,一切正常,元素 1 也被删除了。但删除元素 1 后,就无法再进入 while 循环,此时 it.hasNext() 为 false。原因是删除元素 1 后,元素计数器 size = 1,而迭代器中的 cursor 也等于 1,从而导致 it.hasNext() 返回false。归根结底,上面的代码段没抛异常的原因是,循环提前结束,导致 next 方法没有机会抛异常。不信的话,大家可以把代码稍微修改一下,即可发现问题:

    List<String> a = new ArrayList<>();
    a.add("1");
    a.add("2");
    a.add("3");
    Iterator<String> it = a.iterator();
    while (it.hasNext()) {
        String temp = it.next();
        System.out.println("temp: " + temp);
        if("1".equals(temp)){
            a.remove(temp);
        }
    }

    以上是关于遍历时删除的分析,在日常开发中,我们要避免上面的做法。正确的做法使用迭代器提供的删除方法,而不是直接删除。

    本文内容引自:该博客  建议读原博客

  • 相关阅读:
    【css】怎么让Chrome支持小于12px 的文字
    java操作linux,调用shell命令
    20个非常有用的Java程序片段
    Java集合详解
    SVN使用指南
    利用SQL语句查询数据库中所有表
    HttpClient-03Http状态管理
    HttpClient-02连接管理
    HttpClient-01基本概念
    IDEA安装插件
  • 原文地址:https://www.cnblogs.com/nxzblogs/p/10674026.html
Copyright © 2011-2022 走看看