乐于分享
好东西不私藏

数组内存剖析、ArrayList扩容源码 & 生产坑点、LinkedList底层双向链表、Vector/Stack淘汰原因

数组内存剖析、ArrayList扩容源码 & 生产坑点、LinkedList底层双向链表、Vector/Stack淘汰原因

前言

很多人学习集合只停留在:add、remove、get 简单调用,不懂为什么这样设计。

本章节一次性吃透 List 家族全部成员:

  1. 普通数组:JVM内存布局、隐藏致命缺陷、开发禁忌;

  2. ArrayList:JDK8底层源码、空数组机制、扩容临界点、线上高频BUG;

  3. LinkedList:双向链表结构、首尾缓冲节点、真实性能真相;

  4. Vector & Stack:被淘汰的底层原因、反人类设计;

  5. 冷门但极强的高级写法、生产禁用写法、阿里编码规约。


一、数组:所有集合的底层基石

1.1 数组内存底层布局

数组是Java中唯一连续内存、固定长度的容器。

  • 栈内存:保存数组引用地址;

  • 堆内存:真实数组对象、连续紧凑排布;

  • 依据下标定位,物理内存寻址,查询时间复杂度 O(1)。

1.2 数组不可忽略的隐性缺陷

这也是集合诞生的根本原因:

  1. 长度一旦初始化不可修改,无法动态扩容;

  2. 中间增删元素必须大规模平移数组,性能极差;

  3. 原生数组无封装方法,无法快速查找、批量操作;

  4. 基本类型数组存在默认值污染,业务不好判空。

1.3 开发高频易错点

❌ 错误写法:数组赋值混淆引用

int[] arr1 = {1,2,3};int[] arr2 = arr1; arr2[0] = 99;// arr1 同步被修改!二者指向同一堆内存

✅ 正确写法:深度复制

int[] arr2 = Arrays.copyOf(arr1,arr1.length);

1.4 不常用但极强的数组写法

生产中极少有人用,但源码大量出现:

// 1. 数组快速转List(大坑:定长集合,不可增删)List<Integer> list = Arrays.asList(1,2,3);// 2. 数组批量拷贝(底层native方法,性能极致)System.arraycopy(src,0,dest,0,len);// 3. 数组扩容本质:新建数组+内存拷贝int[] newArr = Arrays.copyOf(oldArr,oldArr.length+5);

重点提醒:Arrays.asList() 返回的不是ArrayList,是内部静态类,不支持add/remove,修改直接报错。


二、ArrayList:生产中集合坑点

2.1 底层源码结构(JDK8)

底层依托:transient Object[] elementData 动态数组。

核心隐秘规则

  1. 空参构造:初始赋值EMPTY_ELEMENTDATA 空数组,容量为0;

  2. 第一次add触发扩容,容量变为 10;

  3. 正常扩容:扩容1.5倍(位运算:oldCapacity + (oldCapacity >> 1));

  4. 大批量add:不死板1.5倍,直接按需扩容;

  5. 最大容量:Integer.MAX_VALUE - 8,防止内存溢出。

2.2 ArrayList 生产高频BUG

坑点1:遍历过程中删除元素(并发修改异常)

❌ 错误写法(foreach/普通for删除必报错)

for (String s : list) {    if("a".equals(s)){        list.remove(s);    }}// 抛出 ConcurrentModificationException

✅ 正确写法:迭代器删除

Iterator<String> iterator = list.iterator();while (iterator.hasNext()){    if("a".equals(iterator.next())){        iterator.remove();    }}

坑点2:subList 截取返回原集合视图(强引用坑)

很多人以为 subList 是新集合,本质是原数组映射视图,修改子集同步影响原集合。

List<String> sub = list.subList(0,2);sub.clear(); // 原集合数据直接被清空

坑点3:无参构造频繁扩容造成内存浪费

预估数据量情况下,必须指定初始容量,减少扩容复制开销。

// 预估1000条,直接初始化容量,避免多次扩容ArrayList<String> list = new ArrayList<>(1000);

2.3 ArrayList 优缺点总结

  • ✅ 连续内存、随机访问极快、遍历性能顶尖;

  • ✅ 尾插尾删效率极高;

  • ❌ 中间插入删除需要平移数组,性能差;

  • ❌ 线程不安全,modCount 快速失败机制;

  • ❌ 扩容产生新数组,存在临时内存占用峰值。


三、LinkedList:双向链表底层结构 & 真实使用场景

3.1 底层源码结构

底层:双向链表,维护 Node 内部节点。

private static classNode<E> {    E item;    Node<E> next;    Node<E> prev;}

隐秘优化点

  1. 维护 first、last 首尾节点,头尾增删不需要遍历;

  2. 查询中间节点采用二分遍历:靠前从头查、靠后从尾查;

  3. 无数组、无扩容、无连续内存。

3.2 LinkedList性能

错误观点:LinkedList 增删都快。

真实真相:

  • 头尾增删:O(1),极快;

  • 根据下标中间增删:先要遍历寻址,效率极低 O(n);

  • 遍历性能远弱于 ArrayList,无连续内存、CPU缓存不命中。

3.3 LinkedList 专属使用场景

  1. 高频头尾插入、删除;

  2. 实现队列、双端队列、栈结构;

  3. 数据量巨大、无法预估容量、不适合数组连续内存。


四、ArrayList VS LinkedList 对比

对比维度
ArrayList
LinkedList
底层结构
动态数组
双向链表
内存分布
连续内存、CPU缓存友好
散乱内存、缓存失效严重
查询速度
极致快 O(1)
慢 O(n)
中间增删
慢(数组平移)
慢(先要寻址)
头尾增删
尾部增删快,头部增删慢
极快
扩容机制
1.5倍自动扩容
无扩容
内存占用
紧凑、无多余开销
每个节点额外双指针,开销大
线程安全
不安全
不安全

**生产铁律:**业务开发99%场景直接使用 ArrayList,不要盲目使用 LinkedList。


五、Vector & Stack:淘汰集合底层设计缺陷

5.1 Vector(ArrayList 古老版本)

底层缺陷

  1. 所有方法加 synchronized 重量级锁,并发效率极低;

  2. 扩容机制:直接扩容 2倍,内存浪费严重;

  3. 设计老旧、代码冗余、无优化。

现代替代方案

并发安全List:CopyOnWriteArrayList(读写分离、写时复制 + 全局锁(ReentrantLock))

5.2 Stack(栈集合)

  1. 直接继承 Vector,底层数组+全局锁;

  2. 后进先出,但是性能极差;

  3. 继承设计不合理,破坏栈封装性。

官方推荐替代

栈、队列统一使用:ArrayDeque,性能吊打 Stack。


六、冷门但生产必备的高级写法

6.1 集合批量初始化(优雅简化代码)

// JDK9+ 快速初始化(不可变集合)List<String> list = List.of("a","b","c");

6.2 集合转数组(避免强转异常)

// 正确:传入空数组,底层自动适配String[] arr = list.toArray(new String[0]);

6.3 不可变集合(防止恶意修改)

List<String> unmodifiable = Collections.unmodifiableList(list);

七、生产避坑规约|阿里Java开发手册硬性要求

  1. 禁止使用 Vector、Stack,一律使用现代集合;

  2. 预估数据量必须指定 ArrayList 初始容量,减少扩容次数;

  3. 禁止 foreach 循环内删除元素,必须使用迭代器;

  4. Arrays.asList 结果禁止增删,如需可变集合,手动new ArrayList包装;

  5. subList 返回视图,严禁强转ArrayList,严禁修改原集合;

  6. 多线程环境禁止直接使用 ArrayList,选用 CopyOnWriteArrayList。


八、全文总结

  1. 数组连续内存、长度固定,是所有List集合底层原型;

  2. ArrayList 动态数组、1.5倍扩容、查询无敌、中间增删弱;

  3. LinkedList 双向链表、头尾极强、查询拉胯、内存散乱;

  4. Vector锁太重、扩容浪费,Stack继承畸形,全部淘汰;

  5. 遍历删元素必用迭代器,subList为视图不可乱改;

  6. 日常业务无脑ArrayList,特殊队列场景用LinkedList。

相关学习资料