Guava——散列

散列

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
相关推荐
用户31268748772036 分钟前
AQS 到底怎么排队的?CLH 队列、state 与条件变量全链路拆解
java
Json____42 分钟前
springboot开发高校选课管理系统:从零构建前后端分离的全栈实践
java·spring boot·后端·毕业设计·毕设·wwwoop.com
xcl09251 小时前
民宿预约系统开发实战:从需求分析到上线部署全流程解析
java·spring boot
步行cgn1 小时前
为何以继承方式引入SpringBoot
java·spring boot·后端
CoderYanger1 小时前
A.每日一题:1140. 石子游戏 II
java·程序人生·算法·leetcode·游戏·职场和发展·深度优先
禾高网络1 小时前
养老系统开发:背景、架构与养老行业展望
java·大数据·人工智能·小程序·架构
CCYe、2 小时前
新模型上线、旧模型下线:企业AI网关如何管住模型版本
java·网络·数据库·人工智能
风筝在晴天搁浅2 小时前
解题代码清爽版
java·开发语言
NeverFear7922 小时前
【黑马点评】短信登录
java·redis·tomcat·intellij-idea