Java集合框架中的Set与Map的共同特性

Set和Map的核心共性是不允许重复key/元素,但语义不同:Set禁重复元素,Map禁重复key(value可重复);均依赖equals()和hashCode()判重(TreeSet/TreeMap例外,用Comparable/Comparator但要求与equals一致);HashSet底层基于HashMap实现,add(e)即map.put(e, PRESENT);默认不保证顺序,需有序用LinkedHashSet/LinkedHashMap或TreeSet/TreeMap;并发集合如ConcurrentHashMap也不保证顺序;二者均非直接实现接口,而是继承AbstractSet/AbstractMap复用逻辑;null容忍度不统一:HashSet和HashMap允许一个null,TreeSet/TreeMap未指定Comparator时插入null抛NPE。

Set 和 Map 都不允许重复的 key / 元素

这是最核心的共性,但要注意语义差异:Set 不允许重复元素,Map 不允许重复 keyvalue 可重复)。底层都依赖 equals()hashCode() 判断“重复”——如果两个对象 equals() 返回 true,它们必须有相同的 hashCode(),否则 HashSetHashMap 会失效。

  • 没重写 equals()hashCode() 的自定义类,放进 HashSet 或作为 HashMapkey 时,几乎必然出现“逻辑重复却未去重”的问题
  • TreeSetTreeMap 是例外:它们不依赖 hashCode(),而是用 ComparableComparator 比较大小,但依然要求比较结果与 equals() 一致(即:若 a.compareTo(b) == 0,则

    a.equals(b)
    必须为 true

底层实现高度耦合,HashMap 是 HashSet 的实际支撑

HashSet 内部直接持有一个 HashMap 实例,只是把所有元素当作 key 存入,value 固定用一个私有静态对象 PRESENT 占位。这意味着:

  • HashSet 的性能、扩容机制、线程安全性(或缺乏)完全继承自其内部的 HashMap
  • 调用 HashSet.add(e) 实际执行的是 map.put(e, PRESENT)
  • 所以 HashSet 的初始容量、负载因子等参数,本质就是配置它背后那个 HashMap

都不保证迭代顺序(除非使用特定子类)

默认的 HashSetHashMap 均不保证插入或遍历顺序,因为基于哈希表实现,顺序取决于 hashCode() 和当前桶数组状态。

  • 需要插入顺序 → 改用 LinkedHashSetLinkedHashMap
  • 需要自然排序 → 改用 TreeSetTreeMap
  • ConcurrentHashMapCopyOnWriteArraySet 这类并发集合,同样不保证顺序,且迭代器行为更特殊(可能反映某一时刻快照,而非实时状态)

都不是接口的直接实现者,而是靠抽象类或组合封装

SetMap 都是接口,但常见实现并非从头写起:

  • AbstractSet 提供了 equals()hashCode()removeAll() 等默认实现,HashSet 继承自 AbstractSet
  • AbstractMap 封装了 entrySet()keySet()values() 等通用逻辑,HashMap 继承自 AbstractMap
  • 这种设计让新增集合实现(如自定义缓存容器)可以复用大量基础行为,但也意味着:直接继承 AbstractSet 却忘了重写 iterator(),会导致 NullPointerException
public class MySet extends AbstractSet {
    private final List list = new ArrayList<>();

    @Override
    public Iterator iterator() {
        return list.iterator(); // 必须重写!否则 super.iterator() 抛 NPE
    }

    @Override
    public int size() {
        return list.size();
    }
}
真正容易被忽略的是:**SetMapnull 的容忍度并不统一**。HashSet 允许一个 null 元素,HashMap 允许一个 null key;但 TreeSetTreeMap 在未指定 Comparator 时,直接放入 null 会抛 NullPointerException——这个差异不是设计疏漏,而是由比较逻辑决定的。