我原来的第三篇笔记覆盖 ArrayList、LinkedList、Map、引用与泛型,但有些短结论容易让人用错。比如“LinkedList 删除快”,漏了怎么找到节点;“asList 返回列表”,漏了列表是否能增长。
这次以JDK 17为基线,先跑一个会产生异常的小程序,再把几个概念分开。并未测吞吐、对象大小或 GC 时机,不把 API 行为实验写成性能排名。
1. Arrays.asList:能set,为什么不能add?
原文写成 ArrayList 的 asList,实际上是 Arrays.asList。它返回由原数组支持的固定大小列表,不是一个可自动增长的 java.util.ArrayList。Arrays API。
下面完整程序分别观察视图和复制,文件名用 ListViewProbe.java:
importjava.util.*;publicclassListViewProbe{publicstaticvoidmain(String[]args){String[]array={"a","b"};List<String>view=Arrays.asList(array);List<String>copy=newArrayList<>(view);view.set(0,"x");System.out.println(Arrays.toString(array));array[1]="y";System.out.println(view);System.out.println(copy);try{view.add("z");thrownewAssertionError("add unexpectedly succeeded");}catch(UnsupportedOperationExceptionexpected){System.out.println("view cannot grow");}copy.add("z");System.out.println(copy);int[]primitive={1,2};List<int[]>oneElement=Arrays.asList(primitive);System.out.println(oneElement.size());System.out.println(oneElement.get(0)==primitive);}}实际输出:
[x, b] [x, y] [a, b] view cannot grow [a, b, z] 1 true| 对象 | 与原数组共享元素槽位? | 能增删长度? |
|---|---|---|
| Arrays.asList(array) | 是 | 否 |
| new ArrayList<>(view) | 容器不共享这些槽位 | 是 |
第二种是容器复制,不是元素深拷贝。若元素本身是可变对象,两份容器仍可能引用同一对象。int[] 传给 asList 则作为一个数组对象元素,不会自动逐项装箱成 List。
2. LinkedList删除快:先把定位成本说出来
ArrayList remove(i):定位下标 -> 移动后续元素 LinkedList remove(i):沿链找到位置 -> 改前后链接ArrayList 按下标 get 通常是 O(1),中间删除需要搬移,尾部删除是例外;尾部 add 的均摊成本为 O(1),扩容那一次仍需复制。
LinkedList 按下标访问需要从较近一端遍历,通常不能把 remove(i) 的整体成本写成 O(1)。已通过迭代器定位后的局部修改,和每次从下标重新查找,不是同一工作量。ArrayList、LinkedList。
实际选型还看访问模式、节点开销与缓存局部性,不能凭“链表不用搬运”断定它在所有数据规模下更快。本篇没有做基准测试。
3. Collection是接口,Collections是工具
Collection(接口) +-- List(接口) -> ArrayList、LinkedList(实现类) +-- Set(接口) -> HashSet(实现类) +-- Queue(接口) -> Deque(接口) Map 是另一条接口分支,不继承 Collection Collections 是工具类,不是上面的父接口这是简化关系图,不列全部继承路径。LinkedList 也实现 Deque,因此不能把所有集合强行放进互不相交的三个盒子。原文把 ArrayList、LinkedList 当成 Collection 的“子接口”,应改为实现类。
4. Map:null规则与并发保证分开看
| 实现 | null键/值 | 使用时的边界 |
|---|---|---|
| HashMap | 允许 | 不提供并发访问的整体安全保证 |
| Hashtable | 都不允许 | 方法同步,不代表任意多步组合都原子 |
| ConcurrentHashMap | 都不允许 | 并发容器,复合需求应选有相应保证的方法 |
这里不是性能排序,也不把旧版本的“分段锁”当作 JDK 17 的通用结构答案。举例:get 后再 put 两次独立调用之间仍可能被其他线程穿插;要对一个键累加,可用有原子更新语义的 merge,而不是凭容器线程安全就认为 get+put 自动成为一次操作。ConcurrentHashMap API。
importjava.util.*;importjava.util.concurrent.ConcurrentHashMap;publicclassMapProbe{publicstaticvoidmain(String[]args){Map<String,Integer>map=newHashMap<>();map.put("known",null);System.out.println(map.get("known")==null);System.out.println(map.get("missing")==null);System.out.println(map.containsKey("known"));System.out.println(map.containsKey("missing"));ConcurrentHashMap<String,Integer>concurrent=newConcurrentHashMap<>();concurrent.merge("hits",1,Integer::sum);concurrent.merge("hits",1,Integer::sum);System.out.println(concurrent.get("hits"));}}输出依次为 true、true、true、false、2。HashMap 的 get 返回 null,不足以区分“键不存在”和“键存在、值为 null”。此程序是顺序 API 演示,不是并发压测;原子性依据 API 保证,不能由两次顺序调用证明。
5. 泛型不是“子类集合自动变成父类集合”
Integer 是 Number 的子类,但 List 不是 List 的子类型。下面的故意错误代码应编译失败:
importjava.util.*;classBadGenericAssignment{voidtest(){List<Integer>integers=newArrayList<>();List<Number>numbers=integers;}}如果允许这个赋值,就可以通过 numbers 添加 Double,让原本声明只装 Integer 的列表混入另一种类型。泛型的编译期检查正是在阻止这个问题。JLS类型与泛型。
只需要读取 Number,可以用 extends;需要接收 Integer 写入,可以用 super。下面是完整的类型安全复制例子:
importjava.util.*;publicclassGenericCopyProbe{static<T>voidcopy(List<?extendsT>source,List<?superT>target){for(Titem:source)target.add(item);}publicstaticvoidmain(String[]args){List<Integer>source=List.of(1,2);List<Number>target=newArrayList<>();copy(source,target);System.out.println(target);}}输出 [1, 2]。泛型提高类型安全,不保证调用永远不抛异常:目标若是固定大小或不可修改列表,add 仍可能失败。类型边界与对象行为是两回事。
6. 原笔记的四种引用,补上时间边界
强可达对象不会因为“内存不足”就直接作为垃圾回收;这不等于永远不会回收。软、弱引用影响可达性判定,但不能据此承诺对象在某一行代码后立刻被回收。虚引用的 get 始终返回 null,结合队列观察引用处理,不是取回对象做一次普通调用。引用包说明、PhantomReference。
资源关闭优先使用明确生命周期与 try-with-resources,不把 GC 时机当成可靠的资源释放时钟。本次没有用 System.gc() 加等待来制造“确定回收”的结论。
7. 本次怎么验证
本地 JDK 17 实际编译正文三个可运行程序,核对输出和异常;泛型错误程序必须编译失败,另外验证接口继承关系与容器浅复制。未进行性能排名、内存布局测量或 GC 时序测试。
复习集合时,我现在更愿意追问三个问题:返回的是视图还是复制?快的是定位还是修改?保证的是单个操作还是整个业务步骤?把边界讲出来,才不是背了几句特点却仍然用错。