首页 > 编程语言 >Java中ArrayList的扩容机制

Java中ArrayList的扩容机制

时间:2023-02-02 23:12:33浏览次数:66  
标签:扩容 Java int ArrayList elementData minCapacity 数组 size

1.简介

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

ArrayList的底层基于数组来实现,故具备一些数组的基本特性,例如:

  • 获取元素效率较高:可以直接通过索引查找
  • 插入元素效率较低:节点后的数据都需要往后移动

关于数组的更多解释,你可以参考这篇文章:数组的底层原理

1.1 类图

在IDEA中按下ctrl + alt + u的组合键,我们可以查看到ArrayList的类图。

image-20230202204332683

这里对UML图形做简单介绍:

实线三角箭头:继承(图中,蓝色为类的继承,绿色为接口的继承)

虚线三角箭头:实现

关于继承和实现此处不做过多的解释,现在我们描述下直接节点的几个含义:

  • RandomAccess

    标志接口,表明实现这个接口的 List 集合是支持快速随机访问的,数组特性决定可以快速访问。

  • Cloneable

    覆盖了函数clone(),能被克隆。

  • Serializable

    支持序列化,能通过序列化去传输。

1.2 属性

    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * 默认初始容量大小
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 空数组(用于空实例)。
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 空数组
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 保存ArrayList数据
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * 所包含的元素个数
     */
    private int size;

2.基础知识

我们知道,List和Array的特性都是数据能有序存放,但与数组不同的是,List还可以动态扩容。

在ArrayList中,数据是通过内部的Object[]数组来存放的。

2.1 System.arraycopy()

public static native void arraycopy(Object 源数组, int 源数组复制的起始位置;, Object 目的数组, int 目的数组放置的起始位置, int 复制的长度);

这是一个native方法,存放于java.lang包下,看名字我们应该能猜到,它能实现数组的复制,例如:

    public static void main(String[] args) {
        int[] arrA = {0, 1, 2};
        int[] arrB = {3, 4, 5, 6, 7, 8};

        System.arraycopy(arrA, 1, arrB, 2, 2);
        System.out.println(Arrays.toString(arrB));
    }

将arrA数组1号位开始的2个元素,复制到arrB数组2号位。

程序输出:

[3, 4, 1, 2, 7, 8]

通过这个native方法,我们可以实现数组的扩容,比如现在我想把一个数组扩容到原来的两倍。

    public static void main(String[] args) {
        int[] arrA = {7, 8, 9};
        int[] arrB = new int[arrA.length * 2];

        System.arraycopy(arrA,0,arrB,0,arrA.length);
        System.out.println(Arrays.toString(arrB));
    }

程序输出:

[7, 8, 9, 0, 0, 0]

2.2 Arrays.copyOf()

以char为例,下面是源代码。

public static char[] copyOf(char[] original, int newLength) {
    char[] copy = new char[newLength];
    System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
    return copy;
}

该方法其实就是调用了上面的native方法,返回了复制过后的数组。

其实实现模式跟我们自己写的一样,只不过它用的是复制的长度做了判断。

// 源数组和新数组中的较小值,即:可以扩容,也可以缩容.
Math.min(original.length, newLength)

下面是一个示例:

    public static void main(String[] args) {
        int[] arrC = {6, 8, 9};
        
        // newLength为5
        int[] charNew = Arrays.copyOf(arrC, 5);
        System.out.println(Arrays.toString(charNew));

        // newLength为2
        charNew = Arrays.copyOf(arrC, 2);
        System.out.println(Arrays.toString(charNew));
    }

程序输出:

[6, 8, 9, 0, 0]
[6, 8]

3.构造方法

查看源码可以知道,我们在new ArrayList()的时候,是不会执行扩容操作的。

image-20230202212908675

3.1 ArrayList()

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

3.2 ArrayList(int initialCapacity)

    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);
        }
    }

3.3 ArrayList(Collection<? extends E> c)

    public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        // 注意此处的size = a.length,更新了当前ArrayList对象的size值.
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            elementData = EMPTY_ELEMENTDATA;
        }
    }

3.4 总结

通过构造方法可以看到,我们在new ArrayList()时,是不会触发扩容操作的。

构造时,无非就是给内部的elementData这个对象数组(属性)赋值。不必困扰transient关键字,就是当前字段不参与序列化的标识。

   transient Object[] elementData;

放张小图,红色部分代表数据值被修改(write),绿色值代表数据值被使用(read)。

如何配置可以参考我这篇文章:IDEA中高亮显示变量的使用位置和赋值位置

image-20230202213814421

image-20230202213848341

4.扩容条件

ArrayList的扩容操作是在add()时进行触发的,我们可以查看add方法的源码。

	public boolean add(E e) {
        ensureCapacityInternal(size + 1);
        elementData[size++] = e;
        return true;
    }

在add开始前,会执行ensureCapacityInternal(size + 1)和elementData[size++] = e两个操作,然后返回true。

4.1 void ensureCapacityInternal(int minCapacity)

确保内部容量不小于minCapacity

ensureCapacityInternal方法又关联了ensureExplicitCapacity和calculateCapacity两个方法。

	private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
	
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

	private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

我们先对calculateCapacity方法做解释。

4.2 int calculateCapacity(Object[] elementData, int minCapacity)

计算所需容量

该方法用于计算容量,传入一个数组和int值。

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 若传入的elementData为默认的空数组
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // 则返回默认容量DEFAULT_CAPACITY(10) 和 minCapacity 中的较大值.
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        // 否则返回 minCapacity
        return minCapacity;
    }

什么时候会是空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA呢?返回上面的构造方法处可以看到是在无参构造中使用到了。

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
  • elementData是无参构造器初始化的,返回值是minCapacity或者默认容量10。
  • elementData不是无参构造器初始化的,返回值是minCapacity。

4.3 void ensureExplicitCapacity(int minCapacity)

确保显示容量不小于minCapacity

    private void ensureExplicitCapacity(int minCapacity) {
        // 记录arrayList被处理的次数,暂时可以忽略.
        modCount++;

        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

从上面4.1处可以看到,这个方法中形参的值(int minCapacity)是由calculateCapacity()方法计算来的。

如果,所需要的容量大于当前对象数组的长度,就执行扩容操作。

4.4 总结

现在我们把3个方法串起来,在进行add()操作时,必然会触发首行的ensureCapacityInternal()方法,传递的参数值是size+1

size是什么?size就是当前arrayList对象的元素个数,我们不是经常使用list.size()吗?在arrayList的源码中,它什么也没做,就是一个简单的get方法。

    public int size() {
        return size;
    }

所以,在add()操作时,ensureCapacityInternal()方法传入了当前元素个数+1的值进去。

add()操作又是干嘛的?将元素添加到list的末尾。

我们要放元素到末尾,那么必须得先确保size+1这个节点是可用的,即用来存放数据的对象数组最小容量是size+1

现在,我们需要根据这个最小容量minCapacity做后续操作,即ensureExplicitCapacity()方法。

        if (minCapacity - elementData.length > 0)
            grow(minCapacity);

如果当前对象数组的长度小于所需最小容量,则执行扩容操作。

5.扩容操作

扩容操作是通过grow(minCapacity)方法完成的。

	private void grow(int minCapacity) {
        // 旧容量
        int oldCapacity = elementData.length;
        // oldCapacity >> 1,位运算,8>>1=4,11>>1=5
        // 新容量: 约为原始容量的1.5倍,10->15,15->22
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        
        // 如果计算出的新容量比所需的最小minCapacity小,扩容到所需最小容量就行了.
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 如果计算出的新容量比MAX_ARRAY_SIZE还要大,执行hugeCapacity()
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        
        // 实际扩容操作,将elementData对象数组扩容到newCapacity,见2.2节.
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    private static int hugeCapacity(int minCapacity) {
        // OOM
        if (minCapacity < 0) throw new OutOfMemoryError();
        // Integer.MAX_VALUE = 0x7fffffff = 214,748,3647.
        return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
    }

标签:扩容,Java,int,ArrayList,elementData,minCapacity,数组,size
From: https://www.cnblogs.com/yang37/p/17087701.html

相关文章

  • java介绍、环境搭建与Hello,World!
    java的诞生C与C++C语言1972年贝尔实验室操作系统、编译器等偏底层应用指针和内存管理漏洞C++1982年面向对象对C兼容在图形领域、游戏领域等方面常用jav......
  • JavaScript学习笔记—DOM:元素的添加、修改、删除
    appendChild(node):向节点添加最后一个子节点insertAdjacentHTML(position,text):把元素插入到指定位置position:beforebegin-插入到当前元素的前面,即开始标签之前a......
  • Java super关键字
    java中的super关键字是一个引用变量,用于引用直接父类对象。每当创建子类的实例时,父类的实例被隐式创建,由super关键字引用变量引用。javasuper关键字的用法如下:super......
  • Java方法重写
    如果子类中具有与父类中声明相同的方法,在java中称为方法覆盖。换句话说,如果子类提供了由其父类提供的其中一个方法的特定实现,则它被称为方法覆盖。所以方法覆盖有两个前提......
  • Java方法重载
    如果一个类中有多个具有相同名称但参数不同的方法,则称为方法重载。如果只需要执行一个操作,具有相同的方法名称将增加程序的可读性。假设必须执行给定数值的添加操作(求和......
  • 《深入理解Java虚拟机》第三章读书笔记(二)——HotSpot垃圾回收算法实现(OopMap,安全点安
    系列文章目录和关于我前面《深入理解Java虚拟机》第三章读书笔记(一)——垃圾回收算法我们学习了垃圾回收算法理论知识,下面我们关注下HotSpot垃圾回收算法的实现,分为以下几......
  • 使用VSCode创建Maven工程测试Java代码
    使用VSCode创建Maven工程测试Java代码发生缘由使用VSCode创建Maven工程测试Java代码环境介绍电脑系统:win10VSCode版本:1.72.0(usersetup)开始搭建搭建......
  • Java 基础语法
    @目录Java基础语法标识符&关键字数据类型1.数据类型的介绍2.类型转换变量运算符包机制JavaDoc文档注释Scanner类流程控制1.1if选择结构1.2switch(匹配)选择结构2.1......
  • JAVA-基础-包
    java、javax、org、sun包都是jdk提供的类包,且都是在rt.jar中。rt.jar是JAVA基础类库(java核心框架中很重要的包),包含lang在内的大部分功能,而且rt.jar默认就在根classloader的......
  • 细节决定成败:探究Mybatis中javaType和ofType的区别
    开启掘金成长之旅!这是我参与「掘金日新计划·12月更文挑战」的第24天,点击查看活动详情一.背景描述今天,壹哥给学生讲解了Mybatis框架,学习了基础的ORM框架操作及多对一......