散列
1、概述
Guava 的 com.google.common.hash 包提供了一套开箱即用、线程安全的散列(Hashing)框架,抽象出了无状态算法、状态化计算流以及对象序列化映射,并内置了高效率的布隆过滤器(BloomFilter)。
核心 API 组件
| 组件 | 核心职责 | 典型方法 |
|---|---|---|
Hashing |
静态工厂工具类,用于快速获取各种预定义算法的 HashFunction 实例。 | |
HashFunction |
线程安全、无状态的散列算法工厂 | hashString(), hashBytes(), newHasher() |
Hasher |
状态化数据输入流(链式调用),用于多字段组合计算 | putInt(), putString(), putObject(), hash() |
HashCode |
散列计算的最终结果封装 | asInt(), asLong(), asBytes(), toString()(16进制) |
Funnel<T> |
定义自定义对象如何分解为基础类型写入 PrimitiveSink |
funnel(T from, PrimitiveSink into) |
BloomFilter<T> |
基于概率的集合存在性判断数据结构(支持指定误判率) | create(), put(), mightContain() |
Guava 的 Hashing 架构由以下 4 个核心角色协同工作:
java
[Hashing] (静态工厂)
│
▼ 产生
[HashFunction] (算法定义: murmur3_128, sha256...)
│
▼ 创建
[Hasher] (数据收集器: putString, putLong...)
│
▼ 导出
[HashCode] (计算结果: asBytes, asInt, toString...)
常用算法工厂 (Hashing)
- 密码学算法:Hashing.sha256(), Hashing.sha512(), Hashing.md5()(已标注 @Deprecated)
- 非密码学高效算法:Hashing.murmur3_128(), Hashing.murmur3_32(), Hashing.sipHash24()
- 通用快速算法:Hashing.goodFastHash(int minimumBits)
2、Hashing
Guava 中的 Hashing(位于 com.google.common.hash 包)是整个 Guava Hashing 框架的静态工具工厂类。
它的核心职责是:作为统一入口,快速创建各种预定义的 HashFunction(如 Murmur3、SHA-256、CRC32 等),并提供一致性哈希计算、哈希码组合等静态工具方法。
2.1、核心API
| 分类 | 核心 API 方法 | 说明 |
|---|---|---|
| 非加密型哈希 | murmur3_128(), murmur3_32() |
极速、散列均匀,最推荐的通用 Hash 算法 |
sipHash24() |
具备防 HashDoS 攻击能力的散列算法 | |
crc32(), adler32() |
常用数据传输/存储校验和算法 | |
| 加密型哈希 | sha256(), sha512(), sha384() |
标准 SHA-2 安全加密散列算法 |
hmacSha256(Key), hmacSha1(Key) |
带密钥的 HMAC 消息认证码算法 | |
md5(), sha1() |
传统哈希(注有 @Deprecated,安全性低,仅作兼容使用) |
|
| 一致性哈希 | consistentHash(HashCode/long, int buckets) |
将 Hash 值均匀映射到指定数量的节点/桶上 |
| 组合/融合算法 | combineOrdered(Iterable<HashCode>) |
有序组合多个 HashCode(顺序敏感) |
combineUnordered(Iterable<HashCode>) |
无序组合多个 HashCode(顺序无关) |
|
concatenating(HashFunction...) |
将多个 HashFunction 的计算结果直接拼接在一起 |
2.2、使用示例
1. 生成各种类型的 HashFunction 并计算摘要
java
import com.google.common.base.Charsets;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
public class HashingFactoryDemo {
public static void main(String[] args) {
String data = "Hello, Guava Hashing!";
// 1. Murmur3 (非加密,性能极高,散列分布均匀)
HashCode murmur3Code = Hashing.murmur3_128().hashString(data, Charsets.UTF_8);
System.out.println("Murmur3 128bit: " + murmur3Code.toString());
// 2. SHA-256 (加密级哈希,防篡改)
HashCode sha256Code = Hashing.sha256().hashString(data, Charsets.UTF_8);
System.out.println("SHA-256 Hex : " + sha256Code.toString());
// 3. HMAC-SHA256 (带密钥的消息摘要,常用于 API 签名认证)
Key secretKey = new SecretKeySpec("my-secret-key-123".getBytes(Charsets.UTF_8), "HmacSHA256");
HashCode hmacCode = Hashing.hmacSha256(secretKey).hashString(data, Charsets.UTF_8);
System.out.println("HMAC-SHA256 : " + hmacCode.toString());
// 4. CRC32 (数据校验)
HashCode crc32Code = Hashing.crc32().hashBytes(data.getBytes(Charsets.UTF_8));
System.out.println("CRC32 Int 值 : " + crc32Code.asInt());
}
}
java
Murmur3 128bit: 39dcfec3345bf6f4ff8466697d964aa4
SHA-256 Hex : e8cd201191dca877b3243a82b6b212ccd8282e64765b565746c55ad2a8c3bbcc
HMAC-SHA256 : 21156e908e4c5bee1f90b400875035a3ae0daba2baaa35ba333dc5e2ac98ae5c
CRC32 Int 值 : 1656078612
2. 一致性哈希算法(Hashing.consistentHash)
在分布式系统中,使用 consistentHash 可以避免节点增减时大量数据路由失效。
java
import com.google.common.hash.Hashing;
public class ConsistentHashDemo {
public static void main(String[] args) {
int bucketCount = 5; // 假设当前有 5 个服务器节点(桶 0~4)
String[] userIds = {"user_1001", "user_1002", "user_2005", "user_9999"};
System.out.println("=== 5 个节点时的路由结果 ===");
for (String uid : userIds) {
long hash = Hashing.murmur3_128().hashUnencodedChars(uid).asLong();
int bucket = Hashing.consistentHash(hash, bucketCount);
System.out.println("用户 [" + uid + "] 分配到节点: " + bucket);
}
// 模拟扩容:节点从 5 个增加到 6 个
int newBucketCount = 6;
System.out.println("\n=== 扩容到 6 个节点后的路由结果 ===");
for (String uid : userIds) {
long hash = Hashing.murmur3_128().hashUnencodedChars(uid).asLong();
int bucket = Hashing.consistentHash(hash, newBucketCount);
System.out.println("用户 [" + uid + "] 分配到节点: " + bucket);
}
}
}
java
=== 5 个节点时的路由结果 ===
用户 [user_1001] 分配到节点: 4
用户 [user_1002] 分配到节点: 4
用户 [user_2005] 分配到节点: 3
用户 [user_9999] 分配到节点: 4
=== 扩容到 6 个节点后的路由结果 ===
用户 [user_1001] 分配到节点: 4
用户 [user_1002] 分配到节点: 4
用户 [user_2005] 分配到节点: 3
用户 [user_9999] 分配到节点: 4
3. 组合多个 HashCode(combineOrdered & combineUnordered)
当你需要根据多个独立的哈希结果合成一个新的综合哈希时(例如汇总校验多个文件的指纹),可使用 Hashing.combineXxx()。
java
import com.google.common.base.Charsets;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import java.util.Arrays;
import java.util.List;
public class CombineHashDemo {
public static void main(String[] args) {
HashFunction hf = Hashing.sha256();
HashCode hashBlock1 = hf.hashString("Block_Data_1", Charsets.UTF_8);
HashCode hashBlock2 = hf.hashString("Block_Data_2", Charsets.UTF_8);
List<HashCode> hashCodes = Arrays.asList(hashBlock1, hashBlock2);
// 1. 有序组合:次序不同会产生不同的最终 Hash
HashCode orderedCombined = Hashing.combineOrdered(hashCodes);
System.out.println("有序组合结果: " + orderedCombined.toString());
// 2. 无序组合:次序打乱后生成的最终 Hash 依然相同
HashCode unorderedCombined = Hashing.combineUnordered(hashCodes);
System.out.println("无序组合结果: " + unorderedCombined.toString());
}
}
java
有序组合结果: d3e513257468da11025fd933748b2f8d46e072de7d3636281f142bce0a5a0150
无序组合结果: f7a15bb512fcaa2d52bbfde7aeeb3bbdfc9690fc85ac7c1213741bd60868ddfe
2.3、算法选型与对比
| 场景 | 推荐使用 API | 原因 |
|---|---|---|
| 内存 Map / 布隆过滤器 / 分片计算 | Hashing.murmur3_128() |
吞吐量极高,CPU 消耗低,比 Java 默认 hashCode() 冲突率低得多。 |
| 数据流校验 / 文件完整性比对 | Hashing.crc32() |
运算极快,适合网络传输与磁盘 Block 校验。 |
| 密码存储 / API 接口防签名篡改 | Hashing.sha256() / hmacSha256(Key) |
安全加密级,不可逆且具备高抗碰撞性。 |
| 分布式负载均衡 | Hashing.consistentHash() |
最小化节点变动引发的数据迁移范围。 |
3、HashFunction
HashFunction 是 Guava 散列框架的核心接口(Interface),它代表一个无状态、线程安全、不可变的哈希算法。
3.1、接口设计与生命周期
HashFunction 的主要职责是作为哈希算法的抽象表达,它的内部没有任何与特定一次计算相关的状态数据,因此可以在多线程间安全地共享与复用。
java
┌──────────────────────┐
│ HashFunction (线程安全)│
└──────────┬───────────┘
│
┌────────────────┴────────────────┐
▼ (单次快速计算) ▼ (流式/多字段计算)
┌─────────────────────────┐ ┌─────────────────────────┐
│ hashXxx(data, ...) │ │ newHasher() │
└───────────┬─────────────┘ └───────────┬─────────────┘
│ │
│ ▼
│ ┌─────────────────────────┐
│ │ Hasher (线程不安全) │
│ └───────────┬─────────────┘
│ │ .hash()
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ HashCode (结果值对象) │
└──────────────────────────────────────────────────────────┘
3.2、核心API
HashFunction 接口提供的方法主要可以分为两类:快捷哈希计算方法 和 算法元数据与构建器方法。
1. 快捷计算方法(一次性输入)
如果所有待计算的数据已经完整存在于内存中,可以直接调用以下快捷方法:
| 方法签名 | 说明 |
|---|---|
hashInt(int input) |
计算 32 位整数的哈希值 |
hashLong(long input) |
计算 64 位长整数的哈希值 |
hashBytes(byte[] input) |
计算字节数组的哈希值 |
hashBytes(byte[] input, int off, int len) |
计算指定范围字节数组的哈希值 |
hashBytes(ByteBuffer input) |
计算 ByteBuffer 中的剩余字节哈希值(不改变 position) |
hashString(CharSequence input, Charset charset) |
将字符串按指定编码格式(如 UTF_8)转为字节后计算哈希 |
hashUnencodedChars(CharSequence input) |
直接按 UTF-16 内存字符序列(每字符 2 字节)计算哈希(速度极快) |
hashObject(T instance, Funnel<? super T> funnel) |
配合 Funnel 直接计算自定义对象的哈希值 |
2. 工厂与元数据方法
| 方法签名 | 说明 |
|---|---|
newHasher() |
创建并返回一个新的、空白的 Hasher 构建器(用于多字段拼装) |
newHasher(int expectedInputSize) |
创建带预估字节大小缓冲区的 Hasher(减少扩容开销) |
bits() |
返回该哈希算法产生的 HashCode 的位数(例如 32、64、128、256 等) |
3.2、使用示例
1. 常见计算场景对比
针对不同数据类型,使用 HashFunction 快捷 API 的典型示例:
java
import com.google.common.base.Charsets;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import java.nio.ByteBuffer;
public class HashFunctionMethodsDemo {
public static void main(String[] args) {
// 使用 Murmur3_128 作为算法实现
HashFunction hf = Hashing.murmur3_128();
// 1. 计算基本数值
HashCode codeInt = hf.hashInt(10086);
HashCode codeLong = hf.hashLong(9876543210L);
// 2. 计算字符串 (按 UTF-8 字符集)
HashCode codeStr = hf.hashString("Guava HashFunction", Charsets.UTF_8);
// 3. 高性能字符串计算 (无编码开销)
HashCode codeFastStr = hf.hashUnencodedChars("Guava HashFunction");
// 4. 计算 ByteBuffer
ByteBuffer buffer = ByteBuffer.wrap(new byte[]{0x01, 0x02, 0x03, 0x04});
HashCode codeBuffer = hf.hashBytes(buffer);
System.out.println("Int Hash : " + codeInt);
System.out.println("String Hash : " + codeStr);
System.out.println("Buffer Hash : " + codeBuffer);
System.out.println("Algorithm Bit: " + hf.bits() + " bits");
}
}
java
Int Hash : bae0af22940cd6ef6a6e45b47bf07eaf
String Hash : 8da845ebc0e4902620679a7e250b2966
Buffer Hash : e30f04daa990000a7317b382f823dcea
Algorithm Bit: 128 bits
2. 自定义业务组合:基于 HashFunction 封装分布式分片路由
在实际应用中,我们可以将 HashFunction 作为依赖注入(DI)组件,实现通用的哈希计算服务或负载均衡算法:
java
import com.google.common.base.Charsets;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
/**
* 通用哈希分片路由组件
*/
public class ShardRouter {
private final HashFunction hashFunction;
private final int totalShards;
// 允许传入不同的 HashFunction 策略(如 Murmur3、SipHash 等)
public ShardRouter(HashFunction hashFunction, int totalShards) {
this.hashFunction = hashFunction;
this.totalShards = totalShards;
}
/**
* 根据 Key 计算路由分片编号 [0, totalShards - 1]
*/
public int getShardIndex(String key) {
// 使用 consistentHash 实现均匀映射
long hash = hashFunction.hashString(key, Charsets.UTF_8).asLong();
return Hashing.consistentHash(hash, totalShards);
}
public static void main(String[] args) {
// 实例化路由组件:使用 128 位 Murmur3,分 8 个片
ShardRouter router = new ShardRouter(Hashing.murmur3_128(), 8);
String[] orderIds = {"ORD_001", "ORD_002", "ORD_003", "ORD_004"};
for (String orderId : orderIds) {
int shard = router.getShardIndex(orderId);
System.out.println("订单 [" + orderId + "] 路由至数据库分片: DB_" + shard);
}
}
}
4、Hasher
Guava 中的 Hasher(位于 com.google.common.hash 包)是用于把多数据源/多字段拼装并计算最终哈希值的流式构建器(Stream-like Builder)。
它类似于 StringBuilder 或 ByteBuffer,允许你通过链式调用持续"塞入"(putXxx)各种类型的数据(基本类型、字符串、字节数组、自定义对象),最后通过调用 .hash() 导出最终的 HashCode。
核心工作流
Hasher 继承自 PrimitiveSink 接口,其典型生命周期如下:
java
[HashFunction.newHasher()] ──► [多次调用 putXxx(...)] ──► [.hash()] ──► [HashCode]
4.1、核心API
1. 数据写入 API(put 系列方法)
| 方法签名 | 说明 |
|---|---|
putByte(byte) / putBytes(byte[]) |
写入单个字节或字节数组 |
putShort(short) / putInt(int) / putLong(long) |
写入整型、长整型数据 |
putFloat(float) / toDouble(double) |
写入浮点型数据 |
putBoolean(boolean) / putChar(char) |
写入布尔值或字符 |
putString(CharSequence, Charset) |
按照指定字符集编码(如 UTF_8)写入字符串 |
putUnencodedChars(CharSequence) |
直接按 UTF-16 内存字符序列写入字符串(速度比 putString 更快) |
putObject(T, Funnel<T>) |
配合 Funnel 写入自定义 Java 实体对象 |
2. 导出与生命周期 API
| 方法签名 | 返回类型 | 说明 |
|---|---|---|
hash() |
HashCode |
终结方法。完成计算并导出最终的 HashCode |
注意:Hasher 是非线程安全且一次性的。一旦调用了 .hash() 方法,该 Hasher 实例即告作废,不能再次调用 putXxx() 或 hash()。
4.2、使用示例
1. 基础用法:多字段拼装生成业务唯一签名
在 API 签名校验(如 userId + orderId + timestamp + secret)或生成复合 Key 摘要时,Hasher 提供了流畅的链式体验。
java
import com.google.common.base.Charsets;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
public class HasherBasicDemo {
public static void main(String[] args) {
// 1. 获取算法工厂(HashFunction 线程安全,可复用)
HashFunction hf = Hashing.sha256();
long userId = 10086L;
String orderId = "ORD_2026_0903";
double amount = 199.9;
boolean isPaid = true;
// 2. 创建 Hasher 并进行链式拼装
Hasher hasher = hf.newHasher();
hasher.putLong(userId)
.putString(orderId, Charsets.UTF_8)
.putDouble(amount)
.putBoolean(isPaid);
// 3. 导出最终的签名结果
HashCode signature = hasher.hash();
System.out.println("数据摘要 (Hex 字符串): " + signature.toString());
}
}
java
数据摘要 (Hex 字符串): ed5594ad9c6acc1e9b8a9284c160e46c07677ead381dc42b91c2f17a0dd68d68
2. 自定义对象哈希化(结合 Funnel)
通过 Hasher.putObject 结合 Guava 的 Funnel,可以实现面向对象的离散化哈希计算,无需手动拆解对象的属性。
java
import com.google.common.base.Charsets;
import com.google.common.hash.*;
// 定义业务对象
class Product {
final long id;
final String skuCode;
final int price;
public Product(long id, String skuCode, int price) {
this.id = id;
this.skuCode = skuCode;
this.price = price;
}
}
public class HasherFunnelDemo {
// 1. 定义 Product 的 Funnel(指导 Hasher 如何读取对象字段)
private static final Funnel<Product> PRODUCT_FUNNEL = (Product from, PrimitiveSink into) -> {
into.putLong(from.id)
.putString(from.skuCode, Charsets.UTF_8)
.putInt(from.price);
};
public static void main(String[] args) {
Product p1 = new Product(501L, "IPHONE_16", 7999);
// 2. 使用 Hasher 写入对象
HashCode hashCode = Hashing.murmur3_128()
.newHasher()
.putObject(p1, PRODUCT_FUNNEL)
.hash();
System.out.println("Product 对象 Hash 结果: " + hashCode.toString());
}
}
java
Product 对象 Hash 结果: 1a744aea377ffdc67553e3410c4117f0
3. 性能优化技巧:putUnencodedChars vs putString
如果只是在 JVM 内部拼装字符串做哈希(无需考虑跨语言编码标准),推荐使用 putUnencodedChars。
java
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
public class HasherPerformanceDemo {
public static void main(String[] args) {
String input = "高性能哈希计算测试";
// 方式 A: putString - 需要字符集编码转换 (Char -> Byte),速度略慢,但跨语言兼容性好
HashCode codeA = Hashing.murmur3_128().newHasher()
.putString(input, com.google.common.base.Charsets.UTF_8)
.hash();
// 方式 B: putUnencodedChars - 直接读取内存 char (每个 char 2字节),免去编解码开销,速度更快
HashCode codeB = Hashing.murmur3_128().newHasher()
.putUnencodedChars(input)
.hash();
System.out.println("Code A: " + codeA);
System.out.println("Code B: " + codeB);
}
}
5、HashCode
Guava 中的 HashCode(位于 com.google.common.hash 包)是专门用于封装和操作哈希算法计算结果的不可变(Immutable)值对象。
与 Java 原始的 int 型 hashCode() 不同,HashCode 支持任意位数的哈希值(如 32 位、64 位、128 位、256 位等),并提供了丰富的方法将其转换为字节数组、整数、长整数或十六进制字符串,同时支持哈希码之间的等值比较与位运算。
5.1、核心API
| 分类 | 核心 API 方法 | 说明 |
|---|---|---|
| 导出转换 | asInt() |
将哈希值导出为 int(取前 32 位,不足 32 位会抛异常) |
asLong() |
将哈希值导出为 long(取前 64 位,不足 64 位会抛异常) |
|
asBytes() |
获取原始字节数组 byte[] 的克隆副本 |
|
toString() |
转换为**小写十六进制(Hex)**格式的字符串 | |
padToLong() |
将不足 64 位的哈希值填充/扩展为 long(多出的高位补 0) |
|
| 属性查询 | bits() |
获取该哈希码的总位数(如 32、64、128、256 等) |
| 静态工厂(手动创建) | fromInt(int) |
从 int 快速构建一个 32 位的 HashCode |
fromLong(long) |
从 long 快速构建一个 64 位的 HashCode |
|
fromBytes(byte[]) |
从字节数组快速构建 HashCode |
|
fromString(String) |
从十六进制(Hex)字符串解析还原 HashCode |
|
| 比较与计算 | equals(Object) |
比较两个 HashCode 的位长和内容是否完全相等 |
writeBytesTo(byte[], offset, maxLength) |
将哈希字节序列直接写入预分配的数组中 |
5.2、使用示例
1. 生成与多种格式导出转换
java
import com.google.common.base.Charsets;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
public class HashCodeExportDemo {
public static void main(String[] args) {
// 使用 SHA-256 (256 bits / 32 bytes)
HashCode sha256Code = Hashing.sha256().hashString("Guava HashCode", Charsets.UTF_8);
// 1. 查询位数
System.out.println("哈希码总位数 (Bits): " + sha256Code.bits()); // 256
// 2. 导出为十六进制字符串 (Hex)
String hexString = sha256Code.toString();
System.out.println("16 进制字符串: " + hexString);
// 3. 导出为 byte[]
byte[] bytes = sha256Code.asBytes();
System.out.println("字节数组长度: " + bytes.length); // 32
// 4. 提取为 long / int (适用于 64位 / 32位 哈希)
HashCode murmur64 = Hashing.murmur3_128().hashString("Test", Charsets.UTF_8);
System.out.println("提取为 Long 值: " + murmur64.asLong());
}
}
java
哈希码总位数 (Bits): 256
16 进制字符串: 70945f63f26b47b9c6c93a9fddde9ae5069f7c09e372e209491de9d2ba86e295
字节数组长度: 32
提取为 Long 值: -1283037231234402493
2. 静态工厂构建与 Hex 字符串解析还原
在数据传输或持久化存储(如 Redis / 数据库)中,我们常将 HashCode 保存为 Hex 字符串或字节数组,读取时需要将其还原为 HashCode 对象。
java
import com.google.common.hash.HashCode;
public class HashCodeFactoryDemo {
public static void main(String[] args) {
// 1. 从 16 进制字符串解析还原
String hex = "d22184e92b3a8867a57a1262d4c062c3";
HashCode codeFromHex = HashCode.fromString(hex);
System.out.println("从 Hex 解析恢复的位数: " + codeFromHex.bits()); // 128
// 2. 从数值快速构建
HashCode codeFromInt = HashCode.fromInt(10086);
HashCode codeFromLong = HashCode.fromLong(9999999999L);
// 3. 安全比较两个 HashCode 是否相同
System.out.println("两个 HashCode 是否相等: " + codeFromHex.equals(HashCode.fromString(hex))); // true
}
}
java
从 Hex 解析恢复的位数: 128
两个 HashCode 是否相等: true
3. 位数填充保护(padToLong)
当使用的算法生成的 Hash 结果少于 64 位(例如 CRC32 只有 32 位),直接调用 .asLong() 会抛出 IllegalStateException。此时使用 .padToLong() 可以保证安全提取为 long 型。
java
import com.google.common.base.Charsets;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
public class HashCodePadDemo {
public static void main(String[] args) {
// CRC32 生成 32 位 (4 字节) 哈希
HashCode crc32Code = Hashing.crc32().hashString("Short Data", Charsets.UTF_8);
System.out.println("CRC32 位数: " + crc32Code.bits()); // 32
// 直接调用 crc32Code.asLong() 会抛异常 !
// 使用 padToLong(): 不足 64 位的高位补零,转换为 64 位 long
long safeLongValue = crc32Code.padToLong();
System.out.println("安全补位转换后的 Long 值: " + safeLongValue);
}
}
java
CRC32 位数: 32
安全补位转换后的 Long 值: 1507452422
6、Funnel<T>
Guava 中的 Funnel<T>(位于 com.google.common.hash 包)是 Guava Hashing 体系中连接自定义复杂对象与底层哈希算法的核心桥梁。
在 Java 中,自定义对象往往包含多个不同类型的属性(如 int id、String name、List<String> tags 等)。如果想直接对这个对象计算 Hash 值,过去的方式通常是先手动把它拼接成字符串或字节数组,这既繁琐又耗费内存。Funnel(漏斗)正是为了解决这个问题而设计的:它定义了如何将一个自定义对象的字段"漏入"(流式注入)到哈希管道(PrimitiveSink)中。
6.1、核心概念与工作原理
1. 架构定位与数据流向
java
[自定义对象 T] ──► [Funnel<T>.funnel(object, primitiveSink)] ──► [PrimitiveSink / Hasher] ──► [HashCode]
- Funnel<T>:定义了拆解对象的规则接口(即告诉 Guava:对象的哪些字段需要参与计算 Hash、按什么顺序提取)。
- PrimitiveSink:接收基本数据类型的"接收器/汇"接口(Hasher 实现了该接口)。它只负责接收 putInt、putString 等原始数据类型。
2. 为什么需要 Funnel?
- 解耦:将"对象的字段提取逻辑"与"具体采用什么 Hash 算法(Murmur3、SHA-256 等)"彻底解耦。
- 高效:避免将对象先序列化为 JSON 或 toString() 拼接,直接将原始数据类型流式写入,零额外临时字符串/字节数组内存分配。
- 布隆过滤器(BloomFilter)的核心底座:Guava 的 BloomFilter 在创建时必须传入一个 Funnel,以便对任意类型的对象进行离散哈希化。
6.2、核心API
Funnel<T> 是一个函数式接口(FunctionalInterface),其核心定义极其简洁:
java
@FunctionalInterface
public interface Funnel<T> extends Serializable {
/**
* 将对象 t 的各个字段提取并写入 into 中
*
* @param from 待计算 Hash 的自定义对象
* @param into 接收原始类型的 Sink (通常是 Hasher)
*/
void funnel(T from, PrimitiveSink into);
}
此外,Guava 在 Funnels 工具类中预定义了一些常用的静态 Funnel 工厂方法:
| 工厂 API | 说明 |
|---|---|
Funnels.byteArrayFunnel() |
处理 byte[] 类型的 Funnel |
Funnels.integerFunnel() |
处理 Integer / int 类型的 Funnel |
Funnels.longFunnel() |
处理 Long / long 类型的 Funnel |
Funnels.stringFunnel(Charset) |
按指定字符集编码(如 UTF_8)处理字符串的 Funnel |
Funnels.unencodedCharsFunnel() |
直接处理 UTF-16 字符序列(免去编解码,速度更快)的 Funnel |
Funnels.sequentialFunnel(Funnel<E>) |
处理集合/数组 Iterable<E> 中所有元素的组合 Funnel |
6.3、使用示例
1. 基础用法:为自定义实体创建 Funnel
我们定义一个 User 实体,并为其编写对应的 Funnel 实现:
java
import com.google.common.base.Charsets;
import com.google.common.hash.*;
// 自定义 User 实体
class User {
final long id;
final String username;
final String email;
final boolean active;
public User(long id, String username, String email, boolean active) {
this.id = id;
this.username = username;
this.email = email;
this.active = active;
}
}
public class FunnelBasicDemo {
// 1. 定义 UserFunnel (可定义为单例常量,共享复用)
public static final Funnel<User> USER_FUNNEL = (User user, PrimitiveSink into) -> {
into.putLong(user.id)
.putString(user.username, Charsets.UTF_8)
.putString(user.email, Charsets.UTF_8)
.putBoolean(user.active);
};
public static void main(String[] args) {
User user1 = new User(1001L, "zhangsan", "zhangsan@example.com", true);
// 2. 配合 HashFunction 直接对对象计算 Hash
HashFunction hf = Hashing.murmur3_128();
HashCode hashCode = hf.hashObject(user1, USER_FUNNEL);
System.out.println("User1 的 Murmur3 Hash: " + hashCode.toString());
}
}
java
User1 的 Murmur3 Hash: 2588cf7f3d48e3be3dac22ca583149dc
2. 处理复杂嵌套对象与 List/集合 属性
如果对象内部包含嵌套的子对象或 List/Set 集合,可以在 funnel 方法内部进行嵌套写入或利用 Funnels.sequentialFunnel:
java
import com.google.common.base.Charsets;
import com.google.common.hash.*;
import java.util.Arrays;
import java.util.List;
class Address {
final String city;
final String street;
public Address(String city, String street) {
this.city = city;
this.street = street;
}
}
class Order {
final String orderId;
final Address shippingAddress;
final List<String> itemNames;
public Order(String orderId, Address shippingAddress, List<String> itemNames) {
this.orderId = orderId;
this.shippingAddress = shippingAddress;
this.itemNames = itemNames;
}
}
public class ComplexFunnelDemo {
// 1. 子对象 Address 的 Funnel
public static final Funnel<Address> ADDRESS_FUNNEL = (Address address, PrimitiveSink into) -> {
into.putString(address.city, Charsets.UTF_8)
.putString(address.street, Charsets.UTF_8);
};
// 2. 复合对象 Order 的 Funnel
public static final Funnel<Order> ORDER_FUNNEL = (Order order, PrimitiveSink into) -> {
into.putString(order.orderId, Charsets.UTF_8);
// 写入嵌套子对象 (利用子 Funnel 的计算规则)
ADDRESS_FUNNEL.funnel(order.shippingAddress, into);
// 遍历写入 List 中的项
for (String item : order.itemNames) {
into.putString(item, Charsets.UTF_8);
}
};
public static void main(String[] args) {
Order order = new Order(
"ORD_2026_0903",
new Address("Seoul", "Gangnam-daero"),
Arrays.asList("Book", "Laptop", "Coffee")
);
HashCode hashCode = Hashing.sha256().hashObject(order, ORDER_FUNNEL);
System.out.println("复杂订单对象 SHA-256 签名: " + hashCode.toString());
}
}
java
复杂订单对象 SHA-256 签名: 2b5e2df0872e2debb3f621d2e5b6a3635b2f06964376ef69fbba80a20f27da9e
3. 实战应用:结合 BloomFilter(布隆过滤器)
Guava 的布隆过滤器底层强制要求通过 Funnel 描述对象数据类型:
java
import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnel;
import com.google.common.hash.Funnels;
import com.google.common.hash.PrimitiveSink;
class Device {
final String macAddress;
public Device(String macAddress) {
this.macAddress = macAddress;
}
}
public class BloomFilterFunnelDemo {
private static final Funnel<Device> DEVICE_FUNNEL = (Device device, PrimitiveSink into) -> {
into.putString(device.macAddress, Charsets.UTF_8);
};
public static void main(String[] args) {
// 创建可容纳 10,000 个 Device 对象的 BloomFilter,误判率为 1%
BloomFilter<Device> bloomFilter = BloomFilter.create(
DEVICE_FUNNEL,
10000,
0.01
);
Device dev1 = new Device("00:1A:2C:3D:4E:5F");
Device dev2 = new Device("AA:BB:CC:DD:EE:FF");
// 写入布隆过滤器
bloomFilter.put(dev1);
// 查询判断
System.out.println("dev1 是否可能存在: " + bloomFilter.mightContain(dev1)); // true
System.out.println("dev2 是否可能存在: " + bloomFilter.mightContain(dev2)); // false
}
}
java
dev1 是否可能存在: true
dev2 是否可能存在: false
7、BloomFilter<T>
Guava 中的 BloomFilter(位于 com.google.common.hash 包)是基于 HashFunction 和 Funnel 实现的极高效概率型数据结构(Probabilistic Data Structure)。
它用极小的内存开销,快速判断一个元素是否"可能存在"或"绝对不存在"于集合中。
7.1、核心原理与特性
1. 工作原理
java
[写入/查询对象 T]
│
▼
[Funnel<T> 提取属性]
│
┌─────────┼─────────┐
▼ ▼ ▼
Hash_1 Hash_2 Hash_k (通过 Hashing 算法导出 k 个不同哈希值)
│ │ │
▼ ▼ ▼
[ Bit_2 ] [ Bit_7 ] [ Bit_11 ] ──► 将对应位置的 Bit 数组标记为 1 / 校验是否全为 1
- 写入:对元素通过 Funnel 进行离散化,利用内嵌的 HashFunction 计算出 k k k 个哈希值,并将位数组(Bit Array)中对应下标的位设为 1。
- 查询:校验对应的 k k k 个位是否全为 1。
- 如果全为 1:元素可能存在(存在一定的误判率)。
- 如果有任意一位为 0:元素一定不存在(100% 确定)。
2. 优缺点对比
| 优势 | 局限性 / 注意事项 |
|---|---|
空间效率极高 :内存占用仅为 HashSet 的几十分之一到百分之一 |
存在误判(False Positive):可能把不存在的元素判定为"可能存在" |
| 查询/写入性能极快 :时间复杂度为 O ( k ) O(k) O(k), k k k 为哈希函数个数(常数级) | 无法删除元素:原始 BitArray 无法精确定位是哪个元素占用了哪一位 |
| 隐私保护好:不存储元素本身,只保存二进制位 | 需要预估容量:如果实际写入量大幅超出预估容量,误判率会急剧上升 |
7.2、核心API
1. 创建与初始化(create 静态工厂)
BloomFilter 没有公开的构造函数,必须通过 create 静态工厂方法创建:
| 工厂 API 签名 | 说明 |
|---|---|
BloomFilter.create(Funnel<T> funnel, long expectedInsertions) |
创建过滤器,默认期望误判率 f p p = 0.03 fpp = 0.03 fpp=0.03(3%) |
BloomFilter.create(Funnel<T> funnel, long expectedInsertions, double fpp) |
指定预估容量与期望误判率(如 0.01 表示 1% 误判率) |
2. 元素写入与查询 API
| 方法签名 | 返回类型 | 说明 |
|---|---|---|
put(T element) |
boolean |
插入元素。如果位状态发生改变返回 true,无改变返回 false |
mightContain(T element) |
boolean |
查询元素是否存在。false 表示绝对不存在,true 表示可能存在 |
expectedFpp() |
double |
获取当前设置的期望误判率 f p p fpp fpp |
approximateElementCount() |
long |
获取估计已插入的元素数量 |
3. 过滤器合并与持久化
| 方法签名 | 说明 |
|---|---|
putAll(BloomFilter<T> other) |
将另一个相同配置(同容量、同 Funnel)的 BloomFilter 合并到当前实例(按位或) |
writeTo(OutputStream) / readFrom(InputStream, Funnel<T>) |
将布隆过滤器二进制序列化导出到磁盘/网络流,或从中读取还原 |
7.3、使用示例
1. 基础用法:防止缓存穿透(Cache Penetration)
在分布式系统中,布隆过滤器常置于 Redis 前端,拦截不存在的非法 Key,避免请求直接穿透到数据库。
java
import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnel;
import com.google.common.hash.Funnels;
public class BloomFilterBasicDemo {
public static void main(String[] args) {
// 1. 预估数据量 100 万,期望误判率 1% (0.01)
long expectedInsertions = 1_000_000L;
double fpp = 0.01;
// 2. 创建处理 String 类型的 BloomFilter
BloomFilter<String> filter = BloomFilter.create(
Funnels.stringFunnel(Charsets.UTF_8),
expectedInsertions,
fpp
);
// 3. 模拟把已存在的 100 万条数据写入布隆过滤器
System.out.println("开始写入 100 万条数据...");
for (int i = 0; i < 1_000_000; i++) {
filter.put("user_id_" + i);
}
// 4. 测试"一定存在"的数据(应当全部返回 true)
System.out.println("测试存在数据 user_id_8888: " + filter.mightContain("user_id_8888")); // true
// 5. 测试"不存在"的数据,统计算法误判率
int falsePositives = 0;
int testCount = 100_000; // 测试 10 万条不存在的数据
for (int i = 1_000_000; i < 1_000_000 + testCount; i++) {
if (filter.mightContain("user_id_" + i)) {
falsePositives++;
}
}
double actualFpp = (double) falsePositives / testCount;
System.out.printf("测试 %d 条不存在的数据,误判次数: %d,实际误判率: %.4f (目标 fpp: %.2f)%n",
testCount, falsePositives, actualFpp, fpp);
}
}
java
开始写入 100 万条数据...
测试存在数据 user_id_8888: true
测试 100000 条不存在的数据,误判次数: 1055,实际误判率: 0.0106 (目标 fpp: 0.01)
2. 高级用法:自定义实体对象的布隆过滤器与持久化
通过结合 Funnel,可以对自定义 Java 对象(如 Order)进行布隆过滤,并支持将布隆过滤器导出到磁盘:
java
import com.google.common.base.Charsets;
import com.google.common.hash.*;
import java.io.*;
// 自定义订单对象
class Order {
final String orderId;
final long userId;
public Order(String orderId, long userId) {
this.orderId = orderId;
this.userId = userId;
}
}
public class BloomFilterAdvancedDemo {
// 1. 定义 Order 的 Funnel
public static final Funnel<Order> ORDER_FUNNEL = (Order order, PrimitiveSink into) -> {
into.putString(order.orderId, Charsets.UTF_8)
.putLong(order.userId);
};
public static void main(String[] args) throws IOException {
File file = new File("bloom_filter.bin");
// 2. 初始化并写入数据
BloomFilter<Order> filter = BloomFilter.create(ORDER_FUNNEL, 50000, 0.001);
Order o1 = new Order("ORD_2026_0901", 1001L);
filter.put(o1);
// 3. 将 BloomFilter 序列化写入本地文件 (持久化)
try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
filter.writeTo(os);
}
System.out.println("布隆过滤器已成功导出至文件");
// 4. 从磁盘读取恢复 BloomFilter
BloomFilter<Order> restoredFilter;
try (InputStream is = new BufferedInputStream(new FileInputStream(file))) {
restoredFilter = BloomFilter.readFrom(is, ORDER_FUNNEL);
}
// 5. 验证恢复后的过滤器
System.out.println("校验恢复后的布隆过滤器 contain o1: " + restoredFilter.mightContain(o1)); // true
// 清理测试文件
file.delete();
}
}
java
布隆过滤器已成功导出至文件
校验恢复后的布隆过滤器 contain o1: true