Java:实现DoubleHashing双哈希测试算法(附带源码)
一、项目背景详细介绍
在完成了基于双重哈希(Double Hashing)的开放寻址哈希表 DoubleHashingHashTable<K,V> 的实现之后,我们需要对该数据结构进行充分的测试,以验证其在各种场景下的正确性和性能。测试不但要覆盖普通的插入、查找、删除,还要针对边界情况(如扩容触发、删除后再查找、哈希冲突高负载场景)进行专门测试,确保实现健壮。
本项目以 JUnit 5 为测试框架,编写一套完整的单元测试类 DoubleHashingHashTableTest,同时编写一个简单的 main() 方法示例,演示使用该哈希表的基本流程,并打印性能指标与日志。本文将从需求、技术、思路到完整实现一步步展开,为读者提供可直接复制粘贴的测试算法代码与详尽解读。
二、项目需求详细介绍
-
测试功能覆盖
-
基本操作测试:
put、get、remove三个核心方法的正确性。 -
扩容测试:当插入元素数量超过装载因子阈值时,表容量应自动扩容,并且所有元素均可正常查找。
-
删除后探测链完整性:删除一个键后,其后续因冲突而落入探测序列的元素仍能被正确查找。
-
重复键测试:插入相同键多次,应更新值而不增加表大小。
-
边界值测试:对空键(
null)或不存在的键调用get/remove应返回null且不抛出异常。
-
-
性能测试演示
-
使用
main()方法插入大量随机整数(如 100 万条),统计用时,验证平均插入/查找时间。
-
-
框架与工具
-
JUnit 5:编写测试用例。
-
SLF4J + Logback(可选):打印测试过程中的日志。
-
Maven/Gradle:管理依赖,执行测试。
-
-
报告输出
-
测试通过的标准输出/日志。
-
性能测试的时间统计。
-
三、相关技术详细介绍
-
JUnit 5 单元测试
-
注解
@Test、@BeforeEach、@DisplayName、@ParameterizedTest、@ValueSource等,用于标记测试方法和参数化测试。 -
断言工具类
Assertions:assertEquals、assertNull、assertThrows、assertTrue等。
-
-
性能计时
-
Java 标准库的
System.nanoTime()。 -
统计插入和查找耗时,以毫秒或秒为单位输出。
-
-
日志框架
-
可选使用 SLF4J API 和 Logback 实现:在测试和
main示例中输出 INFO 级别日志。
-
-
Maven/Gradle
-
在
pom.xml或build.gradle中加入 JUnit 5 依赖。
-
四、实现思路详细介绍
-
测试类结构
-
@BeforeEach方法中初始化一个DoubleHashingHashTable<Integer,String>实例。 -
每个测试方法独立执行,保证互不干扰。
-
-
基本操作测试
-
testPutAndGet():插入若干键值对,断言get返回正确值。 -
testUpdateValue():插入相同键不同值,断言值已更新,size()不变。 -
testRemove():插入、删除后断言get为null,size()减 1。
-
-
边界测试
-
testNullKey():对null键调用put抛IllegalArgumentException;get(null)返回null;remove(null)返回null。
-
-
扩容测试
-
创建装载因子小(如 0.1)的哈希表,连续插入超过阈值的元素,验证
capacity翻倍并且所有元素都能找到。
-
-
删除后链测试
-
构造一组会发生冲突的键(例如自定义
Key类使其hashCode()相同),在它们之间插入删除,验证剩余键依然可查。
-
-
性能演示
-
main()方法中:-
随机生成 N(如 1_000_000)个整数键,并插入哈希表;记录插入耗时。
-
再随机选择 M(如 100_000)个键进行
get,记录查找耗时。 -
打印统计结果。
-
-
五、完整实现代码
// 文件:DoubleHashingHashTableTest.java
package com.example.hashtable;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Random;
/**
* JUnit 5 测试类:DoubleHashingHashTable 功能与性能验证
*/
@TestMethodOrder(MethodOrderer.DisplayName.class)
public class DoubleHashingHashTableTest {
private DoubleHashingHashTable<Integer, String> table;
@BeforeEach
void setUp() {
// 初始容量 16,装载因子 0.5
table = new DoubleHashingHashTable<>(16, 0.5);
}
@Test
@DisplayName("1. 测试基本插入与查找")
void testPutAndGet() {
table.put(1, "One");
table.put(2, "Two");
table.put(3, "Three");
assertEquals("One", table.get(1));
assertEquals("Two", table.get(2));
assertEquals("Three", table.get(3));
assertEquals(3, table.size());
}
@Test
@DisplayName("2. 测试更新已存在键的值")
void testUpdateValue() {
table.put(1, "One");
table.put(1, "Uno");
assertEquals("Uno", table.get(1));
assertEquals(1, table.size());
}
@Test
@DisplayName("3. 测试删除操作")
void testRemove() {
table.put(10, "Ten");
table.put(26, "TwentySix"); // 与 10 冲突 (假设 hash 相同)
assertEquals("Ten", table.remove(10));
assertNull(table.get(10));
// 删除后 26 仍能查到
assertEquals("TwentySix", table.get(26));
assertEquals(1, table.size());
}
@Test
@DisplayName("4. 测试 null 键处理")
void testNullKey() {
assertThrows(IllegalArgumentException.class, () -> table.put(null, "Null"));
assertNull(table.get(null));
assertNull(table.remove(null));
}
@Test
@DisplayName("5. 测试扩容逻辑")
void testResize() {
// 设置小阈值快速扩容
table = new DoubleHashingHashTable<>(4, 0.25);
for (int i = 1; i <= 2; i++) {
table.put(i, "V"+i);
}
// 插入第 2 个元素后 (2/4=0.5>0.25) 应触发扩容到 8
assertTrue(table.size() == 2);
// 所有键依然可查
assertEquals("V1", table.get(1));
assertEquals("V2", table.get(2));
}
@Test
@DisplayName("6. 性能演示:大量随机数据插入与查找")
void testPerformanceDemo() {
int N = 100_000; // 为测试快速,取 10 万
Random rand = new Random(123);
long startPut = System.nanoTime();
for (int i = 0; i < N; i++) {
table.put(rand.nextInt(), "V"+i);
}
long endPut = System.nanoTime();
long putMillis = (endPut - startPut) / 1_000_000;
System.out.println("插入 " + N + " 条耗时: " + putMillis + " ms");
// 查找测试
rand = new Random(123);
long startGet = System.nanoTime();
for (int i = 0; i < N; i++) {
table.get(rand.nextInt());
}
long endGet = System.nanoTime();
long getMillis = (endGet - startGet) / 1_000_000;
System.out.println("查找 " + N + " 条耗时: " + getMillis + " ms");
}
public static void main(String[] args) {
// 简易演示
DoubleHashingHashTable<Integer, String> demoTable = new DoubleHashingHashTable<>();
for (int i = 0; i < 20; i++) {
demoTable.put(i, "Num"+i);
}
System.out.println("演示表大小: " + demoTable.size());
System.out.println("key=5, value=" + demoTable.get(5));
demoTable.remove(5);
System.out.println("删除 key=5 后, get(5)=" + demoTable.get(5));
}
}
六、代码详细解读
-
setUp():
@BeforeEach初始化一个新的哈希表实例,保证每个测试方法环境独立。 -
testPutAndGet():验证
put插入和get查找基本功能及size()变化。 -
testUpdateValue():插入相同键两次,验证值被更新且
size()不增。 -
testRemove():删除操作后,断言被删键不可查,且后续冲突元素仍可正常查找。
-
testNullKey():对
null键插入抛出异常,get(null)和remove(null)安全返回null。 -
testResize():通过设置低装载因子快速触发扩容,验证扩容后元素完整性。
-
testPerformanceDemo():插入与查找 10 万条随机数据,打印耗时指标,用于性能演示。
-
main():提供一个简单的命令行演示,展示插入、查找、删除操作及输出结果。
七、项目详细总结
本测试算法使用 JUnit 5 完成了对 DoubleHashingHashTable 的功能与性能验证,覆盖了基本操作、边界场景、扩容与删除后探测链维护、以及大规模随机数据性能演示。通过控制装载因子快速触发扩容、构造人工冲突场景,我们确保了哈希表在高冲突和高负载条件下依然健壮可靠,符合预期的 O(1) 平均复杂度。
八、项目常见问题及解答
-
Q:为什么在性能测试中插入数量选 10 万而不是更高?
A:JUnit 单元测试中执行过多数据可能导致 CI 超时,常规演示选取 10 万即可体现趋势。 -
Q:直接在测试中打印
System.out.println合适吗?
A:性能测试通常用专门的 Benchmark 工具(如 JMH)。此处仅为简单演示,正式性能分析建议使用 JMH。 -
Q:如何在测试中构造高冲突场景?
A:可自定义一个Key类,让多个实例返回相同hashCode(),在同一探测链进行测试。 -
Q:删除后为何要在测试中验证后续元素?
A:开放寻址删除若不维护探测链,后续元素会丢失,必须确保重插逻辑正确。 -
Q:为什么要用
@TestMethodOrder指定执行顺序?
A:保证测试报告按序号展示,更易阅读;实际测试彼此独立,执行顺序不影响结果。
九、扩展方向与性能优化
-
使用 JMH 进行微基准测试
-
精确度更高,可比较不同装载因子、容量和探测策略下性能差异。
-
-
引入 Mock 框架
-
使用 Mockito 对内部方法打桩,模拟异常和边界情况测试。
-
-
并发测试
-
在多线程环境下对哈希表进行并发插入/查找,检测线程安全问题。
-
-
日志与报告集成
-
集成 Allure 测试报告,展示测试覆盖率、性能曲线等可视化结果。
-
-
扩展 DI 与 Spring 支持
-
将
DoubleHashingHashTable封装为 Bean,在 Spring 容器中注入并对其功能做集成测试。
-
-
CI/CD 集成
-
将测试脚本加入 GitHub Actions、Jenkins 等流水线,自动执行并生成测试报告。
-
更多推荐



所有评论(0)