Java集合之ArrayList扩容机制

Java集合之ArrayList扩容机制ArrayList的构造函数//默认初始容量大小(默认能添加10条数据)privatestaticfinalintDEFAULT_CAPACITY=10;//默认实例化一个空数组privatestaticfinalObject[]DEFAULTCAPACITY_EMPTY_ELEMENTDATA={};//默认构造函数,使用初始容量为10构造一个空列表(无参数构造…

大家好,又见面了,我是你们的朋友全栈君。

ArrayList的构造函数

//默认初始容量大小(默认能添加10条数据)
private static final int DEFAULT_CAPACITY = 10;
//默认实例化一个空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//默认构造函数,使用初始容量为10构造一个空列表(无参数构造)
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
//带初始容量参数的构造函数。(用户自己指定容量)
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);
        }
    }    
//构造包含指定collection元素的列表,这些元素利用该集合的迭代器按照顺序返回,如果指定的集合为空,则抛出NullPointerException

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时,实际上初始化赋值的是一个空数组(public ArrayList())。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为10

一步步分析ArrayList扩容机制

  • 先来看Add方法
/**
*将指定的元素追加到此列表的末尾
*/
public boolean add(E e) {
		//添加元素之前,先调用ensureCapacityInternal方法
        ensureCapacityInternal(size + 1);  // Increments modCount!!(增量modCount)
        //这里看到ArrayList添加元素的实质就相当于为数组赋值
        elementData[size++] = e;
        return true;
    }
  • 再来看看ensureCapacityInternal()方法
    可以看到add()方法首先调用了ensureCapacityInternal(size+1)
//得到最小扩容量
private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        		//获取默认的容量和传入参数的较大值(第一次的较大值是DEFAULT_CAPACITY=10,minCapacity=1)
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

当要add进第一个元素时,minCapacity为1,在Math.max()方法比较后,minCapacity为10

  • ensureExplicitCapacity()方法
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
        //调用grow()方法进行扩容,调用此方法代表已经开始扩容了
            grow(minCapacity);
    }

我们来仔细分析一下

  1. 当我们要add进第一个元素到ArrayList时,elementData.length为0(因为还是一个空的list,里面还没有数据,所以没有进行扩容,默认扩容10),因为执行了ensureCapacityInternal()方法,所以minCapacity此时为10。此时,minCapacity – elemetData.length > 0(minCapacity=10,elemetData.length=0)成立,所以会进入==grow(minCapacity)==方法。
  2. 当add第2个元素时,minCapacity为2,此时elementData.length(容量)在添加第一个元素后扩容成10了。此时,minCapacity – elementData.length > 0不成立,所以不会进入(执行)==grow(minCapacity)==方法。
  3. 添加第3、4…到第10个元素时,依然不会执行==grow()==方法,数组容量都为10。
    知道添加第11个元素,minCapacity(为11)比elementData.length(为10)要大。进行grow方法进行扩容
  4. grow方法
private void grow(int minCapacity) {
        // oldCapacity为旧容量,newCapacity为新容量
        int oldCapacity = elementData.length;//(0,10,15)
        //将oldCapacity右移一位,其效果相当于oldCapacity/2;
        // 我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么久把最小需要容量当作数组的新容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
            //判断新容量是否大于集合的最大容量(一般大不了)
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 给elementData从新赋值(10,15)
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍!

“>>”(移位运算符):>>1 右移一位相当于除2,右移n位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了1位所以相当于oldCapacity /2。对于大数据的2进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源

通过例子探究一下grow()方法

  • 当add第一个元素时,oldCapacity为0,经比较后第一个if判断成立,newCapacity = minCapacity(为10)。但是第二个if判断不会成立,即newCapacity不比MAX_ARRAY_SIZE大,则不会进入hugeCapacity方法。数组容量为10,add方法中return true,size增为1。
  • 当add第11个元素进入grow方法时,newCapacity为15,比minCapacity(为11)大,第一个if判断不成立。新容量没有大于数组最大size,不会进入hugeCapacity方法。数组容量扩为15,add方法中rerurn,true,size增为11。
  • 以此类推…
    这里补充一点比较重要,但是容易被忽视掉的知识点:
  • java中的length属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了length这个属性。
  • java中的length() 方法是针对字符串说的,如果想看这个字符串的长度则用到 length() 这个方法。
  • java中的size() 方法是针对泛型集合说的,如果想看这个泛型有多少元素,就调用此方法类查看!

System.arraycopy() 方法

// 将指定的元素插入此列表中的指定位置。  
// 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加1)。  
public void add(int index, E element) {  
   if (index > size || index < 0)  
       throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);  
   // 如果数组长度不足,将进行扩容。  
   ensureCapacity(size+1);  // Increments modCount!!  
   // 将 elementData中从Index位置开始、长度为size-index的元素,  
   // 拷贝到从下标为index+1位置开始的新的elementData数组中。  
   // 即将当前位于该位置的元素以及所有后续元素右移一个位置。  
   // //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量;
   System.arraycopy(elementData, index, elementData, index + 1, size - index);  
   elementData[index] = element;  
   size++;  
}

插入数据

public static void main(String[] args)
{
    List<String> list = new ArrayList<String>();
    list.add("111");
    list.add("222");
    list.add("333");
    list.add("444");
    list.add("555");
    list.add("666");
    list.add("777");
    list.add("888");
    list.add(2, "000");
    System.out.println(list);
}

看到插入的时候,按照指定位置,把从指定位置开始的所有元素利用System.arraycopy方法做一个整体的复制,向后移动一个位置(当然先要用ensureCapacity方法进行判断,加了一个元素之后数组会不会不够大),然后指定位置的元素设置为需要插入的元素,完成了一次插入的操作。用图表示这个过程是这样的
插入数据

在这个方法中最根本的方法就是System.arraycopy()方法,该方法的根本目的就是将index位置空出来以供新数据插入,这里需要进行数组数据的右移,这是非常麻烦和耗时的,所以如果指定的数据集合需要进行大量插入(中间插入)操作,推荐使用LinkedList。

删除数据

public E remove(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(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; // clear to let GC do its work

        return oldValue;
    }
public static void main(String[] args)
{
    List<String> list = new ArrayList<String>();
    list.add("111");
    list.add("222");
    list.add("333");
    list.add("444");
    list.add("555");
    list.add("666");
    list.add("777");
    list.add("888");
    list.remove("333");
}

删除数据
1、把指定元素后面位置的所有元素,利用System.arraycopy方法整体向前移动一个位置

2、最后一个位置的元素指定为null,这样让gc可以去回收它

int[]a = new int[10];
        a[0] =0;
        a[1] =1;
        a[2] =2;
        a[3] =3;
        a[4]=4;
        a[5]=5;
        a[6]=6;
        a[7]=7;
        a[8]=8;
        a[9]=9;
       System.arraycopy(a,2,a,3,7);
        for (int i=0;i<a.length;i++){
            System.out.println(i+"****"+a[i]);
        }

从原数组(a)中第2位开始往后面取三位(2.3.4),然后copy到新数组(这里还是a)中,从第3位开始放(3.4.5被替换成原数组的2.3.4)

2019-04-15 17:50:27.658 28361-28361/? I/System.out: 0****0
2019-04-15 17:50:27.658 28361-28361/? I/System.out: 1****1
2019-04-15 17:50:27.658 28361-28361/? I/System.out: 2****2
2019-04-15 17:50:27.658 28361-28361/? I/System.out: 3****2
2019-04-15 17:50:27.658 28361-28361/? I/System.out: 4****3
2019-04-15 17:50:27.658 28361-28361/? I/System.out: 5****4
2019-04-15 17:50:27.658 28361-28361/? I/System.out: 6****5
2019-04-15 17:50:27.658 28361-28361/? I/System.out: 7****6
2019-04-15 17:50:27.658 28361-28361/? I/System.out: 8****7
2019-04-15 17:50:27.658 28361-28361/? I/System.out: 9****8

参考文献

https://juejin.im/post/5ba1a3156fb9a05d1478178b

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

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/134309.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • url传递参数_url encode

    url传递参数_url encodeWerkzeug之URL路由原文链接http://werkzeug.pocoo.org/docs/0.12/routing/当需要组合控制器和视图函数时,我们需要一个调度器来实现。一个简单的实现方式是采用正则表达式匹测试路由信息,调用回调函数并返回结果。Werkzeug提供了一个类似Route[1]的强大功能.下文提到的所有对象都是从werkzeug.routing导入而不是

  • 热插拔——矿机先行利器[通俗易懂]

    热插拔——矿机先行利器[通俗易懂]IPFSFilecoin上线在即,准备挖矿的小伙伴们已近磨刀霍霍了,都在积极选择自己心仪的矿机。但是如今市场上矿机众多,对于矿机的配置也是众说纷纭,相信许多的小伙伴也是十分茫然,当然,星际魔方今天只谈专业IPFS矿机,家用电脑组装的矿机我们后期再谈。工欲善其事,必先利其器。Fliecoin挖矿就是一种优质资源竞争的行为。形象理解就类似于嘀嘀打车,一个人想去另一个地方,在滴滴下单,司机开始抢单…

  • K8S架构详解

    K8S架构详解

  • pycharm2022.01 linux激活码[最新免费获取]

    (pycharm2022.01 linux激活码)JetBrains旗下有多款编译器工具(如:IntelliJ、WebStorm、PyCharm等)在各编程领域几乎都占据了垄断地位。建立在开源IntelliJ平台之上,过去15年以来,JetBrains一直在不断发展和完善这个平台。这个平台可以针对您的开发工作流进行微调并且能够提供…

  • ASP.net Session原理「建议收藏」

    ASP.net Session原理「建议收藏」1.创建SessionMgr类usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Web;//////SessionMgr的摘要说明///publicclassSessionMgr{privatestaticIDiction

  • Spring Boot发生java.lang.AbstractMethodError解决方案

    Spring Boot发生java.lang.AbstractMethodError解决方案Exceptioninthread“main”java.lang.AbstractMethodError问题描述:Exceptioninthread&quot;main&quot;java.lang.AbstractMethodError:org.springframework.boot.context.config.ConfigFileApplicationListener.support…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号