ArrayList内部原理

简介

ArrayList 是一种变长的基于数组实现的集合类,允许空值和重复元素,当往 ArrayList 中添加的元素数量大于其底层数组容量时,它会自动扩容至一个更大的数组。

另外,由于 ArrayList 底层基于数组实现,所以其可以保证在 O(1) 复杂度下完成随机查找操作。其他方面,ArrayList 是非线程安全类,并发环境下,多个线程同时操作 ArrayList,会引发不可预知的错误。

ArrayList 是大家最为常用的集合类,我们先来看下常用的方法:

List<String> dataList = new ArrayList<>();//创建 ArrayList
dataList.add("test");//添加数据
dataList.add(1,"test1");//指定位置,添加数据
dataList.get(0);//获取指定位置的数据
dataList.remove(0);//移除指定位置的数据
dataList.clear();//清空数据

 

构造方法

ArrayList 有两个构造方法,一个是无参,另一个需传入初始容量值。大家平时最常用的是无参构造方法,相关代码如下:

private static final int DEFAULT_CAPACITY = 10; // 初始容量为 10
private static final Object[] EMPTY_ELEMENTDATA = {};// 一个空对象
transient Object[] elementData; //当前数据对象存放地方,当前对象不参与序列化
private int size; // 当前数组长度

public ArrayList(int initialCapacity) {
    super();
    if (initialCapacity < 0) {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity];
}

public ArrayList() {
    super();
    this.elementData = EMPTY_ELEMENTDATA;
}

上面的代码比较简单,两个构造方法做的事情并不复杂,目的都是初始化底层数组 elementData。区别在于无参构造方法会将 elementData 初始化一个空数组,插入元素时,扩容将会按默认值重新初始化数组。而有参的构造方法则会将 elementData 初始化为参数值大小(>= 0)的数组。

 

add()

对于数组(线性表)结构,插入操作分为两种情况。一种是在元素序列尾部插入,另一种是在元素序列其他位置插入。

  • 尾部插入元素
/** 在元素序列尾部插入 */
public boolean add(E e) {
    // 1. 检测是否需要扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 2. 将新元素插入序列尾部
    elementData[size++] = e;
    return true;
}

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

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

如下图:

  • 指定位置插入元素
/** 在元素序列 index 位置处插入 */
public void add(int index, E element) {
    if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    // 1. 检测是否需要扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 2. 将 index 及其之后的所有元素都向后移一位
    // arraycopy(被复制的数组, 从第几个元素开始, 复制到哪里, 从第几个元素开始粘贴, 复制的元素个数)
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    // 3. 将新元素插入至 index 处
    elementData[index] = element;
    size++;
}

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

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

如下图:

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

 

扩容机制

下面就来简单分析一下 ArrayList 的扩容机制,对于变长数据结构,当结构中没有空余空间可供使用时,就需要进行扩容。在 ArrayList 中,当空间用完,其会按照原数组空间的 1.5 倍进行扩容。相关源码如下:

/** 计算最小容量 */
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

/** 扩容的核心方法 */
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // newCapacity = oldCapacity + oldCapacity / 2 = oldCapacity * 1.5
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 进行扩容
    elementData = Arrays.copyOf(elementData, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    // 如果最小容量超过 MAX_ARRAY_SIZE,则将数组容量扩容至 Integer.MAX_VALUE
    return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

上面就是扩容的逻辑,逻辑很简单,这里就不赘述了。

 

get()

public E get(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    return (E) elementData[index];
}

get 的逻辑很简单,就是检查是否越界,根据 index 获取元素。

 

remove()

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)
        // 将 index + 1 及之后的元素向前移动一位,覆盖被删除值
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // 将最后一个元素置空,并将 size 值减 1     
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

E elementData(int index) {
    return (E) elementData[index];
}

/** 删除指定元素,若元素重复,则只删除下标最小的元素 */
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; // clear to let GC do its work
}

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

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

如下图:

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

现在,考虑这样一种情况。我们往 ArrayList 插入大量元素后,又删除很多元素,此时底层数组会空闲处大量的空间。因为 ArrayList 没有自动缩容机制,导致底层数组大量的空闲空间不能被释放,造成浪费。对于这种情况,ArrayList 也提供了相应的处理方法,如下:

/** 将数组容量缩小至元素数量 */
public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}

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

 

clear()

public void clear() {
    modCount++;

    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

clear 的逻辑很简单,就是遍历一下将所有的元素设置为空。

 

contains()

public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

/** 挨个比较,调用元素的equals方法,返回相同元素的下标 */
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;
}

 

参考:https://blog.csdn.net/freekiteyu/article/details/88539560

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
附有面试讲解视频,不是网盘,下载既有视频,屡试不爽的面试宝典。 Java面试题01.面试的整体流程 Java面试题02.java的垮平台原理 Java面试题03.搭建一个java的开发环境 Java面试题04.java中int占几个字节 Java面试题05.java面向对象的特征 Java面试题06.装箱和拆箱 Java面试题07.==和equals的区别 Java面试题08.String Java面试题09.讲一下java中的集合 Java面试题10.ArrayList 和LinkedList的区别 Java面试题11.HashMap和HashTable的区别 Java面试题12.实现一个拷贝文件的工具类要使用字节流还是字符串 Java面试题13.线程的的实现方式?怎么启动线程?怎么区分线程? Java面试题14.线程并发库和线程池的作用 Java面试题15.设计模式和常用的设计模式 Java面试题16.http get post请求的区别 Java面试题17.说说你对Servlet的理解 Java面试题18.Servlet的生命周期 Java面试题19.Servlet中forward和redirect的区别 Java面试题20.jsp和Servlet的相同点和不同点 Java面试题21.内置对象和四大作用域和页面传值 Java面试题22.Session和Cookie的区别和使用场景 Java面试题23.mvc模式和mvc各部分的实现 Java面试题24.数据库分类和常用数据库 Java面试题25.关系型数据库的三范式 Java面试题26.事务的四大特征 Java面试题27.mysql数据库最大连接数 Java面试题28.mysql和oracle的分页语句 Java面试题29.触发器的使用场景? Java面试题30.存储过程的优点 Java面试题31.jdbc调用存储过程 Java面试题32.简单说一下你对jdbc的理解 Java面试题33.写一个jdbc的访问oracle的列子 Java面试题34.jdbc中preparedStatement比Statement的好处 Java面试题35.数据库连接池的作用 Java面试题36.HTML Java面试题37.简单介绍了一下Ajax Java面试题38.js和JQuery的关系 Java面试题39.jQuery中的常用选择器 Java面试题40.jQuery中页面加载完毕事件 Java面试题41.jQuery中Ajax和原生js实现Ajax的关系 Java面试题42.简单说一下html5 Java面试题43.简单说一下css3 Java面试题44.bootstrap的是什么 Java面试题45.什么是框架 Java面试题46.简单介绍一下MVC模式 Java面试题47.简单说一下对mvc框架的理解 Java面试题48.struts2的执行流程或者struts2的原理 Java面试题49.Struts2的拦截器是什么 Java面试题50.Spring MVC的执行流程 Java面试题51.SpringMVC和Struts2的不同 Java面试题52.简单介绍一下Spring或者Spring的两大核心 Java面试题53.AOP是什么?都用它做什么? Java面试题54.Spring事务的传播特性和隔离级别 Java面试题55.ORM是什么?ORM框架是什么? Java面试题56.ibatis和hibernate有什么不同 Java面试题57.hibernate对象状态及其转换 Java面试题58:hibernate的缓存 Java面试题59.webservice的使用场景 Java面试题60.activiti的简单介绍 Java面试题61.linux的使用场景 Java面试题62.linux常用命令 Java面试题63:怎么操作linux服务器 Java面试题64:有没有使用过云主机 Java面试题65:数据库优化方面的事情 Java面试题66:如果查询和定位慢查询 Java面试题67:数据库优化之数据库表设计遵循范式 Java面试题68:选择合适的数据库引擎 Java面试题69:选择合适的索引 Java面试题70:使用索引的一些技巧 Java面试题71:数据库优化之分表 Java面试题72:数据库的读写分离 Java面试题73:数据库优化之缓存 Java面试题74:sql语句优化小技巧 Java面试题75:批量插入几百万条数据 Java面试题76:有没有使用过redis Java面试题77:redis的使用场景 Java面试题78:redis存储对象的方式 Java面试题79:redis数据淘汰机制 Java面试题80:java访问redis级redis集群 ......

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值