课堂要点回顾:
- Set 是接口,继承 Collection,无索引、元素不能重复;不能使用普通 for 循环遍历
- HashSet 底层哈希表,无序去重;自定义类存入 HashSet 必须重写
hashCode()、equals(),否则无法按业务属性去重- LinkedHashSet 底层哈希表 + 双向链表,存取顺序一致、依旧去重,维护链表带来额外内存与性能开销
- 集合不能直接存基本数据类型,必须使用包装类;集合也可以存放自定义实体类
- 实体类要点:属性私有(封装)、get/set、构造方法、重写
toString()用于对象打印输出- Set 全部遍历方式:迭代器、增强 for、forEach (Lambda / 匿名内部类),和 List 通用
一、Set 接口
1. 特点
- 属于
Collection子接口,没有索引,不能用普通 for 循环(依靠下标 get)遍历 - 元素不允许重复,自带去重能力
- 存取顺序是否一致取决于具体实现类(HashSet 无序;LinkedHashSet 存取有序;TreeSet 排序)
- 方法全部继承自
Collection:add、remove、contains、size、clear、isEmpty()
课堂提示:List 可以
get(索引)获取元素;Set 没有索引,没有 get 方法,想要查找元素只能遍历。
2. Set 的三种实现类
| 实现类 | 底层结构 | 特点 |
|---|---|---|
| HashSet | 哈希表(数组 + 链表 + 红黑树 JDK8) | 无序、去重,读写速度快 |
| LinkedHashSet | 哈希表 +双向链表 | 存取顺序一致、去重;链表带来额外开销 |
| TreeSet | 平衡二叉树(红黑树) | 元素大小排序、去重(下一期讲解) |
3. Set 集合遍历(全部 3 种方式,和 List 通用)
Set<String> set = new HashSet<>(); set.add("张三"); set.add("李四"); //①迭代器 Iterator<String> it = set.iterator(); while(it.hasNext()){ System.out.println(it.next()); } //②增强for循环 for(String s : set){ System.out.println(s); } //③forEach,支持匿名内部类 / Lambda表达式(课堂讲到Consumer函数式接口) //匿名内部类写法 set.forEach(new Consumer<String>() { @Override public void accept(String s) { System.out.println(s); } }); //Lambda简写 set.forEach(s -> System.out.println(s)); //方法引用简写 set.forEach(System.out::println);课堂重点:forEach 底层使用
Consumer函数式接口,接口中只有一个accept()抽象方法,接收参数无返回值。
二、HashSet
1. 底层与特点
- 底层哈希表,存取顺序无序,不保证存入和取出顺序一致
- 可以存入
null,最多允许一个 null - 线程不安全;增删查询效率很高
- 默认初始容量 16,加载因子 0.75,达到阈值自动扩容为原来 2 倍;链表长度≥8 且数组≥64 转为红黑树
2. add 添加元素、去重完整流程(课堂讲解 “桶” 机制)
- 调用对象
hashCode()得到哈希值,通过哈希值计算数组存储位置(桶的位置) - 如果该桶位置没有元素 → 直接存入
- 如果桶位置已经存在元素(哈希冲突):
- 比较两个对象哈希值;哈希值不同:挂到链表后面
- 哈希值相同,调用
equals()做内容比较equals=true:判定重复元素,舍弃,不存入(实现去重)equals=false:不是重复,挂到链表节点后面
⚠️课堂强调:HashSet 去重依赖
hashCode() + equals()两个方法同时生效,缺一不可JDK 自带类(String、Integer)已经重写好这两个方法,可以直接去重;自定义实体类不重写,则按照对象地址比较,业务上无法实现去重!
3. 自定义实体类存入 HashSet(课堂 Student 实体案例)
实体类编写规范(封装思想):
- 成员变量private 私有,保护数据安全
- 提供无参、有参构造方法
- 根据业务需要编写 get、set 方法,不需要就不写,不是必须全部生成
- 重写
toString():打印对象时输出属性内容,否则打印类名@内存地址 - 如果需要 HashSet 按业务属性去重:必须重写
hashCode()和equals()
import java.util.Objects; //实体类,对应现实业务实体,未来可以映射数据库表 class Student{ private Integer id; private String name; //无参构造 public Student() {} //有参构造 public Student(Integer id, String name) { this.id = id; this.name = name; } //get、set public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } //重写toString:输出对象时展示属性,而不是内存地址 @Override public String toString() { return "Student{id=" + id + ", name='" + name + "'}"; } //重写hashCode + equals:业务规则:id相同就视为同一个学生,做去重 @Override public boolean equals(Object o) { //1.同一个对象(地址完全相同)直接true if (this == o) return true; //2.判断对象是否为null,判断类型是否一致 if (o == null || getClass() != o.getClass()) return false; //向下转型 Student student = (Student) o; //业务规则:只比较学号id return Objects.equals(id, student.id); } @Override public int hashCode() { //只使用id计算哈希值 return Objects.hash(id); } }测试代码:
HashSet<Student> stuSet = new HashSet<>(); stuSet.add(new Student(1001,"小明")); stuSet.add(new Student(1001,"小明二号")); //id重复,会被去重丢弃 stuSet.add(new Student(1002,"小红")); stuSet.forEach(System.out::println);这里只对比 id;哪怕 name 名字不一样,只要 id 相同就判定重复。重写 equals、hashCode 时根据业务需求确定比较哪些属性。
三、LinkedHashSet
1. 底层和特点
底层:哈希表 + 双向链表
- 依旧元素不可重复,依靠 hashCode+equals 去重
- 双向链表记录元素添加顺序,存取顺序保持一致
- 缺点:需要维护双向链表,占用更多内存;增删操作会有额外性能开销
LinkedHashSet<String> lhs = new LinkedHashSet<>(); lhs.add("A"); lhs.add("B"); lhs.add("C"); //遍历输出顺序:A B C 和添加顺序完全一致 lhs.forEach(System.out::println);2. HashSet 与 LinkedHashSet 对比(课堂总结)
| 项目 | HashSet | LinkedHashSet |
|---|---|---|
| 底层 | 哈希表(数组 + 链表 + 红黑树) | 哈希表 + 双向链表 |
| 存取顺序 | 无序,不保证顺序 | 存取顺序保持一致 |
| 性能 | 增删查询快,开销小 | 性能略低,维护链表有开销 |
| 内存占用 | 较小 | 更大,需要存储双向链表指针 |
| 适用场景 | 只需要去重,不关心顺序(开发最常用) | 需要保留元素存入顺序同时做去重 |
课堂建议:没有顺序需求优先使用
HashSet;需要保证存入顺序再选LinkedHashSet。
四、课堂高频易错汇总
- Set 没有索引,没有 get (int index) 方法,遍历只能迭代器、增强 for、forEach;不能普通 for 下标遍历。
- 集合不能存储基本类型,必须使用包装类,如
HashSet<Integer>,不能写HashSet<int>。 - 自定义类存入 HashSet 想要业务去重,必须重写
hashCode()和equals();只重写其中一个,去重会失效。 toString()不重写,打印对象输出内存地址;重写后打印对象属性,方便调试输出。- LinkedHashSet 有序≠排序!只是记录添加的先后顺序;想要按大小排序用 TreeSet。
- 遍历 Set 的时候,不要直接用集合 add/remove 修改集合,会触发并发修改异常;需要删除用迭代器
it.remove()。
五、速记口诀
Set 接口无索引,元素重复不允许;
HashSet 哈希表,无序去重效率好;
自定义类存集合,hashCode equals 不能少;
LinkedHash 加链表,存入取出顺序保;
链表维护有开销,需要顺序才去找;
实体类要封装好,toString 打印巧;
遍历三种方式记,迭代增强 forEach。