1. 从Arrays.asList开始:你以为的List可能不是List
第一次使用Arrays.asList()时,我天真地以为它返回的就是个普通ArrayList。直到某天尝试调用add()方法时,那个UnsupportedOperationException异常直接给我整不会了。这玩意儿居然是个固定大小的列表!
Arrays.asList()返回的是Arrays内部类ArrayList,不是我们熟悉的java.util.ArrayList。这个内部类继承自AbstractList,但没实现add/remove等方法。更坑的是,它直接引用了原始数组:
java复制String[] arr = {"a", "b"};
List<String> list = Arrays.asList(arr);
arr[0] = "modified"; // 这里修改会影响list!
重要提示:如果需要可修改的List,应该这样用:
java复制new ArrayList<>(Arrays.asList("a", "b"))
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. subList的隐藏陷阱:你以为的切片其实是视图
.subList()方法返回的也不是独立列表,而是原列表的视图。这意味着:
- 对子列表的修改会影响原列表
- 原列表结构变化会导致子列表操作抛ConcurrentModificationException
java复制List<Integer> source = new ArrayList<>(Arrays.asList(1,2,3,4));
List<Integer> sub = source.subList(1, 3);
sub.set(0, 99); // source现在变成[1,99,3,4]
source.add(5); // 结构改变
sub.get(0); // 这里会抛异常!
3. 集合判空:size()==0和isEmpty()的微妙差异
新手常犯的错误是用size()==0判断空列表。虽然大多数情况下没问题,但有些自定义List实现可能重写isEmpty()有特殊逻辑。比如:
java复制List<String> list = new LinkedList<>() {
@Override
public boolean isEmpty() {
return size() == 0 && someOtherCondition();
}
};
最佳实践:始终优先使用isEmpty(),除非你明确知道需要检查size()==0
4. 遍历删除:用Iterator还是removeIf?
直接for循环删除元素会出大问题:
java复制for(int i=0; i<list.size(); i++){
if(condition(list.get(i))){
list.remove(i); // 漏删+越界风险!
}
}
正确姿势:
java复制// JDK8+
list.removeIf(item -> condition(item));
// 传统写法
Iterator<String> it = list.iterator();
while(it.hasNext()){
if(condition(it.next())){
it.remove();
}
}
5. 浅拷贝之痛:new ArrayList(list)的陷阱
很多人以为new ArrayList<>(list)是深拷贝,其实它只拷贝了引用:
java复制List<User> original = new ArrayList<>();
original.add(new User("张三"));
List<User> copy = new ArrayList<>(original);
copy.get(0).setName("李四"); // original里的张三也被改了!
真正的深拷贝需要:
java复制List<User> deepCopy = original.stream()
.map(User::clone)
.collect(Collectors.toList());
6. contains的坑:equals与hashCode必须成对实现
这个错误我见过无数次:
java复制class Person {
String id;
// 忘了重写equals和hashCode
}
List<Person> list = new ArrayList<>();
list.add(new Person("123"));
list.contains(new Person("123")); // 返回false!
血泪教训:放在集合里的对象,必须正确实现equals和hashCode
7. 排序的玄学:Comparable vs Comparator
List排序看似简单,但坑不少:
- 元素没实现Comparable时调用Collections.sort()会抛ClassCastException
- 用Comparator时注意null值处理
- 并行排序可能不稳定
java复制list.sort((a,b) -> {
if(a == null) return b == null ? 0 : -1;
if(b == null) return 1;
return a.compareTo(b);
});
8. 线程安全幻觉:Collections.synchronizedList不是万能药
以为用了synchronizedList就线程安全?太天真了!复合操作仍需同步:
java复制List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// 这个操作仍然不安全!
if(!syncList.contains("a")) {
syncList.add("a");
}
// 正确做法
synchronized(syncList) {
if(!syncList.contains("a")) {
syncList.add("a");
}
}
9. 性能巨坑:LinkedList真的比ArrayList快?
教科书说LinkedList插入快,但实际测试可能颠覆认知:
- 随机访问:ArrayList快1000倍以上
- 头部插入:LinkedList快
- 尾部插入:两者相当
- 内存占用:LinkedList多50%左右
java复制// 实测代码示例
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
long start = System.nanoTime();
for(int i=0; i<100000; i++){
arrayList.add(0, i); // 头部插入
}
System.out.println("ArrayList耗时:"+(System.nanoTime()-start)/1_000_000+"ms");
start = System.nanoTime();
for(int i=0; i<100000; i++){
linkedList.add(0, i); // 头部插入
}
System.out.println("LinkedList耗时:"+(System.nanoTime()-start)/1_000_000+"ms");
10. 终极巨坑:List.toArray()的数组类型问题
这个坑真的惊掉下巴!看代码:
java复制List<String> list = Arrays.asList("a", "b");
Object[] objects = list.toArray(); // 正常
String[] strings = list.toArray(); // 编译通过,运行抛ArrayStoreException!
// 正确写法
String[] rightWay = list.toArray(new String[0]);
原因在于toArray()返回的是Object[],不能强制转为String[]。而toArray(T[])的实现有特殊处理:
- 传入数组长度不足时会新建数组
- 长度足够时直接复用
- 所以最佳实践是传new T[0]而不是new T[size]
11. 番外篇:List与Stream的转换陷阱
Java8之后常见的坑:
java复制List<Integer> list = Arrays.asList(1,2,3);
Stream<Integer> stream = list.stream();
list.add(4); // 修改原列表
stream.forEach(System.out::println); // 可能不包含新增元素
// 正确做法:需要确定性的结果时先collect
List<Integer> snapshot = new ArrayList<>(list);
snapshot.stream().forEach(...);
12. 防御性编程实战建议
根据多年踩坑经验,总结这些最佳实践:
- 任何来自外部的List参数,先用new ArrayList<>()包装
- 返回不可变列表时用Collections.unmodifiableList()
- 多线程环境考虑使用CopyOnWriteArrayList
- 大列表查找用HashSet辅助(空间换时间)
- 定期用Collections.synchronizedList包装检查线程安全
java复制// 防御性编程示例
public void process(List<String> input) {
// 防御性拷贝
List<String> workingCopy = new ArrayList<>(Objects.requireNonNull(input));
// 业务逻辑...
// 返回不可变结果
return Collections.unmodifiableList(result);
}
13. 性能优化小技巧
几个实测有效的优化手段:
- ArrayList初始化时指定容量(减少扩容)
- 批量操作使用addAll替代循环add
- 频繁插入删除考虑LinkedList(但要先实测!)
- 并行处理用parallelStream(数据量大时)
- 排序前检查是否已有序
java复制// 容量优化示例
List<String> list = new ArrayList<>(1000); // 已知要存1000元素
// 批量操作优化
List<String> toAdd = ...;
list.addAll(toAdd); // 比循环add快3-5倍
14. 常见面试题精讲
这些List相关问题经常出现在面试中:
- ArrayList和LinkedList的区别?
- 从底层结构、访问复杂度、内存占用等方面回答
- 快速失败(fail-fast)机制是什么?
- 讲解ConcurrentModificationException的产生原理
- 如何实现一个不可变List?
- Collections.unmodifiableList的局限
- Guava的ImmutableList实现原理
- List去重有哪些方法?
- Stream.distinct()
- 转为HashSet再转回
- 使用LinkedHashSet保持顺序
15. 工具推荐与诊断技巧
开发中实用的工具和方法:
- IDEA的Debugger可视化查看List结构
- 使用Collections.disjoint检查集合是否有交集
- Apache Commons CollectionsUtils工具类
- Guava的Lists.partition分割大列表
- 使用JMH进行集合操作性能测试
java复制// 使用Guava分割大列表示例
List<Integer> bigList = ...;
List<List<Integer>> partitions = Lists.partition(bigList, 1000);
16. 真实案例:线上事故复盘
分享一个我经历的真实事故:
某次使用subList获取分页数据,结果原列表被GC回收后,subList操作导致内存泄漏。原因是subList会强引用原列表,解决方案:
java复制// 错误用法
List<Data> page = bigList.subList(start, end);
storeInCache(page); // 导致bigList无法回收
// 正确做法
List<Data> page = new ArrayList<>(bigList.subList(start, end));
17. Java8/11/17中的List新特性
现代Java版本带来的改进:
- Java9的List.of()创建不可变列表
- Java11的toArray(IntFunction)简化数组转换
- Java17的stream().toList()(返回不可变列表)
- 模式匹配instanceof简化列表元素类型检查
java复制// Java17新特性示例
if(list.get(0) instanceof String s) {
System.out.println(s.toLowerCase());
}
18. 其他JVM语言中的List实现
对比其他JVM语言的处理:
- Kotlin的List默认不可变,mutableListOf创建可变列表
- Scala的List是不可变的,使用ListBuffer可变
- Groovy的列表操作更灵活(支持负索引等)
kotlin复制// Kotlin示例
val immutable = listOf(1,2,3)
val mutable = mutableListOf(1,2,3)
19. 终极解决方案:什么时候不用List?
有时候List不是最佳选择:
- 需要去重 → Set
- 需要键值对 → Map
- 需要双端操作 → Deque
- 需要优先级 → PriorityQueue
- 超大数据量 → 数据库分页
最后记住:没有完美的数据结构,只有适合场景的选择。理解每种实现的特性,才能写出健壮的代码。
