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

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

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

(0)
上一篇 2022年6月7日 上午6:00
下一篇 2022年6月7日 上午6:00


相关推荐

  • 协同过滤推荐算法在python上的实现

    1.引言信息大爆炸时代来临,用户在面对大量的信息时无法从中迅速获得对自己真正有用的信息。传统的搜索系统需要用户提供明确需求,从用户提供的需求信息出发,继而给用户展现信息,无法针对不同用户的兴趣爱好提供相应的信息反馈服务。推荐系统相比于搜索系统,不需要提供明确需求,便可以为每个用户实现个性化推荐结果,让每个用户更便捷地获取信息。它是根据用户的兴趣特点和购买行为,向用户推荐用户感兴趣…

    2022年4月6日
    69
  • MT60B1G16HC-48B:A美光内存颗粒FBGA代码D8BNK[通俗易懂]

    MT60B1G16HC-48B:A美光内存颗粒FBGA代码D8BNK[通俗易懂]MT60B1G16HC-48B:A美光内存颗粒FBGA代码D8BNK美光科技宣布已开始批量出货全球首款基于176层NAND技术的通用闪存UFS3.1移动解决方案。该产品为高端旗舰手机量身打造,与前代产品相比可实现高达75%的顺序写入和随机读取性能提升,从而解锁5G潜力—只需9.6秒即可下载一部2小时的4K电影。美光176层NAND的紧凑尺寸,完美契合移动设备的大容量和小体积需求。此前,美光于6月宣布已出货搭载176层NAND的PCIe4.0SS

    2022年6月22日
    28
  • 【免费领取】OpenClaw 国内部署完全指南,打造你的专属 AI 数字助手(文末有获取方法)

    【免费领取】OpenClaw 国内部署完全指南,打造你的专属 AI 数字助手(文末有获取方法)

    2026年3月15日
    2
  • 用matlab导入excel数据画图_matlab导入数据并绘图

    用matlab导入excel数据画图_matlab导入数据并绘图MATLAB导入Excel数据并用plot函数绘图第一次写博客,心里有点小激动!写这一篇博客的目的是帮助像我一样刚入门的小白,因为昨天查了相关博客,但是发现和我想找的还是比较少的,所以特此写一篇来总结一下我摸索出来的经验。第一步:打开matlab并找导入数据这一项第二步:点击并找到需要导入的excel文件第三步:导入并选中需要导入工作区的数据第四步:用plot绘图其他关于mat…

    2022年10月15日
    5
  • java工程师薪资待遇_java软件工程师一个月挣多少钱

    java工程师薪资待遇_java软件工程师一个月挣多少钱Java软件工程师工资待遇详解时间:2018-08-16来源:未知Java软件工程师工资待遇情况怎么样?Java软件工程师的工资水平与哪些因素有关呢?今天小编从这两方面和大家来进行分析一下Java工程师工资待遇。Java软件工程师工资待遇情况据有关数据显示,目前,我国对软件人才的需求已达20万,并且以每年20%左右的速度增长。在未来5年内,合格软件人才的需求将远大于供给。2016年的时候…

    2026年3月2日
    4
  • vsftp 用户_共享提示用户账户限制

    vsftp 用户_共享提示用户账户限制背景Oracle全库备份,异地备份在实现异地备份后,由第三方人员登录服务器拉取dmp文件.为了确保安全,创建一个特定ftp账号用于第三方人员使用要求1.可以登录服务器2.可以拉取dmp文件3.仅限在dmp文件的目录下,不能cd其他路径,ls其他目录解决过程yum安装ftp服务[root@78778e06dc0a/]#yuminstallvsftpd-y修改vsftp配置文件,开启限制[…

    2026年3月9日
    4

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

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