ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java集合与泛型:从Arrays.asList的异常,看清视图、复制和类型边界

Java集合与泛型:从Arrays.asList的异常,看清视图、复制和类型边界 我原来的第三篇笔记覆盖 ArrayList、LinkedList、Map、引用与泛型但有些短结论容易让人用错。比如“LinkedList 删除快”漏了怎么找到节点“asList 返回列表”漏了列表是否能增长。这次以JDK 17为基线先跑一个会产生异常的小程序再把几个概念分开。并未测吞吐、对象大小或 GC 时机不把 API 行为实验写成性能排名。1. Arrays.asList能set为什么不能add原文写成 ArrayList 的 asList实际上是 Arrays.asList。它返回由原数组支持的固定大小列表不是一个可自动增长的 java.util.ArrayList。Arrays API。下面完整程序分别观察视图和复制文件名用 ListViewProbe.javaimportjava.util.*;publicclassListViewProbe{publicstaticvoidmain(String[]args){String[]array{a,b};ListStringviewArrays.asList(array);ListStringcopynewArrayList(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};Listint[]oneElementArrays.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. Mapnull规则与并发保证分开看实现null键/值使用时的边界HashMap允许不提供并发访问的整体安全保证Hashtable都不允许方法同步不代表任意多步组合都原子ConcurrentHashMap都不允许并发容器复合需求应选有相应保证的方法这里不是性能排序也不把旧版本的“分段锁”当作 JDK 17 的通用结构答案。举例get 后再 put 两次独立调用之间仍可能被其他线程穿插要对一个键累加可用有原子更新语义的 merge而不是凭容器线程安全就认为 getput 自动成为一次操作。ConcurrentHashMap API。importjava.util.*;importjava.util.concurrent.ConcurrentHashMap;publicclassMapProbe{publicstaticvoidmain(String[]args){MapString,IntegermapnewHashMap();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));ConcurrentHashMapString,IntegerconcurrentnewConcurrentHashMap();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(){ListIntegerintegersnewArrayList();ListNumbernumbersintegers;}}如果允许这个赋值就可以通过 numbers 添加 Double让原本声明只装 Integer 的列表混入另一种类型。泛型的编译期检查正是在阻止这个问题。JLS类型与泛型。只需要读取 Number可以用 extends需要接收 Integer 写入可以用 super。下面是完整的类型安全复制例子importjava.util.*;publicclassGenericCopyProbe{staticTvoidcopy(List?extendsTsource,List?superTtarget){for(Titem:source)target.add(item);}publicstaticvoidmain(String[]args){ListIntegersourceList.of(1,2);ListNumbertargetnewArrayList();copy(source,target);System.out.println(target);}}输出 [1, 2]。泛型提高类型安全不保证调用永远不抛异常目标若是固定大小或不可修改列表add 仍可能失败。类型边界与对象行为是两回事。6. 原笔记的四种引用补上时间边界强可达对象不会因为“内存不足”就直接作为垃圾回收这不等于永远不会回收。软、弱引用影响可达性判定但不能据此承诺对象在某一行代码后立刻被回收。虚引用的 get 始终返回 null结合队列观察引用处理不是取回对象做一次普通调用。引用包说明、PhantomReference。资源关闭优先使用明确生命周期与 try-with-resources不把 GC 时机当成可靠的资源释放时钟。本次没有用 System.gc() 加等待来制造“确定回收”的结论。7. 本次怎么验证本地 JDK 17 实际编译正文三个可运行程序核对输出和异常泛型错误程序必须编译失败另外验证接口继承关系与容器浅复制。未进行性能排名、内存布局测量或 GC 时序测试。复习集合时我现在更愿意追问三个问题返回的是视图还是复制快的是定位还是修改保证的是单个操作还是整个业务步骤把边界讲出来才不是背了几句特点却仍然用错。
返回列表