数据结构-ArrayList原理

1. ArrayList 对比数组能做些什么?

众所周知,Java 中数组能用来批量存储同一类对象,但是数组在使用中会有所限制。数组需要在声明的时候就需要声明长度。按照长度从内存中申请一整片连续的内存。

因为内存是连续的所以数组的检索非常快,可以快速的通过数组下标找到对应下标在内存中的位置。

同时也因为内存是连续的导致数组无法进行扩容,例如声明了一个长度为 10 的数组,此时已经放进去了 10 个元素,想要放入第 11 个元素,但是数组后的内存块可能已经被占用,就会导致无法扩容。

ArrayList 的出现就是为了解决数组无法自动扩容的问题,ArrayList 在其内部维护了一个数组,在需要进行扩容时便会申请一个容量更大的数组同时将老数组的数据拷贝到新数组。

2. ArrayList 方法

2.1 构造方法

构造器 描述
ArrayList() 构造一个初始容量为10的空列表。10 仅为默认容量,实际数组指向空数组
ArrayList(int initialCapacity) 构造具有指定初始容量的空列表。
ArrayList(Collection<? extends E> c) 按照集合的迭代器返回的顺序构造一个包含指定集合元素的列表。

2.2 具体方法(主要方法)

变量和类型 方法 描述
boolean add(E e) 将指定的元素追加到此列表的末尾。
void add(int index, E element) 将指定元素插入此列表中的指定位置。
E remove(int index) 删除此列表中指定位置的元素。
void clear() 从此列表中删除所有元素。
void ensureCapacity(int minCapacity) 如有必要,增加此 ArrayList
boolean removeAll(Collection<?> c) 从此列表中删除指定集合中包含的所有元素。
boolean removeIf(Predicate<? super E> filter) 删除此集合中满足给定谓词的所有元素。
boolean retainAll(Collection<?> c) 仅保留此列表中包含在指定集合中的元素。

2.3 重点方法原理讲解

add(E e)

  • 判断数组是否是空数组(新建 ArrayList 时,其中的数组默认指向空数组)

    • 是:获取当前数组需要的最小容量: minCapacity= max(默认初始容量10, 存量元素数+增量元素数)
    • 否:获取当前数组需要的最小容量:minCapacity=存量元素数+增量元素数
  • 查看当前需要的数组最小容量是否小于数组长度

    • 是:插入数据

    • 否:

      • 计算新的容量:newCapacity=oldCapacity+(oldCapacity>>1) 约为之前 1.5 倍

        • 新容量 newCapacity 是否大于当前所需最小容量 minCapacity

          • 是:将 minCapacity 赋值给新容量 newCapacity
          • 否:newCapacity 保持不变
      • 根据获取到的新容量 newCapacity 生成新数组

      • 将老数组数据拷贝到新数组

      • 插入数据


newCapacity=oldCapacity+(oldCapacity>>1) 进行了位运算,将 oldCapacity 的二进制向右移了一位。 例如

● 7 的二进制表示为 111,二进制向右移一位变为 11 对应十进制 3

● 8 的二进制表示位 1000,二进制向右移一位变为 100 对应十进制 4

可见对于奇数,二进制最后一位为 1,做右移一位操作会丢掉最后一位的 1 同时除以 2:n >> 1 = (n - 1) / 2 对于偶数,二进制最后一位为 0,做右移一位操作无影响,仅为原先数值处以 2:n >> 1 = n / 2


如果需要连续多次的调用 add(E e) 添加元素,可以调用 ensureCapacity(int minCapacity) 输入期待数组的最小容量让 ArrayList 提前进行扩容,避免进行多次扩容

add(int index, E element)

  • 插入新元素先确保容量是否足够插入新元素

    • 否:先进行扩容
  • 将要插入下标往后的元素统一往后挪一个下标

  • 将新元素放到对应下标


疑问:将数组 2, 3, 4, 5 往后移动是怎么做的?

如果是正常思路可能是利用遍历依次将 5 往后挪一位,再将 4 往后挪一位,然后一次挪动 3, 2

但是这样可能会造成 cpu 资源的浪费,

为此查看 ArrayList 代码发现底层调用了,System 的本地静态方法

static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)

直接将原数组 2, 3, 4, 5 所在的内存块统一往后挪了一个单位,这样仅需要进行一次移动。

可以对比两种方式的性能差异

java 复制代码
// 设置较大容量,避免数组扩容影响
ArrayList<Integer> list = new ArrayList<>(20000);
for (int i = 1; i <= 10000; i++) {
    list.add(i);
}
list.add(null);
long start = System.currentTimeMillis();

/**
* 第一种方式:
* 循环调用往后挪,耗时 3~5 ms
**/
for (int i = 9999; i >= 1 ; i--) {
    list.set(i + 1, list.get(i));
}
list.set(1, 2);
/**
* 第二种方式
* 直接拷贝一整块内存,耗时 0ms
**/
list.add(1, 2);

System.out.println(System.currentTimeMillis() - start);

remove(int index)

在删除元素过程中,底层数组不会进行缩容

3. 总结

ArrayList 的扩缩容主要利用 System 的本地静态方法

static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)

进行一次数据块的拷贝 + 新建数组实现。

实际扩容,会扩容为原容量的 1.5 倍,所以如果需要进行多次增操作,需要提前评估数组容量避免多次扩容。

相关推荐
lizhou8287 分钟前
win10下使用docker、k8s部署java应用
java·docker·kubernetes
程序员阿鹏1 小时前
ArrayList 与 LinkedList 的区别?
java·开发语言·后端·eclipse·intellij-idea
18你磊哥1 小时前
java重点学习-JVM类加载器+垃圾回收
java·jvm
聂 可 以1 小时前
在SpringBoot项目中利用Redission实现布隆过滤器(布隆过滤器的应用场景、布隆过滤器误判的情况、与位图相关的操作)
java·spring boot·redis
长安初雪1 小时前
Java客户端SpringDataRedis(RedisTemplate使用)
java·redis
aloha_7891 小时前
B站宋红康JAVA基础视频教程(chapter14数据结构与集合源码)
java·数据结构·spring boot·算法·spring cloud·mybatis
尘浮生2 小时前
Java项目实战II基于Java+Spring Boot+MySQL的洗衣店订单管理系统(开发文档+源码+数据库)
java·开发语言·数据库·spring boot·mysql·maven·intellij-idea
java_heartLake2 小时前
微服务中间件之Nacos
后端·中间件·nacos·架构
猿饵块2 小时前
cmake--get_filename_component
java·前端·c++
编程小白煎堆2 小时前
C语言:枚举类型
java·开发语言