Java 集合:HashSet 类

HashSet 是 Java 集合框架中 Set 接口的经典实现类,基于哈希表(Hash Table)存储元素,核心特点是无序、不允许重复元素、允许 null 值,并提供高效的添加、删除、查找操作。它是日常开发中处理“去重”和“快速查询”场景的常用工具。

例如,分别向 Set 和 List 中插入 100 万数据,然后统计各自判断元素是否存在 100 次,输出耗时时间:

// 创建 set 和 list,并初始化数据为 100 万
Set<String> set = new HashSet<>();
List<String> list = new ArrayList<>();
for(int i = 0; i < 1000000; i++) {
    String s = "ID" + i;
    set.add(s);
    list.add(s);
}

// 分别统计判断某个值是否存在的 100 次的耗时
// Set(HashSet)通过Hash运算定位
long start = System.currentTimeMillis();
for(int i = 0; i < 100; i++) {
    set.contains("ID5432100");
}
System.out.println("set 耗时:" + (System.currentTimeMillis() - start));

// List(ArrayList)遍历查找
long start2 = System.currentTimeMillis();
for(int i = 0; i < 100; i++) {
    list.contains("ID5432100");
}
System.out.println("list 耗时:" + (System.currentTimeMillis() - start2));

运行结果:

set 耗时:1
list 耗时:836

注意,待查找的元素越靠近列表末尾,耗时越长,因为需要遍历的元素更多。

  

HashSet 核心特性

  • 无序性:元素的存储顺序与添加顺序无关,也不保证元素在迭代时的顺序(底层依赖哈希值分配存储位置,哈希值不连续)。注意,JDK 1.8 后,HashSet 在哈希冲突较多时会将链表转为红黑树,但仍不保证顺序。

  • 去重性:不允许存储重复元素,判断 “重复” 的规则是:

    • 先比较两个元素的 哈希值(hashCode() 方法返回值),若哈希值不同,则认为元素不同;

    • 若哈希值相同,再调用 equals() 方法,若返回 true 则认为元素重复,拒绝添加;若返回 false 则视为不同元素(哈希冲突)。

  • 允许 null 值:最多只能存储 1 个 null 值(符合去重规则)。

  • 线程不安全:非线程安全集合,多线程环境下并发修改(如同时 add/remove)可能导致 ConcurrentModificationException(快速失败机制)。

  • 高效性能:理想情况下(哈希分布均匀,无冲突),add()、remove()、contains() 等操作的时间复杂度为 O(1);若哈希冲突严重,性能会退化至 O (n)(链表结构)或 O (log n)(红黑树结构,JDK 1.8+)。

  

HashSet 简单示例

下面是一个 HashSet 的简单示例,演示初始化、添加元素、删除元素、遍历等常见操作:

package com.hxstrive.java_collection.hashSet;

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

public class HashSetDemo {
    public static void main(String[] args) {
        // 1. 初始化 HashSet(无参构造)
        Set<String> hashSet = new HashSet<>();
        
        // 2. 添加元素(add()方法)
        // 返回值为boolean:添加成功返回true,重复元素添加失败返回false
        boolean isAdded1 = hashSet.add("苹果");
        boolean isAdded2 = hashSet.add("香蕉");
        boolean isAdded3 = hashSet.add("橙子");
        boolean isAdded4 = hashSet.add("苹果"); // 添加重复元素
        System.out.println("添加结果:" + isAdded1 + "," + isAdded2 + "," + isAdded3 + "," + isAdded4);
        System.out.println("添加后集合元素:" + hashSet); // 无序,且无重复元素


        // 3. 判断元素是否存在(contains()方法)
        boolean hasBanana = hashSet.contains("香蕉");
        boolean hasGrape = hashSet.contains("葡萄");
        System.out.println("\n是否包含香蕉:" + hasBanana);
        System.out.println("是否包含葡萄:" + hasGrape);


        // 4. 删除元素(remove()方法)
        // 返回值为boolean:删除成功返回true,元素不存在返回false
        boolean isRemoved = hashSet.remove("香蕉");
        System.out.println("\n删除香蕉的结果:" + isRemoved);
        System.out.println("删除后集合元素:" + hashSet);


        // 5. 集合大小(size()方法)
        System.out.println("\n集合当前大小:" + hashSet.size());


        // 6. 遍历 HashSet 的四种方式
        
        // 方式1:增强for循环(最常用)
        System.out.println("\n增强for循环遍历:");
        for (String fruit : hashSet) {
            System.out.print(fruit + " ");
        }
        
        // 方式2:迭代器(Iterator)
        System.out.println("\n\n迭代器遍历:");
        Iterator<String> iterator = hashSet.iterator();
        while (iterator.hasNext()) {
            String fruit = iterator.next();
            System.out.print(fruit + " ");
        }
        
        // 方式3:forEach + lambda表达式(Java 8+)
        System.out.println("\n\nlambda表达式遍历:");
        hashSet.forEach(fruit -> System.out.print(fruit + " "));
        
        // 方式4:流式处理(Java 8+)
        System.out.println("\n\n流式处理遍历:");
        hashSet.stream().forEach(fruit -> System.out.print(fruit + " "));


        // 7. 清空集合(clear()方法)
        hashSet.clear();
        System.out.println("\n\n清空集合后是否为空:" + hashSet.isEmpty());
    }
}

运行结果:

添加结果:true,true,true,false
添加后集合元素:[苹果, 香蕉, 橙子]

是否包含香蕉:true
是否包含葡萄:false

删除香蕉的结果:true
删除后集合元素:[苹果, 橙子]

集合当前大小:2

增强for循环遍历:
苹果 橙子 

迭代器遍历:
苹果 橙子 

lambda表达式遍历:
苹果 橙子 

流式处理遍历:
苹果 橙子 

清空集合后是否为空:true

  

HashSet 原理分析

HashSet 本身不直接实现哈希表,而是依赖 HashMap 存储元素—— 将 HashSet 的元素作为 HashMap 的 Key,Value 则使用一个固定的空对象(PRESENT)占位。

源码如下:

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    @java.io.Serial
    static final long serialVersionUID = -5024744406713321676L;

    /**
     * 底层存储结构:依赖 HashMap 来存储元素
     * HashSet 的元素作为 HashMap 的 key,value 固定为 PRESENT 对象
     */
    private transient HashMap<E,Object> map;

    /**
     * 用于填充 HashMap 的 value 的占位符
     * 所有元素共享同一个 PRESENT 对象,节省内存
     */
    private static final Object PRESENT = new Object();

    /**
     * 创建一个空的 HashSet
     * 底层 HashMap 使用默认初始容量(16)和默认负载因子(0.75)
     */
    public HashSet() {
        map = new HashMap<>();
    }

    /**
     * 创建一个包含指定集合中所有元素的 HashSet
     * 底层 HashMap 的初始容量会根据指定集合的大小计算,确保足够容纳所有元素
     * 负载因子使用默认值 0.75
     *
     * @param c 包含要添加到HashSet中的元素的集合
     * @throws NullPointerException 如果指定的集合为 null
     */
    public HashSet(Collection<? extends E> c) {
        // 计算初始容量:取"集合大小/0.75 + 1" 和 16 中的较大值
        // 确保 HashMap 有足够容量,避免频繁扩容
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        // 将集合c中的所有元素添加到当前 HashSet
        addAll(c);
    }

    /**
     * 创建一个空的 HashSet,指定底层 HashMap 的初始容量和负载因子
     *
     * @param initialCapacity 底层 HashMap 的初始容量
     * @param loadFactor 底层 HashMap 的负载因子
     * @throws IllegalArgumentException 如果初始容量小于0,或负载因子非正数
     */
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }

    /**
     * 创建一个空的 HashSet,指定底层 HashMap 的初始容量
     * 负载因子使用默认值 0.75
     *
     * @param initialCapacity 底层 HashMap 的初始容量
     * @throws IllegalArgumentException 如果初始容量小于0
     */
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }

    /**
     * 包访问权限的构造方法,仅用于 LinkedHashSet(HashSet 的子类)
     * 底层使用 LinkedHashMap 作为存储结构,保证元素的插入顺序
     * dummy 参数无实际意义,仅用于区分其他相同参数类型的构造方法
     *
     * @param initialCapacity 初始容量
     * @param loadFactor 负载因子
     * @param dummy 忽略此参数(用于方法签名区分)
     * @throws IllegalArgumentException 如果初始容量小于0,或负载因子非正数
     */
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

}

下面继续介绍 HashSet 如何通过 HashMap 来实现一些关键方法的:

add(E e) 添加元素

将指定的元素添加到集合中(如果该元素尚未存在)。当集合中不存在满足 Objects.equals(e, e2) 的元素  e2 时,才会将元素 e 添加到集合中。如果集合中已包含该元素,则调用此方法后集合保持不变,并返回 false。源码如下:

/**
 * 将指定的元素添加到集合中(如果该元素尚未存在)。
 *
 * @param e 要添加到集合中的元素
 * @return 如果集合中之前不包含指定元素,则返回 true;否则返回 false
 */
public boolean add(E e) {
    return map.put(e, PRESENT) == null;
}

将待添加元素 e 作为 HashMap 的 key,PRESENT(固定空对象)作为 value。若 key 不存在,添加后返回 null。若 key 已存在,返回该 key 对应的旧 value(此处为 PRESENT)。因此,当返回值为 null 时,表示添加成功(集合中原本无此元素),返回 true。当返回值为 PRESENT 时,表示添加失败(集合中已有此元素),返回 false。

remove(Object o) 集合删除元素

从集合中移除指定的元素(如果该元素存在)。移除集合中满足 Objects.equals(o, e) 的元素 e(如果存在)。如果集合中包含该元素,则返回 true(等价于:调用此方法导致集合发生了改变)。调用返回后,集合中将不再包含该元素。源码如下:

/**
 * 从集合中移除指定的元素。
 *
 * @param o 要从集合中移除的对象(如果存在)
 * @return 如果集合中包含指定元素,则返回 true;否则返回 false
 */
public boolean remove(Object o) {
    return map.remove(o) == PRESENT;
}

利用底层 HashMap 的 remove 方法实现移除,若 key 存在,移除后返回该 key 对应的 value(此处应为PRESENT),若key不存在,返回 null。因此,当返回值等于 PRESENT 时,表示移除成功(集合中原本包含该元素),返回 true。当返回值为 null 时,表示移除失败(集合中原本不包含该元素),返回 false。

contains(Object o) 集合包含元素

判断当前集合是否包含指定元素。更正式地说,当且仅当集合中存在一个元素 e,且满足 Objects.equals(o, e) 时,才返回 true,否则返回 false。源码如下:

/**
 * 判断当前集合是否包含指定元素。
 *
 * @param o 要测试是否存在于当前集合中的元素
 * @return 若集合包含指定元素,则返回 true;否则返回 false;
 */
public boolean contains(Object o) {
    return map.containsKey(o);
}

复用底层 HashMap 的 containsKey 方法实现元素存在性判断,因为 HashSet 的元素本质是 HashMap 的 Key,所以判断 "集合是否包含 o",等价于判断 "HashMap 的 Key 中是否存在 o"。

size() 集合大小

返回此集合中的元素数量(即集合的基数)。源码如下:

public int size() {
    return map.size();
}

直接返回底层 HashMap 的元素数量,由于 HashSet 的元素都存储为 HashMap 的 Key,因此 HashMap 的 size() 就是 HashSet 的元素数量。

isEmpty() 集合判空

判断当前集合是否不包含任何元素(即是否为空集合)。如果集合中没有元素,则返回 true;否则返回false,源码如下:

public boolean isEmpty() {
    return map.isEmpty();
}

直接复用底层 HashMap 的 isEmpty() 方法,因为 HashSet 的元素存储在 HashMap 的 Key 中,所以当HashMap 为空时,HashSet 也为空。

更多信息请参考 https://docs.oracle.com/javase/8/docs/api/java/util/HashSet.html API 文档。

  

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号