前言
很多人学习集合只停留在:add、remove、get 简单调用,不懂为什么这样设计。
本章节一次性吃透 List 家族全部成员:
普通数组:JVM内存布局、隐藏致命缺陷、开发禁忌;
ArrayList:JDK8底层源码、空数组机制、扩容临界点、线上高频BUG;
LinkedList:双向链表结构、首尾缓冲节点、真实性能真相;
Vector & Stack:被淘汰的底层原因、反人类设计;
冷门但极强的高级写法、生产禁用写法、阿里编码规约。
一、数组:所有集合的底层基石
1.1 数组内存底层布局
数组是Java中唯一连续内存、固定长度的容器。
栈内存:保存数组引用地址;
堆内存:真实数组对象、连续紧凑排布;
依据下标定位,物理内存寻址,查询时间复杂度 O(1)。
1.2 数组不可忽略的隐性缺陷
这也是集合诞生的根本原因:
长度一旦初始化不可修改,无法动态扩容;
中间增删元素必须大规模平移数组,性能极差;
原生数组无封装方法,无法快速查找、批量操作;
基本类型数组存在默认值污染,业务不好判空。
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 动态数组。
核心隐秘规则
空参构造:初始赋值
EMPTY_ELEMENTDATA空数组,容量为0;第一次add触发扩容,容量变为 10;
正常扩容:扩容1.5倍(位运算:oldCapacity + (oldCapacity >> 1));
大批量add:不死板1.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;}
隐秘优化点
维护 first、last 首尾节点,头尾增删不需要遍历;
查询中间节点采用二分遍历:靠前从头查、靠后从尾查;
无数组、无扩容、无连续内存。
3.2 LinkedList性能
错误观点:LinkedList 增删都快。
真实真相:
头尾增删:O(1),极快;
根据下标中间增删:先要遍历寻址,效率极低 O(n);
遍历性能远弱于 ArrayList,无连续内存、CPU缓存不命中。
3.3 LinkedList 专属使用场景
高频头尾插入、删除;
实现队列、双端队列、栈结构;
数据量巨大、无法预估容量、不适合数组连续内存。
四、ArrayList VS LinkedList 对比
**生产铁律:**业务开发99%场景直接使用 ArrayList,不要盲目使用 LinkedList。
五、Vector & Stack:淘汰集合底层设计缺陷
5.1 Vector(ArrayList 古老版本)
底层缺陷
所有方法加 synchronized 重量级锁,并发效率极低;
扩容机制:直接扩容 2倍,内存浪费严重;
设计老旧、代码冗余、无优化。
现代替代方案
并发安全List:CopyOnWriteArrayList(读写分离、写时复制 + 全局锁(ReentrantLock))
5.2 Stack(栈集合)
直接继承 Vector,底层数组+全局锁;
后进先出,但是性能极差;
继承设计不合理,破坏栈封装性。
官方推荐替代
栈、队列统一使用: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开发手册硬性要求
禁止使用 Vector、Stack,一律使用现代集合;
预估数据量必须指定 ArrayList 初始容量,减少扩容次数;
禁止 foreach 循环内删除元素,必须使用迭代器;
Arrays.asList 结果禁止增删,如需可变集合,手动new ArrayList包装;
subList 返回视图,严禁强转ArrayList,严禁修改原集合;
多线程环境禁止直接使用 ArrayList,选用 CopyOnWriteArrayList。
八、全文总结
数组连续内存、长度固定,是所有List集合底层原型;
ArrayList 动态数组、1.5倍扩容、查询无敌、中间增删弱;
LinkedList 双向链表、头尾极强、查询拉胯、内存散乱;
Vector锁太重、扩容浪费,Stack继承畸形,全部淘汰;
遍历删元素必用迭代器,subList为视图不可乱改;
日常业务无脑ArrayList,特殊队列场景用LinkedList。
夜雨聆风