上午 3h Set 集合总览 + HashSet 基础

1.1 Set 接口核心特征(0.5h 必背)

核心三大特点(和 List 完全反着记)

  1. 无序:存取顺序不一致,存的顺序和取出顺序不保证相同
  2. 不可重复:自带自动去重,不允许存储重复元素
  3. 无索引:没有数字下标,不能使用普通 for 循环遍历

补充基础

  • Set 同样继承 Collection 顶层接口
  • 通用方法和 List 完全一致:add()、remove()、contains()、size()、clear()
  • 只是特性、底层结构、去重规则不一样

极简对比记忆

  • List:有序、可重复、有索引
  • Set:无序、不可重复、无索引

1.2 Set 三大常用实现类(0.3h)

  1. HashSet底层:哈希表(数组 + 链表 + 红黑树)特点:无序、去重、查询存取速度极快,开发最常用
  2. LinkedHashSet底层:哈希表 + 双向链表特点:存取有序 + 自动去重
  3. TreeSet底层:红黑树特点:自动自然排序 + 去重

1.3 HashSet 基础使用(1h)

1. 创建对象 + 泛型语法

java

运行

// 泛型约束:只能存储字符串
HashSet<String> set = new HashSet<>();

2. 通用常用 API

完全复用 Collection 通用方法:

  • add(E e):添加元素
  • remove(Object o):删除指定元素
  • contains(Object o):判断是否包含
  • size():获取元素个数
  • clear():清空集合

3. Set 仅支持的两种遍历方式

因为无索引,直接淘汰普通 for:

  1. 增强 for 循环(推荐)
  2. 迭代器 Iterator 遍历

完整基础演示代码 + 逐行解析

java

运行

import java.util.HashSet;
import java.util.Iterator;

public class HashSetBaseDemo {
    public static void main(String[] args) {
        // 1. 创建HashSet集合,泛型约束String
        HashSet<String> set = new HashSet<>();

        // 2. 添加元素
        set.add("Java");
        set.add("C++");
        set.add("Python");

        // 3. 通用方法测试
        System.out.println("元素个数:" + set.size());
        System.out.println("是否包含Java:" + set.contains("Java"));

        // 4. 增强for遍历(无索引首选)
        System.out.println("===== 增强for遍历 =====");
        for (String s : set) {
            System.out.println(s);
        }

        // 5. 迭代器遍历
        System.out.println("===== 迭代器遍历 =====");
        Iterator<String> it = set.iterator();
        while (it.hasNext()) {
            String s = it.next();
            System.out.println(s);
        }
    }
}
逐行解释
  1. HashSet<String> set = new HashSet<>();创建哈希表集合,强制只能存字符串,编译类型校验;
  2. 所有 add 添加的元素,底层会自动校验重复;
  3. 无索引 → 不能用 get(索引)、不能用普通 for;
  4. 迭代器、增强 for 是 Set 标准遍历方案。

1.4 字符串自动去重实操(1.2h)

核心原理

String 类 官方已经重写好了 hashCode()equals()所以 HashSet 可以直接对字符串自动去重。

去重 + 无序完整案例代码

java

运行

import java.util.HashSet;

public class SetStringRepeatDemo {
    public static void main(String[] args) {
        HashSet<String> set = new HashSet<>();

        // 故意添加大量重复元素
        set.add("张三");
        set.add("李四");
        set.add("张三");
        set.add("王五");
        set.add("李四");

        // 直接打印:无序 + 自动去重
        System.out.println(set);
    }
}
运行现象
  1. 输出顺序和添加顺序不一样 → 验证无序
  2. 重复的「张三、李四」只保留一份 → 验证不可重复
关键结论
  • String、Integer 等 JDK 自带类,都重写了哈希与比较方法
  • 存入 HashSet 天然支持去重,直接即用

下午 2.5h 哈希底层 + 去重核心原理(面试高频)

2.1 哈希值 hashCode(0.7h)

1. 概念

哈希值:JDK 根据对象,通过算法算出的int 类型整数编号相当于:对象的「身份证编号」

2. 核心方法

java

运行

// Object类自带方法,所有对象都有
public int hashCode()

3. 三大特点

  1. 同一个对象,多次调用 hashCode(),哈希值固定不变
  2. 不同对象,哈希值绝大多数不同
  3. 存在哈希冲突:不同对象,刚好算出相同哈希值(小概率)

简单代码演示

java

运行

public class HashCodeDemo {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "abc";
        String s3 = "def";

        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode()); // 相同内容,哈希值一致
        System.out.println(s3.hashCode()); // 不同内容,哈希值不同
    }
}

2.2 HashSet 底层结构(JDK1.8+ 0.9h)

底层组合

哈希表 = 数组 + 单向链表 + 红黑树

元素存入完整流程

  1. 调用对象 hashCode() 计算哈希值
  2. 哈希值经过算法,换算成数组下标
  3. 判断数组下标位置是否为空
    • 为空:直接存入该位置
    • 不为空:产生哈希冲突,向下挂载链表
  4. 链表长度 ≥ 阈值,自动转为红黑树,提升查询效率

结构优势

  • 数组:寻址快
  • 链表:解决哈希冲突
  • 红黑树:防止链表过长导致效率过低

2.3 HashSet 去重双重规则(重中之重 0.9h)

两个条件 必须同时满足,才判定为重复元素、自动去重

  1. 两个对象的 hashCode () 哈希值相同
  2. 两个对象调用 equals() 比较,返回 true

完整判断流程

  1. 先对比哈希值
    • 哈希值不同 → 直接判定为不同元素,直接存入
    • 哈希值相同 → 进入第二步
  2. 再调用 equals 比较内容
    • equals 为 true → 判定重复,舍弃不存入
    • equals 为 false → 哈希冲突,挂载链表存储

面试必背一句话

先比哈希码,再比内容;哈希不同直接存,哈希相同比 equals。


晚上 1.5h 自定义对象去重 + 复盘实战

3.1 自定义对象无法去重的原因(0.8h)

现象

自己写的 Student 类,name、age 完全一样;存入 HashSet 不会自动去重

根本原因

  1. 自定义类默认继承 Object
  2. 使用 Object 原生的:
    • hashCode():比较内存地址
    • equals():比较内存地址
  3. new 出来的两个对象,地址一定不同 → 永远判定不重复

唯一解决方案

必须手动重写两个方法

  1. 重写 equals():对比 对象内部属性(姓名、年龄)
  2. 重写 hashCode():根据属性计算哈希值

实操统一使用 IDEA 一键生成:Alt+Insert → equals and hashCode


3.2 全套实战练习(完整代码 + 逐行解析)

步骤 1:未重写方法 —— 无法去重

java

运行

// 学生类:只写成员变量、构造、get/set、toString
public class Student {
    private String name;
    private int age;

    public Student() {}
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // get & set 省略
    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public String toString() {
        return "Student{name='" + name + "', age=" + age + "}";
    }

    // 关键:没有重写 equals()、hashCode()
}

测试类:

java

运行

import java.util.HashSet;

public class StudentSetNoRewrite {
    public static void main(String[] args) {
        HashSet<Student> set = new HashSet<>();

        // 内容完全相同,但是两个不同对象
        Student s1 = new Student("张三", 18);
        Student s2 = new Student("张三", 18);

        set.add(s1);
        set.add(s2);

        // 输出两个对象,无法去重
        System.out.println(set);
    }
}

步骤 2:IDEA 自动重写后 —— 成功去重

在 Student 类空白处 Alt+Insert → 选择 equals() and hashCode(),自动生成:

java

运行

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Student student = (Student) o;
    return age == student.age && Objects.equals(name, student.name);
}

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

再次运行上面测试代码:

  • 重复属性的学生自动去重
  • 只保留一个对象,完全符合业务需求

3.3 今日全量复盘总结

  1. Set 集合三大核心:无序、无索引、不可重复
  2. HashSet 底层:哈希表(数组 + 链表 + 红黑树)
  3. 哈希值:对象唯一整数标识,Object 原生提供
  4. 去重核心逻辑:hashCode 一致 + equals 为 true 双重校验
  5. JDK 自带类(String/Integer)已重写方法,天然去重
  6. 自定义实体类:必须重写 equals+hashCode 才能实现去重

Day21 验收标准(自查)

✅ 熟练口述 Set 与 List 区别、三大特点✅ 独立写出 HashSet 增删查、增强 for / 迭代器两种遍历✅ 理解哈希值概念、哈希表底层组成✅ 完整默写 HashSet 双重去重判断流程✅ 会使用 IDEA 一键生成 equals&hashCode,解决自定义对象去重

总结一下SET的无序

超级大白话总结(必背)

  1. HashSet 不是随机乱序!
  2. 每次运行打印顺序都一样,永远不变!
  3. 无序 = 存的顺序 ≠ 取的顺序
  4. 顺序由 哈希值 决定,固定不变
  • 你添加每一个元素,会固定算出唯一哈希值
  • 哈希值 → 算出固定数组下标(存放位置)
  • 存放位置永远不变
  • 所以每次运行程序,遍历 / 打印顺序完全一模一样

补充:我们还可以尝试将今天的代码全写在一个类里,要用到我们之前学习的知识类不能定义在方法内部且只能有一个public class

更多推荐