本文纲要
HashSet基本使用- 哈希值概念及计算方式
- JDK7 中
HashSet底层原理(数组 + 链表) - JDK8 底层优化(数组 + 链表 + 红黑树)
HashSet存储自定义对象练习- 小结:
Set集合使用总结
HashSet 基本使用
HashSet 是 Set 接口的典型实现类,它底层基于哈希表结构存储元素。在使用层面,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 + Insert → equals() 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 +
'}';
}
}
重写后的特点:
- 哈希值由
name和age两个属性共同计算得出 - 如果两个对象的属性值完全相同,则它们的
hashCode()返回相同的值
JDK7 中 HashSet 底层原理(数组 + 链表)
在 JDK7(以及更早版本,不包括 JDK8)中,HashSet 底层使用 哈希表结构,具体实现为 数组 + 链表
1 ) 底层数据结构
当创建一个 HashSet 对象时,底层会初始化一个默认长度为 16、默认加载因子为 0.75 的数组,数组名称为 table。
数组中每个索引位置(position)初始值为 null。
2 ) 元素添加流程
元素添加的核心步骤:计算哈希值 → 计算应存入的索引 → 判断该位置是否有元素 → 决定是否添加。
详细过程图解:
假设四次添加元素的哈希值经过计算后索引位置均为 4
- 第一次添加:索引
4为null,直接存入。
- 第二次添加(索引仍为4):索引
4不为空,调用equals比较,属性值不同,新元素存入数组,老元素挂在其下形成链表。
- 第三次添加(索引仍为4):新元素与链表上的元素逐个比较,属性值均不同,再次形成链表。
- 第四次添加(索引仍为4):比较时发现与链表中某个元素属性值相同,则不存入,链表保持不变。
3 ) 加载因子与扩容
当数组中的元素数量达到 容量 × 加载因子(16 × 0.75 = 12) 时,数组会扩容为原来的 两倍(即 32)。加载因子决定了哈希表扩容的时机,平衡了空间与时间的矛盾。
4 ) JDK7 原理总结
数据结构:数组 + 链表。
默认数组长度:16,加载因子:0.75。
存入流程:先根据哈希值计算索引位置;若为 null 直接存;否则遍历链表通过 equals 比较,全不相同才会添加;有一个相同则不存。
扩容:元素数达到 16 × 0.75 = 12 时,数组扩容至 32。
JDK8 底层优化(数组 + 链表 + 红黑树)
JDK7 中,随着元素增多,挂载在某个索引下的链表可能变得很长。如果链表长度达到几百甚至上千,添加和查询时都需要逐一比较,性能严重下降。
1 ) 优化方案:引入红黑树
从 JDK8 开始,当一个索引位置上的链表长度 ≥ 8 时,链表会自动转换为红黑树。这样数组的每个索引上挂载的不再只是链表,还可能是红黑树。
2 ) 存储流程变化
基本流程没有变化:仍然是 计算哈希值 → 计算索引 → 判断 null → 比较。
但比较时根据结构不同采用不同策略:
优势:在红黑树中比较时,无需遍历全部节点,通过大小比较即可快速定位,极大提高查询和插入性能。
3 ) JDK8 原理总结
数据结构:数组 + 链表 + 红黑树。
当链表长度 ≥ 8 时,自动转换为红黑树。
存储流程与 JDK7 核心一致,但在非空索引处会区分链表比较与红黑树比较。
HashSet 存储自定义对象练习
需求:创建一个存储多个学生对象的 HashSet 集合并进行遍历。要求:学生对象的成员变量值相同,就认为是同一个对象(即集合中不得出现同名同年龄的重复学生)。
1 ) 未重写 hashCode / equals 的情况
如果不重写 hashCode() 和 equals(),默认采用 Object 中的实现(基于地址值)。此时两个属性值完全相同的对象,哈希值不同,计算出的索引也不同,都能存入集合,无法去重。
2 ) 正确做法:重写 hashCode() 和 equals()
重写后,属性值相同的对象哈希值相同,索引相同,再通过 equals 比较发现属性值也一样,于是第二个对象不会被添加。
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);
}
}
}
运行结果:只有两个学生对象被打印出来(xiaohei 和 xiaomei),重复的 xiaohei 没有被添加。
结论:
如果 HashSet 要存储自定义对象,必须重写 hashCode() 和 equals() 方法,才能实现基于属性值的去重。对于 String、Integer 等 JDK 内置类型,这些方法已经被重写好了,直接使用即可
小结:Set 集合使用总结
Set 集合的整体特点是无序、无索引、不可重复
两个核心实现类 HashSet 和 TreeSet 的对比如下:
| 实现类 | 底层结构 | 去重机制 | 排序能力 | 使用注意 |
|---|---|---|---|---|
| HashSet | 哈希表(JDK7: 数组+链表;JDK8: 数组+链表+红黑树) | 依赖 hashCode() 和 equals() | 不保证顺序 | 存储自定义对象必须重写 hashCode() 和 equals() |
| TreeSet | 红黑树 | 根据比较规则(比较器或自然排序)去重 | 可以对元素进行排序 | 必须指定排序规则:自然排序(Comparable) 或 比较器排序(Comparator) |
核心要点:
HashSet基于哈希表,通过hashCode()定位索引,equals()比较内容;JDK8 引入红黑树解决长链表性能问题TreeSet基于红黑树,不仅能去重,还能自动排序- 两套集合各自有适合的场景:需要快速存取去重时用
HashSet;需要排序输出时用TreeSet
掌握这些基本特性和底层原理,可以让我们在使用 Set 集合时更加得心应手。
1741

被折叠的 条评论
为什么被折叠?



