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 不同 → 同桶共存(哈希碰撞),照常入住
两个要点:
- hashCode 相同 ≠ 重复——它只负责把人领到同一个桶,真正的否决权在 equals
- equals 没跑一趟的机会就没有:桶都对不上,equals 根本不会被调
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. 常见误区
Set 没有下标,set.get(0) 编译就报错。要"按位置取"说明你需要的是 List;Set 的取法是遍历或 contains 判断。
不保证顺序是它的特性,不是错误。同一个 HashSet 多次遍历顺序通常一致(内部结构稳定),但别写依赖顺序的代码——要顺序换 LinkedHashSet / TreeSet。
HashSet/HashMap 去重失灵的重灾区:不同桶里 equals 没机会出场。套餐规矩:两个一起重写,一个都不许落。
hashCode 相同只是"分进同一桶"(哈希碰撞很正常),equals 才有最终否决权。反过来 equals 相同则 hashCode 必须相同——单向成立。
Set 合同里没有 indexOf(无下标);排序用 TreeSet 或以后学的工具类。方法表里找不到的就别硬调。
9. 自测一下
不重复(add 返回 false 拒收)、无下标(没有 get/set(i)/indexOf,普通 for 出局)、顺序不保证(HashSet 随缘,LinkedHashSet 保插入序,TreeSet 按值排)。
① 先调 hashCode 定位桶(快速缩小范围);② 同桶再挨个 equals 验明正身。hashCode 管找位置,equals 管最终判定。
不重写 hashCode 时默认按地址算——内容相同的不同对象号码不同、进不同桶,equals 永远没机会被调,去重失灵。HashSet 正常工作的前提是"equals 相同 → hashCode 相同"。
大概率 2。两个对象 hashCode 不同(默认按地址)→ 分进不同桶 → equals 没被调用的机会 → 都入住。解法:equals + hashCode 一起重写。
LinkedHashSet(去重 + 保插入序)。核心:Set<String> set = new LinkedHashSet<>(); 逐个 set.add(word);(重复的自动被拒)遍历输出 set 即为去重后按首次出现顺序的单词表。
10. 下一步学什么
- ☑ 7.6 Map接口 ✅ —— 已完成:双列键值对、put 覆盖、getOrDefault 计数器、遍历三姿势 keySet/values/entrySet;HashSet 内部就是一个 HashMap,7.5 的搭伴规矩原样用于键。点此复习
相关笔记
- 第七章 集合/7.4 集合遍历 ← 上一篇:遍历五姿势,Set 只剩两招
- 第四章 面向对象(下)/4.5 Object类:equals 与 hashCode 搭伴规矩的本体课,本节正式收网
- 第七章 集合/7.2 Collection接口:contains 走 equals 的伏笔,HashSet 把它用成了去重
- 第六章 Java API/6.2 字符串类:String 官方重写了 equals + hashCode,所以直接能去重