Java集合框架的核心概念与使用技巧

ArrayList尾部add均摊O(1),中间插入需移动元素;LinkedList任意位置add本为O(1),但add(int,E)先遍历查节点致O(n);高频尾加选ArrayList,已知位置插删可用LinkedList配合listIterator()。

为什么 ArrayList 和 LinkedList 的 add() 行为看似一样,性能却差十倍?

因为 ArrayList 底层是数组,add() 默认追加时均摊 O(1),但中间插入(如 add(index, e))需移动后续元素;LinkedList 是双向链表,任意位置 add() 都是 O(1),但前提是已持有目标节点引用——而 add(int, E) 会先调用 node(int) 遍历查找,实际变成 O(n)。

  • 高频尾部追加 → 选 ArrayList(注意预设 initialCapacity 避免多次扩容)
  • 频繁在头部或已知位置插入/删除 → 用 LinkedList,但必须配合 listIterator() 或直接操作 Node(不推荐手动)
  • 别用 LinkedList 当“更灵活的 List”——它在随机访问、内存占用、缓存局部性上全面劣于 ArrayList

HashMap 的 key 为什么必须重写 equals() 和 hashCode()?

因为 HashMap 查找流程是:先算 hashCode() 定位桶(bucket),再用 equals() 在该桶内比对键。若只重写 equals() 不重写 hashCode(),两个逻辑相等的对象可能被散列到不同桶,导致 get() 返回 null;若只重写 hashCode(),所有对象哈希值相同,退化为链表遍历,性能暴跌。

  • IDE 自动生成的 hashCode()/equals() 覆盖全部参与比较的字段
  • 不可变类(如 String、自定义 final 类)作 key 最安全;若用可变对象,确保修改后 hashCode() 不变
  • Objects.hash(a, b, c) 比手写 31 * a.hashCode() + b.hashCode() 更简洁且防空指针

ConcurrentHashMap 在 JDK 8 后为什么不再用分段锁?

JDK 7 的 ConcurrentHashMapSegment 数组分段加锁,粒度粗、内存开销大;JDK 8 改为 synchronized 锁单个 Node(即哈希桶头结点),配合 CAS + 红黑树优化长链表场景,锁粒度更细、扩容支持并发、代码更简洁。

  • 高并发读多写少场景下,ConcurrentHashMap 远优于 Collections.synchronizedMap()
  • computeIfAbsent() 是线程安全的“查无则建”,避免先 get()put() 的竞态条件
  • 不要假设 size() 实时准确——它返回估算值;需精确

    计数建议用 mappingCount()

Stream.collect() 用 Collectors.toList() 为什么有时返回不可变集合?

从 JDK 16 开始,Collectors.toList() 默认返回的是不可修改的 List(内部是 ImmutableCollections.ListN),调用 add()clear() 会抛 UnsupportedOperationException。这不是 bug,而是为了防止意外修改流式处理结果。

  • 需要可变列表 → 显式构造:new ArrayList(stream.collect(Collectors.toList()))
  • 或改用 Collectors.toCollection(ArrayList::new)
  • 同理,toSet()toMap() 在新版本中也倾向返回不可变实现,留意运行时异常
List mutable = stream
    .map(String::toUpperCase)
    .collect(Collectors.toCollection(ArrayList::new));

集合框架的坑不在 API 多难,而在默认行为随 JDK 版本悄悄变化——尤其是不可变性、并发策略和性能边界这些隐性契约。