☕ 第七章 集合
📝 一句话概括

Set = 不许重复的抽屉——元素唯一、无下标。主力 HashSet 靠"hashCode 分桶 + equals 验身"两道关卡去重,4.5 埋的"重写 equals 必须搭伴重写 hashCode"规矩在这里正式收网。

7.5 Set接口

1. Set:不许重复的盒子

Collection 家族的另一条支线。和 List 一比,特点立刻清楚:

List(点名册) Set(抽屉)
顺序 存取顺序一致 HashSet 不保证(LinkedHashSet 保插入序,TreeSet 按值排)
下标 有 没有
重复 允许 不允许
add 重复元素 照单全收 返回 false,拒收
取元素 get(i) 没有按位置取,只能遍历或 contains

无下标带来一条连锁反应:get(i)、add(i,e)、普通 for 全部出局——Set 的遍历只剩增强 for 和迭代器(7.4 的口诀里"只读增强 for"在 Set 这就是唯一答案)。

2. 三兄弟:HashSet 主角,另两位会认

实现 一句话 顺序 何时用
HashSet 主力,查重最快 不保证 去重第一反应
LinkedHashSet HashSet + 排队绳 插入顺序 又要去重又要记先后
TreeSet 自动按值排序 升序 要有序去重(进阶再细学)

3. 第一个 HashSet:add 的返回值有用武之地

Set<String> set = new HashSet<>();      // 多态声明,7.2 的姿势
set.add("波仔");
set.add("墨池");
set.add("AI");
set.add("波仔");                         // 重复!返回 false,装不进去

System.out.println(set);                 // [AI, 波仔, 墨池] —— 顺序和存入不一致,正常
System.out.println(set.size());          // 3
System.out.println(set.contains("波仔")); // true(contains/remove/remove 都通用,7.2 合同)

Collection 六大方法在 Set 上照用——总合同没白签。变化只有一处:add 的 boolean 第一次有意义了(List 里永远 true)。

4. 去重原理:hashCode 分桶 + equals 验身

HashSet 底层是哈希表:一排"桶",元素进门先算 hashCode(相当于凭工牌号码找到该去的桶),再验明正身:

add(新元素)
   ↓
① 调 hashCode() 算号码 → 定位到某个桶
   ↓
② 桶里没人?        → 直接入住,add = true
   桶里有人?        → 挨个调 equals() 验身
        ├─ equals 相同 → 判定为重复,拒收,add = false
        └─ equals 不同 → 同桶共存(哈希碰撞),照常入住

两个要点:

💡 4.5 搭伴规矩正式收网

4.5 说过"重写 equals 必须连 hashCode 一起重写",现在知道为什么了:如果两个"内容相同"的对象 equals 是 true、hashCode 却不同(没重写时默认按地址算),它们会被分进不同的桶——equals 永远没机会见面,去重彻底失灵。equals 相同 → hashCode 必须相同,这是 HashSet 正常工作的前提。

5. 自定义类去重实战:7.2 毕业题的续集

7.2 毕业题里 Collection<Person> contains 同内容对象返回 false——现在有了 HashSet,坑更深一层:

Set<Person> set = new HashSet<>();
set.add(new Person("波仔", 25));
set.add(new Person("波仔", 25));
System.out.println(set.size());   // 没重写时:2!

翻车原因:Person 没重写 hashCode → 两个对象地址不同、号码不同 → 分进不同桶 → equals 连出场机会都没有 → "重复"的两个都住进去了。

解法(IDEA:alt+insert → equals() and hashCode() 一键生成,两个一起):

@Override
public boolean equals(Object o) { /* 按姓名+年龄比内容 */ }
@Override
public int hashCode() { /* 按姓名+年龄算号码 */ }

重写后再跑:set.size() = 1,去重生效。记住套餐:写自己的类要进 HashSet/HashMap,equals 和 hashCode 必须成对出现。

6. 遍历 Set:口诀直接砍掉一半

无下标 → 普通 for 出局;剩两招:

for (String s : set) {              // 增强 for:首选
    System.out.println(s);
}

Iterator<String> it = set.iterator();  // 迭代器:要删元素时用
while (it.hasNext()) {
    if (it.next().equals("AI")) {
        it.remove();                    // 同样是 it.remove(),规矩不变
    }
}

7.4 的全部遍历规矩在这里原样生效——遍历中删除依然只有迭代器一条路(forEach 删照样炸)。

7. 动手试一试(30 秒实验)

新建 Demo05(import java.util.*;):

实验 1:Set<String> set = new HashSet<>(); 存 "BB" "AA" "CC" "AA",打印——看重复的 AA 只剩一个,且打印顺序和存入顺序不一致(不保证顺序,别慌)。

实验 2:set.add("AA") 再调一次,用变量接住返回值打印——亲眼 see false;对比 List 的 add 永远 true。

实验 3:自定义 Person 不重写任何方法,两个同内容对象 add 进 HashSet——size() 是 2,翻车现场;alt+insert 生成 equals + hashCode 后再跑——size() 是 1。

实验 4:只重写 equals、不重写 hashCode(生成时手动删掉 hashCode 方法),再实验 3——可能还是 2(不同桶,equals 没出场),亲眼见"搭伴"二字的分量。

实验 5:LinkedHashSet 存同样的元素——重复照样去,但打印顺序 = 插入顺序;换 TreeSet——自动升序。

8. 常见误区

⚠️ 误区 1:想用 get(i) 取 Set 的元素

Set 没有下标,set.get(0) 编译就报错。要"按位置取"说明你需要的是 List;Set 的取法是遍历或 contains 判断。

⚠️ 误区 2:以为 HashSet 输出顺序是乱的 = bug

不保证顺序是它的特性,不是错误。同一个 HashSet 多次遍历顺序通常一致(内部结构稳定),但别写依赖顺序的代码——要顺序换 LinkedHashSet / TreeSet。

⚠️ 误区 3:只重写 equals 不重写 hashCode

HashSet/HashMap 去重失灵的重灾区:不同桶里 equals 没机会出场。套餐规矩:两个一起重写,一个都不许落。

⚠️ 误区 4:以为 hashCode 相同就是重复

hashCode 相同只是"分进同一桶"(哈希碰撞很正常),equals 才有最终否决权。反过来 equals 相同则 hashCode 必须相同——单向成立。

⚠️ 误区 5:在 Set 上找 indexOf / sort

Set 合同里没有 indexOf(无下标);排序用 TreeSet 或以后学的工具类。方法表里找不到的就别硬调。

9. 自测一下

❓ Q1:Set 和 List 的三大差异?

不重复(add 返回 false 拒收)、无下标(没有 get/set(i)/indexOf,普通 for 出局)、顺序不保证(HashSet 随缘,LinkedHashSet 保插入序,TreeSet 按值排)。

❓ Q2:HashSet 判断"重复"的两道关卡?各用什么方法?

① 先调 hashCode 定位桶(快速缩小范围);② 同桶再挨个 equals 验明正身。hashCode 管找位置,equals 管最终判定。

❓ Q3:为什么重写 equals 必须搭伴重写 hashCode?

不重写 hashCode 时默认按地址算——内容相同的不同对象号码不同、进不同桶,equals 永远没机会被调,去重失灵。HashSet 正常工作的前提是"equals 相同 → hashCode 相同"。

❓ Q4:两个内容相同的对象只重写了 equals,HashSet 的 size() 是多少?为什么?

大概率 2。两个对象 hashCode 不同(默认按地址)→ 分进不同桶 → equals 没被调用的机会 → 都入住。解法:equals + hashCode 一起重写。

❓ Q5(毕业题):要统计一段文字里出现过的不同单词,并保持"第一次出现的顺序",选谁?写出核心三行。

LinkedHashSet(去重 + 保插入序)。核心:Set<String> set = new LinkedHashSet<>(); 逐个 set.add(word);(重复的自动被拒)遍历输出 set 即为去重后按首次出现顺序的单词表。

10. 下一步学什么


相关笔记