ARTICLE DETAIL

资讯详情

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

唯品会 Java 开发手册(vjtools)之集合处理:12 条规约与 vjkit 源码级实践

唯品会 Java 开发手册(vjtools)之集合处理:12 条规约与 vjkit 源码级实践 开发工具可观测性后端【免费下载链接】vjtoolsThe vip.coms java coding standard, libraries and tools项目地址https://gitcode.com/gh_mirrors/vj/vjtools点击查看免费下载导读本文基于《唯品会 Java 开发手册》vjtools 仓库 docs/standard/chapter08.md中的集合处理章节系统讲解 Java 集合在初始化、遍历、修改、并发、泛型与类型转换等场景下的 12 条开发规约。每一条规约都配有强制/推荐的等级标识、正确与错误的代码对比以及对应的 Sonar / Facebook-Contrib 检查规则文中还结合 vjkit 工具库中 MapUtil、ListUtil、CollectionUtil 等源码实现说明规约在工程中的落地方式。读完本文你将掌握数组扩容与加载因子的量化计算方法、foreach 与 Iterator 的正确取舍、并发集合的安全使用姿势以及 Array 与 List 互转的全部正确写法。规约总览本章共 12 条规约按强制程度分为两类等级规约核心主题推荐Rule 1数组型集合指定初始大小推荐Rule 2使用 foreach 遍历强制Rule 3foreach 中禁止 remove/add强制Rule 4用 entrySet 遍历 Map强制Rule 5集合场景重写 hashCode/equals强制Rule 6注意各 Map 对 null 的容忍度强制Rule 7长生命周期集合防内存泄漏强制Rule 8并发修改用线程安全集合推荐Rule 9正确使用泛型通配符推荐Rule 10List / List? / List推荐Rule 11有限 Key 用 EnumMap推荐Rule 12Array 与 List 互转的正确写法Rule 1. 【推荐】底层数据结构是数组的集合指定集合初始大小底层数据结构为数组的集合包括ArrayList、HashMap、HashSet、ArrayDeque等。数组有大小限制当元素数超过容量时会触发复制式扩容新申请一个容量为原来的 150% 或 200% 的数组把原内容整体复制过去既浪费内存又损失性能。而对 HashMap / HashSet 来说扩容还需要将所有键值对重新计算哈希并落位开销更大。默认构造函数使用默认的数组大小例如 ArrayList 默认 10HashMap 默认 16。因此建议使用ArrayList(int initialCapacity)、HashMap(int initialCapacity)等构造函数明确初始化大小。加载因子的量化计算HashMap / HashSet 的初始值还必须考虑加载因子load factor哈希冲突的本质是Key 的哈希值按数组大小取模后若落在同一个数组下标上就会组成一条需要遍历的 Entry 链在 JDK 8 中为链表或红黑树拖慢 get 性能默认加载因子为 0.75即当键值对数量达到数组大小的75%时就会触发扩容因此若预估容量是 100应设定数组大小为100 / 0.75 1 135。vjkit 的落地MapUtil 封装容量计算vjkit 的 MapUtil.newHashMapWithCapacity 正是把上述计算封装成了工具方法避免每个开发者重复推导公式public static K, V HashMapK, V newHashMapWithCapacity(int expectedSize, float loadFactor) { int finalSize (int) (expectedSize / loadFactor 1.0F); return new HashMapK, V(finalSize, loadFactor); }同时MapUtil还暴露了DEFAULT_LOAD_FACTOR 0.75f常量。其注释明确指出加载因子是 HashMap 中减少 Hash 冲突的重要一环如果读写频繁、总记录数不多的 Map可以比默认值 0.75 进一步降低建议 0.5。即如果希望加快 Key 查找的时间可以进一步降低加载因子、加大初始大小以降低哈希冲突的概率——这是以空间换时间的经典权衡。测试用例 MapUtilTest.guavaBuildMap 中即验证了MapUtil.newHashMapWithCapacity(10, 0.5f)的调用形态。除容量计算外MapUtil还提供了一批带初始元素的便捷构造newHashMap(key, value)、newHashMap(K[] keys, V[] values)、newHashMap(ListK, ListV)后者在 key/value 长度不一致时会抛出IllegalArgumentException见测试 MapUtilTest.buildMap。Rule 2. 【推荐】尽量使用新式的 foreach 语法遍历 Collection 与数组foreach 是语法糖遍历集合时其实际字节码等价于基于Iterator的循环。使用 foreach 有两个好处代码简洁有效避免出错当存在多个循环或嵌套循环时因复制粘贴而不小心用错 iterator 或循环计数器i、j是高频低级错误foreach 从语法层面杜绝了这种可能。注意如果遍历过程中需要根据条件删除元素则不能使用 foreach参见下一条规约。Rule 3. 【强制】不要在 foreach 循环里进行元素的 remove/add 操作在 foreach 中直接调用list.remove(str)会触发ConcurrentModificationExceptionmodCount校验失败因为 foreach 隐藏的 Iterator 并不知晓集合被结构性修改。正确做法是显式使用 Iterator 的remove()方法// WRONG for (String str : list) { if (condition) { list.remove(str); } } // RIGHT IteratorString it list.iterator(); while (it.hasNext()) { String str it.next(); if (condition) { it.remove(); } }该规约对应以下静态检查规则可在 CI 中自动拦截Facebook-Contrib: Correctness - Method modifies collection element while iteratingFacebook-Contrib: Correctness - Method deletes collection element while iteratingvjkit 的 ListUtil.notNullList 就是一个基于 Iterator 安全删除元素的典型实现——它通过ite.remove()清理 List 中的 null 对象ListUtil.uniqueNotNullList 则在同一次迭代中既去 null 又去重去重用到的 HashSet 初始化同样遵循 Rule 1 的公式new HashSet((int) (list.size() / 0.75F 1.0F))。Rule 4. 【强制】使用 entrySet 遍历 Map 类集合的 Key/Value而不是 keySet如果既需要 Key 又需要 Value用 keySet 遍历会在循环体内额外执行 N 次map.get(key)查询即增加了 N 次哈希查找// WRONG循环内每轮都 get 一次 for (K key : map.keySet()) { V value map.get(key); } // RIGHT一次遍历同时取得键值对 for (Map.EntryK, V entry : map.entrySet()) { K key entry.getKey(); V value entry.getValue(); }对应检查规则Sonar-2864entrySet() should be iterated when both the key and value are needed。vjkit 的 MapUtil.sortByValueInternal 正是以map.entrySet()为起点、用entryArray排序后再写入 LinkedHashMap 的实现可作为按 Value 排序场景的参考范本同族函数topNByValue(map, reverse, n)可返回按 Value 排序的 Top N 结果适合 Counter 类统计场景。Rule 5. 【强制】当对象用于集合时下列情况需要重新实现 hashCode() 和 equals()以下两种情况必须重写hashCode()与equals()以对象作为 Map 的 KEY 时将对象存入 Set 时。原因在于上述场景都依赖 hashCode 和 equals 比较对象是否逻辑相等而 Object 的默认实现比较的是对象引用是否相等即是否为同一个对象。不重写的话两个内容相同的对象会被当作不同元素重复存入或无法从 Map 中正确取回。此外还有一个容易被忽略的约束对象放入集合后会影响 hashCode()、equals() 结果的属性将不允许修改——否则对象在 Map 桶中的位置可能已失效导致再也无法被正确查找或删除。对应检查规则Sonar-2141: Classes that dont define hashCode() should not be used in hashes。Rule 6. 【强制】高度注意各种 Map 类集合 Key/Value 能否存储 null 值不同 Map 对 null 的容忍度差异极大用错会抛出NullPointerExceptionMapKeyValueHashMapNullableNullableConcurrentHashMapNotNullNotNullTreeMapNotNullNullable由于 HashMap 的干扰很多人误以为 ConcurrentHashMap 也可以置入 null 值——这是并发场景下的高危误解。同理Set 中的元素实际上就是底层 Map 的 keyHashSet 内部就是 HashMap因此 HashSet 同样不能存放 null 的约束要按对应 Map 的 Key 规则来判定。Rule 7. 【强制】长生命周期的集合里面内容需要及时清理避免内存泄漏长生命周期集合包括以下三类都要小心处理静态属性定义中的集合长生命周期对象的属性中的集合保存在 ThreadLocal 中的集合。如果无法保证集合大小是有限的应当使用合适的缓存方案代替直接使用 HashMap例如带过期策略的本地缓存。另外如果使用WeakHashMap保存对象当对象本身失效时不会因为它被集合引用而阻止 GC 回收但JDK 的 WeakHashMap 并不支持并发版本并发场景下可使用 Guava Cache 等并发实现。vjkit 的落地弱引用并发 Mapvjkit 的 MoreMaps 用 Guava 的MapMaker补上了JDK 没有 WeakHashMap 并发实现的缺口// Key 为弱引用的 ConcurrentMapKey 对象可被回收 public static K, V ConcurrentMapK, V createWeakKeyConcurrentMap(int initialCapacity, int concurrencyLevel) { return new MapMaker().weakKeys().initialCapacity(initialCapacity).concurrencyLevel(concurrencyLevel).makeMap(); } // Value 为弱引用的 ConcurrentMapValue 对象可被回收 public static K, V ConcurrentMapK, V createWeakValueConcurrentMap(int initialCapacity, int concurrencyLevel) { return new MapMaker().weakValues().initialCapacity(initialCapacity).concurrencyLevel(concurrencyLevel).makeMap(); }可见在需要弱引用 并发的长生命周期缓存场景vjkit 给出的答案正是本条规约推荐的思路直接用 Guava 系实现替代 JDK 的 WeakHashMap。Rule 8. 【强制】集合如果存在并发修改的场景需要使用线程安全的版本8.1 首选 JUC 并发集合著名的反例是HashMap 扩容时遇到并发修改可能造成 100% CPU 占用JDK 7 时代的多线程 resize 循环链表问题。因此推荐使用java.util.concurrentJUC包中的并发集合如 ConcurrentHashMap 等。它们优于Collections.synchronizedXXX()系列函数——后者等价于给每个方法都加上 synchronized 关键字把整张表锁死并发度极低。8.2 CopyOnWriteArrayList 的适用边界例外ArrayList 所对应的CopyOnWriteArrayList每次更新都会复制整个底层数组因此只适合读多写很少的场景。如果频繁写入反而可能退化为使用Collections.synchronizedList(list)。vjkit 的 ListUtil.synchronizedList 注释也明确指出其定位用于 CopyOnWriteArrayList 与 ArrayDeque 均不符合的场景。8.3 线程安全类也要注意函数正确使用即使使用了 ConcurrentHashMap直接使用 get/put 组合依然可能在多线程间互相覆盖// WRONG两条线程可以同时通过判空各自 put互相覆盖 E e map.get(key); if (e null) { e new E(); map.put(key, e); } return e; // RIGHT用 putIfAbsent 原子完成不存在才写入 E e map.get(key); if (e null) { e new E(); E previous map.putIfAbsent(key, e); if (previous ! null) { return previous; } } return e;vjkit 的落地putIfAbsent 增强封装ConcurrentMap.putIfAbsent()返回的是之前的值而非最终的值语义绕人。vjkit 的 MapUtil.putIfAbsentReturnLast 将其封装为返回最终存储在 Map 中的 Value语义更符合直觉public static K, V V putIfAbsentReturnLast(NotNull final ConcurrentMapK, V map, final K key, final V value) { final V result map.putIfAbsent(key, value); return result ! null ? result : value; }更进一步如果创建 Value 对象的成本较高每次putIfAbsent都要先构造一个对象可能被白白浪费可用 MapUtil.createIfAbsentReturnLast 配合ValueCreator回调做Lazy 创建先 get为空时才回调 creator 构造对象再 putIfAbsent。其行为在 MapUtilTest.generalMethod 中有完整验证putIfAbsentReturnLast(map2, haha, 4)在已有值 3 时仍返回 3createIfAbsentReturnLast对已存在的 key 不会重复覆盖。Rule 9. 【推荐】正确使用集合泛型的通配符ListString并不是ListObject的子类。如果希望泛型集合能向上/向下兼容转型而不仅仅适配唯一类型就需要定义通配符。可以按 extends/super 的字面意义来记也可以遵循PECS 原则Producer Extends, Consumer Super1) 集合要被读取作为生产者定义成? extends Tclass StackE { public void pushAll(Iterable? extends E src) { for (E e : src) push(e); } } StackNumber stack new StackNumber(); IterableInteger integers ...; stack.pushAll(integers); // 只有用 ? extends E 才能接收 IterableInteger2) 集合要被写入作为消费者定义成? super Tclass StackE { public void popAll(Collection? super E dist) { while (!isEmpty()) dist.add(pop); } } StackNumber stack new StackNumber(); CollectionObject objects ...; stack.popAll(objects); // 只有用 ? super E 才能把元素写入 CollectionObjectvjkit 工具方法中的泛型签名也普遍遵循这一风格例如 CollectionUtil.min 的min(Collection? extends T coll, Comparator? super T comp)——读取端用 extends比较器端用 super正是 PECS 的典型应用。Rule 10. 【推荐】List、List? 与 List定义成裸类型List会被 IDE 提示需要定义泛型如果实在无法确定泛型就仓促定义成List?蒙混过关则该 list只能读、不能增改无法向其中放入任何具体类型的元素定义成ListObject呢如 Rule 9 所述ListString并不是ListObject的子类除非函数定义使用了通配符。因此实在无法明确泛型时使用List也是可以的——裸类型至少保留了对集合的完整读写能力在少数历史遗留或异构数据场景下是务实的妥协。Rule 11. 【推荐】如果 Key 只有有限的可选值先将 Key 封装成 Enum并使用 EnumMapEnumMap是以 Enum 为 Key 的 Map其内部存储结构为Object[enum.size]访问时通过value Object[enum.ordinal()]直接按下标取值——同时具备 HashMap 的清晰结构语义化 Key与数组的性能无哈希计算、无冲突。public enum COLOR { RED, GREEN, BLUE, ORANGE; } EnumMapCOLOR, String moodMap new EnumMapCOLOR, String(COLOR.class);对应检查规则Sonar-1640: Maps with keys that are enum values should be replaced with EnumMap。vjkit 的 MapUtil.newEnumMap 对 EnumMap 的构造做了薄封装通过 GuavaPreconditions.checkNotNull校验 type 非空注释强调相比 HashMap当 key 是枚举类时性能与空间占用俱佳。Rule 12. 【推荐】Array 与 List 互转的正确写法List → ArrayString[] array (String[]) list.toArray(); // WRONG直接强转运行期抛 ClassCastException String[] array list.toArray(new String[0]); // RIGHTJDK 会按传入数组的类型创建正确大小的数组 String[] array list.toArray(new String[list.size()]); // RIGHT显式指定大小但 size() 可用 0 代替要点list.toArray()无参版本返回Object[]强转成String[]会在运行期失败应传入带类型的数组作为运行时类型指示。Array → List// 非原始类型数组且 List 不再需要扩展 List list Arrays.asList(array); // 非原始类型数组但希望 List 能再扩展add/remove List list new ArrayList(array.length); Collections.addAll(list, array); // 原始类型数组如 int[]JDK 8 List myList Arrays.stream(intArray).boxed().collect(Collectors.toList()); // 原始类型数组JDK 7 则需要自己写一个循环逐个加入关键陷阱Arrays.asList(array)如果传入的是原始类型数组如int[]会把整个 array 当作 List 的一个元素即得到一个Listint[]String[]或Foo[]则无此问题。Collections.addAll()实际是循环加入元素性能相对较低同样会把int[]认作一个元素。对应检查规则Facebook-Contrib: Correctness - Impossible downcast of toArray() resultFacebook-Contrib: Correctness - Method calls Array.asList on an array of primitive valuesvjkit 的落地佐证vjkit 源码本身大量使用这些正确姿势可作为可直接抄用的范例MapUtil.sortByValueInternal 用entrySet.toArray(new Entry[0])将 entry 集合转数组后Arrays.sort排序再写入 LinkedHashMapListUtil.union 用new ArrayListE(list1.size() list2.size())按预估总容量初始化后addAll正是 Rule 1 与集合运算初始大小优化的结合同族的intersection、difference、disjoint均基于此模式实现ListUtil.reverse 通过 GuavaLists.reverse返回一个倒序 View不会实际多生成一个 List避免了大集合反转的内存浪费。附规范如何落地检查以上 12 条规约属于《唯品会 Java 开发手册》1.0.3 版集合处理章整个手册的规范落地依靠代码格式模板 Sonar 代码规则检查两条路径详见 docs/standard/README.mdEclipse / IntelliJ 格式化模板位于 standard/formatter对 Sonar 规则不满意之处vjkit 仓库提供了定制示例 standard/sonar-vj其中包含了MissingCurlyBracesCheck、UnusedMethodParameterCheck、CatchUsesExceptionWithContextCheck等自研 Check 的实现源码standard/sonar-vj/src/main/java/com/vip/vjkit/sonarvj/checks可供参考如何把手册规则固化为可自动执行的静态检查。集合是 Java 开发中使用频率最高的数据结构也是并发、性能与内存问题的重灾区。将上述 12 条规约配合 vjkit 的MapUtil/ListUtil/CollectionUtil/MoreMaps等工具落地即可在团队层面系统性地规避扩容开销、并发修改、内存泄漏与类型转换等经典陷阱。赞分享开发工具可观测性后端【免费下载链接】vjtoolsThe vip.coms java coding standard, libraries and tools项目地址https://gitcode.com/gh_mirrors/vj/vjtools点击查看免费下载相关推荐如何为MPC-HC打造终极影音体验从零开始的完整配置指南如何为MPC HC打造终极影音体验从零开始的完整配置指南 想象一下你正在观看一部4K HDR电影画面色彩暗淡音频效果平平字幕显示不清晰——这就是MPC开发工具可观测性后端唯品会Java开发手册vjtools命名规约全解13条强制与推荐规则及Sonar落地实践唯品会Java开发手册vjtools命名规约全解13条强制与推荐规则及Sonar落地实践 《唯品会Java开发手册》1.0.3版本仓库 vjtools开发工具可观测性后端《阿里巴巴Java开发手册》集合处理规约全解13 条实战准则与 p3c 插件的源码级印证《阿里巴巴Java开发手册》集合处理规约全解13 条实战准则与 p3c 插件的源码级印证 导读 集合是 Java 日常开发中使用频率最高的数据结构之一也是各代码质量静态分析Lint开发工具上一篇Stitches哈希生成toHash函数如何确保样式唯一性下一篇Relay 查询变量GraphQL Variables完整指南从 Query 级变量到 argumentDefinitions 局部变量创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表