Java基础快速入门:HashSet深入解析

本文纲要

  1. HashSet 基本使用
  2. 哈希值概念及计算方式
  3. JDK7 中 HashSet 底层原理(数组 + 链表)
  4. JDK8 底层优化(数组 + 链表 + 红黑树)
  5. HashSet 存储自定义对象练习
  6. 小结:Set 集合使用总结

HashSet 基本使用

HashSetSet 接口的典型实现类,它底层基于哈希表结构存储元素。在使用层面,HashSet 具备 Set 集合的三个核心特点:

  • 无序:存储顺序和取出顺序不一定一致。
  • 无索引:不能使用带索引的方式(如普通 for 循环)操作集合。
  • 元素唯一:集合中不能包含重复元素。

因为 HashSet 没有提供带索引的方法,所以遍历时只能使用 迭代器增强 for 循环

项目结构

MySet/
└── src/
    └── com/wb/
        ├── myhashset/
        │   ├── Student.java 
        │   ├── HashSetDemo1.java 
        │   └── HashSetDemo2.java 
        └── hashsettest/
            ├── Student.java 
            └── HashSetTest1.java 

基本示例:存储字符串并遍历

package com.wb.myhashset;
 
import java.util.HashSet;
import java.util.Iterator;
 
/*
添加字符串并进行遍历 
 */
public class HashSetDemo1 {
    public static void main(String[] args) {
        HashSet<String> hs = new HashSet<>();
 
        hs.add("hello");
        hs.add("world");
        hs.add("java");
        hs.add("java");
        hs.add("java");
        hs.add("java");
        hs.add("java");
        hs.add("java");
 
        Iterator<String> it = hs.iterator();
        while(it.hasNext()){
            String s = it.next();
            System.out.println(s);
        }
        System.out.println("=============================");
 
        for (String s : hs) {
            System.out.println(s);
        }
    }
}

运行结果分析:

  • 打印结果与添加顺序不同,验证了 无序 特点
  • 通过 ctrl + B 查看 HashSet 源码结构,可以看到没有类似 get(int index)add(int index, E element) 这样的方法,验证了 无索引。
  • java 虽然被多次添加,但实际只存储了一次,验证了 元素唯一。

哈希值概念及计算方式

1 ) 哈希值(哈希码)JDK 根据对象的地址值或内部属性值计算出来的一个 int 类型整数。哈希值是哈希表结构的核心,HashSet 的去重和存取都依赖于它。

Object 类中定义了获取哈希值的方法:

public native int hashCode();

默认计算方式:Object 中的 hashCode() 方法根据对象的地址值计算哈希值。
特点:

  • 同一个对象多次调用 hashCode(),返回的哈希值相同。
  • 不同对象(默认情况下)的哈希值不同(根据地址值,通常不同)。

2 ) 验证默认哈希值

package com.wb.myhashset;
 
/*
计算哈希值 
 */
public class HashSetDemo2 {
    public static void main(String[] args) {
        Student s1 = new Student("xiaozhi",23);
        Student s2 = new Student("xiaomei",22);
 
        //因为在Object类中,是根据对象的地址值计算出来的哈希值。
        System.out.println(s1.hashCode());//1060830840 
        System.out.println(s1.hashCode());//1060830840 
 
        System.out.println(s2.hashCode());//2137211482 
    }
}

输出结果:

  • s1 两次调用结果一致(1060830840),因为地址值未变。
  • s2 的哈希值不同(2137211482),因为它是一个不同的对象,地址值不同。

3 ) 重写 hashCode() 方法

实际开发中,我们通常需要根据对象的属性值来判断两个对象是否“相等”,此时就必须重写 hashCode()equals() 方法,让它们基于属性值计算和比较。

Student 类中,通过 Alt + Insertequals() and hashCode() 可以快速生成(选择 IntelliJ Default 模板):

package com.wb.myhashset;
 
public class Student {
    private String name;
    private int age;
 
    public Student() {}
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    // getter / setter 略...
 
    @Override 
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
 
        Student student = (Student) o;
 
        if (age != student.age) return false;
        return name != null ? name.equals(student.name) : student.name == null;
    }
 
    // 重写 hashCode,根据属性值计算哈希值,与地址值无关 
    @Override 
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }
 
    @Override 
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

重写后的特点:

  • 哈希值由 nameage 两个属性共同计算得出
  • 如果两个对象的属性值完全相同,则它们的 hashCode() 返回相同的值

JDK7 中 HashSet 底层原理(数组 + 链表)

在 JDK7(以及更早版本,不包括 JDK8)中,HashSet 底层使用 哈希表结构,具体实现为 数组 + 链表

1 ) 底层数据结构

当创建一个 HashSet 对象时,底层会初始化一个默认长度为 16、默认加载因子为 0.75 的数组,数组名称为 table
数组中每个索引位置(position)初始值为 null

table 数组(长度 16)

索引0: null

索引1: null

索引2: null

索引3: null

索引4: null

索引5: null

索引6: null

索引7: null

索引8: null

索引9: null

索引10: null

索引11: null

索引12: null

索引13: null

索引14: null

索引15: null

2 ) 元素添加流程

元素添加的核心步骤:计算哈希值 → 计算应存入的索引 → 判断该位置是否有元素 → 决定是否添加

没有

计算元素的 hashCode

hashCode 与数组长度计算应存入索引

该索引处元素是否为 null?

直接存入数组该位置

依次比较链表中每个元素 equals

是否有属性值相同?

不存入集合

新元素存入数组, 旧元素挂在新元素下, 形成链表

详细过程图解:

假设四次添加元素的哈希值经过计算后索引位置均为 4

  • 第一次添加:索引 4null,直接存入。

数组 table

索引4

元素1

  • 第二次添加(索引仍为4):索引 4 不为空,调用 equals 比较,属性值不同,新元素存入数组,老元素挂在其下形成链表。

数组 table

索引4

新元素2

老元素1

  • 第三次添加(索引仍为4):新元素与链表上的元素逐个比较,属性值均不同,再次形成链表。

数组 table

索引4

新元素3

元素2

元素1

  • 第四次添加(索引仍为4):比较时发现与链表中某个元素属性值相同,则不存入,链表保持不变。

3 ) 加载因子与扩容

当数组中的元素数量达到 容量 × 加载因子(16 × 0.75 = 12) 时,数组会扩容为原来的 两倍(即 32)。加载因子决定了哈希表扩容的时机,平衡了空间与时间的矛盾。

4 ) JDK7 原理总结

数据结构:数组 + 链表
默认数组长度:16,加载因子:0.75
存入流程:先根据哈希值计算索引位置;若为 null 直接存;否则遍历链表通过 equals 比较,全不相同才会添加;有一个相同则不存。
扩容:元素数达到 16 × 0.75 = 12 时,数组扩容至 32

JDK8 底层优化(数组 + 链表 + 红黑树)

JDK7 中,随着元素增多,挂载在某个索引下的链表可能变得很长。如果链表长度达到几百甚至上千,添加和查询时都需要逐一比较,性能严重下降

1 ) 优化方案:引入红黑树

JDK8 开始,当一个索引位置上的链表长度 ≥ 8 时,链表会自动转换为红黑树。这样数组的每个索引上挂载的不再只是链表,还可能是红黑树。

数组 table

索引4

红黑树根节点

较小值

较大值

...

...

...

...

2 ) 存储流程变化

基本流程没有变化:仍然是 计算哈希值计算索引判断 null比较
但比较时根据结构不同采用不同策略:

链表

红黑树

计算 hashCode

计算索引

索引处元素是否为 null?

直接存入

索引下结构类型

依次 equals 比较链表元素

按红黑树规则比较: 小向左比, 大向右比, 相等不存

有相同?

不存

存入数组, 形成链表

有相同?

存入, 加入红黑树

优势:在红黑树中比较时,无需遍历全部节点,通过大小比较即可快速定位,极大提高查询和插入性能。

3 ) JDK8 原理总结

数据结构:数组 + 链表 + 红黑树
当链表长度 ≥ 8 时,自动转换为红黑树。
存储流程与 JDK7 核心一致,但在非空索引处会区分链表比较红黑树比较

HashSet 存储自定义对象练习

需求:创建一个存储多个学生对象的 HashSet 集合并进行遍历。要求:学生对象的成员变量值相同,就认为是同一个对象(即集合中不得出现同名同年龄的重复学生)。

1 ) 未重写 hashCode / equals 的情况

如果不重写 hashCode()equals(),默认采用 Object 中的实现(基于地址值)。此时两个属性值完全相同的对象,哈希值不同,计算出的索引也不同,都能存入集合,无法去重

数组 table

索引 x

索引 y

s1: 小黑,23

s2: 小黑,23

2 ) 正确做法:重写 hashCode()equals()

重写后,属性值相同的对象哈希值相同,索引相同,再通过 equals 比较发现属性值也一样,于是第二个对象不会被添加。

数组 table

索引 z

s1: 小黑,23

s2: 小黑,23
(不存入)

3 ) 完整代码示例

// Student.java (与前面重写方法一致)
package com.wb.hashsettest;
 
public class Student {
    private String name;
    private int age;
 
    public Student() {}
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
 
    @Override 
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        if (age != student.age) return false;
        return name != null ? name.equals(student.name) : student.name == null;
    }
 
    @Override 
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }
 
    @Override 
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
package com.wb.hashsettest;
 
import java.util.HashSet;
 
/*
创建一个存储学生对象的集合,存储多个学生对象,使用程序实现在控制台遍历该集合 
要求:学生对象的成员变量值相同,我们就认为是同一个对象 

结论:
如果HashSet集合要存储自定义对象,那么必须重写hashCode和equals方法。
 */
public class HashSetTest1 {
    public static void main(String[] args) {
        HashSet<Student> hs = new HashSet<>();
 
        Student s1 = new Student("xiaohei",23);
        Student s2 = new Student("xiaohei",23);
        Student s3 = new Student("xiaomei",22);
 
        hs.add(s1);
        hs.add(s2);
        hs.add(s3);
 
        for (Student student : hs) {
            System.out.println(student);
        }
    }
}

运行结果:只有两个学生对象被打印出来(xiaoheixiaomei),重复的 xiaohei 没有被添加。

结论:
如果 HashSet 要存储自定义对象,必须重写 hashCode()equals() 方法,才能实现基于属性值的去重。对于 StringInteger 等 JDK 内置类型,这些方法已经被重写好了,直接使用即可

小结:Set 集合使用总结

Set 集合的整体特点是无序、无索引、不可重复
两个核心实现类 HashSetTreeSet 的对比如下:

实现类底层结构去重机制排序能力使用注意
HashSet哈希表(JDK7: 数组+链表;JDK8: 数组+链表+红黑树)依赖 hashCode() 和 equals()不保证顺序存储自定义对象必须重写 hashCode() 和 equals()
TreeSet红黑树根据比较规则(比较器或自然排序)去重可以对元素进行排序必须指定排序规则:自然排序(Comparable) 或 比较器排序(Comparator)

核心要点:

  • HashSet 基于哈希表,通过 hashCode() 定位索引,equals() 比较内容;JDK8 引入红黑树解决长链表性能问题
  • TreeSet 基于红黑树,不仅能去重,还能自动排序
  • 两套集合各自有适合的场景:需要快速存取去重时用 HashSet;需要排序输出时用 TreeSet

掌握这些基本特性和底层原理,可以让我们在使用 Set 集合时更加得心应手。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值