【问题标题】:Fast Incremental Hash in JavaJava中的快速增量哈希
【发布时间】:2026-01-04 00:05:02
【问题描述】:

我正在寻找一个散列函数来散列字符串。出于我的目的(在导入期间识别更改的对象),它应该具有以下属性:

  1. 可以增量使用,即我可以这样使用:

    Hasher h = new Hasher();
    h.add("somestring");
    h.add("another part");
    h.add("eveno more");
    Long hash = h.create();
    

    在整个过程中不会影响其他属性或将字符串保留在内存中。

  2. 防止碰撞。如果我在余生中每天比较来自不同字符串的两个哈希值 100 万次,那么发生冲突的风险应该可以忽略不计。

它不一定要防止恶意尝试创建冲突。

我可以使用什么算法?首选在 Java 中具有现有免费实现的算法。

澄清

  1. 哈希不必很长。例如一个字符串就可以了。

  2. 要散列的数据将来自文件或数据库,其中包含许多 10MB 或最多几 GB 的数据,这些数据将分布到不同的散列中。所以将完整的字符串保存在内存中并不是一个真正的选择。

【问题讨论】:

  • “防止冲突” - 那么散列不是你要找的。​​span>
  • @Durandal Mind 解释为什么?
  • 我猜@Durandal 想指出,每个散列函数都有冲突。我猜你想要的是一个类似输入不太可能发生冲突的函数。
  • 假设您还有 10000 天。这是 10 ^ 10 对哈希值需要具有可忽略不计的相等概率 - 例如,小于 1 ppm - 与 10 ^ 16 相比较大的图像(如 63 位甚至 64 位)很容易。但是每天添加一百万个哈希码并要求每个人都是独一无二的,这会将您的预期寿命限制在 12 岁以下。

标签: java algorithm hash


【解决方案1】:

哈希是一个明智的话题,很难根据您的问题推荐任何此类哈希。您可能想在 https://security.stackexchange.com/ 上提出这个问题,以获得有关哈希在某些用例中的可用性的专家意见。

到目前为止,我的理解是,大多数哈希都是在核心中逐步实现的;另一方面,执行时间并不那么容易预测。

我向您介绍两个Hasher 实现,它们依赖于“Java 中现有的免费实现”。这两种实现的构造方式都是您可以在调用add() 之前任意拆分Strings,只要您不更改其中字符的顺序即可获得相同的结果:

import java.math.BigInteger;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

/**
 * Created for https://*.com/q/26928529/1266906.
 */
public class Hashs {

    public static class JavaHasher {
        private int hashCode;

        public JavaHasher() {
            hashCode = 0;
        }

        public void add(String value) {
            hashCode = 31 * hashCode + value.hashCode();
        }

        public int create() {
            return hashCode;
        }
    }

    public static class ShaHasher {
        public static final Charset UTF_8 = Charset.forName("UTF-8");
        private final MessageDigest messageDigest;

        public ShaHasher() throws NoSuchAlgorithmException {
            messageDigest = MessageDigest.getInstance("SHA-256");
        }

        public void add(String value) {
            messageDigest.update(value.getBytes(UTF_8));
        }

        public byte[] create() {
            return messageDigest.digest();
        }
    }

    public static void main(String[] args) {
        javaHash();

        try {
            shaHash();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();  // TODO: implement catch
        }
    }

    private static void javaHash() {
        JavaHasher h = new JavaHasher();
        h.add("somestring");
        h.add("another part");
        h.add("eveno more");
        int hash = h.create();
        System.out.println(hash);
    }

    private static void shaHash() throws NoSuchAlgorithmException {
        ShaHasher h = new ShaHasher();
        h.add("somestring");
        h.add("another part");
        h.add("eveno more");
        byte[] hash = h.create();
        System.out.println(Arrays.toString(hash));
        System.out.println(new BigInteger(1, hash));
    }
}

这里显然“SHA-256”可以替换为其他常见的哈希算法; Java 提供了很多。

现在您调用了 Long 作为返回值,这意味着您正在寻找 64 位哈希。如果这真的是故意的,请查看What is a good 64bit hash function in Java for textual strings? 的答案。接受的答案是JavaHasher 的一个轻微变体,因为String.hashCode() 进行基本相同的计算,但溢出边界更低:

    public static class Java64Hasher {
        private long hashCode;

        public Java64Hasher() {
            hashCode = 1125899906842597L;
        }

        public void add(CharSequence value) {
            final int len = value.length();

            for(int i = 0; i < len; i++) {
                hashCode = 31*hashCode + value.charAt(i);
            }
        }

        public long create() {
            return hashCode;
        }
    }

你的观点:

  1. 由于 SHA-256 比其他两个慢,我仍然认为所有三种方法都很快。

  2. 可以增量使用,而不会影响其他属性或在整个过程中将字符串保留在内存中。

    我不能保证ShaHasher 的属性是基于块的,并且我缺少源代码。但我建议最多保留一个块,保留散列和一些内部状态。另外两个显然只存储对add()的调用之间的部分哈希

  3. 防止碰撞。如果我在余生中每天比较来自不同字符串的两个哈希值 100 万次,那么发生冲突的风险应该可以忽略不计。

    对于每个散列都有冲突。给定一个良好的分布,哈希的比特大小是冲突发生频率的主要因素。 JavaHasher 用于例如HashMaps 并且似乎是“无冲突​​”的,足以将相似的密钥分配得彼此相距很远。至于任何更深入的分析:请自行测试或询问您当地的安全工程师 - 抱歉。

我希望这是一个好的起点,细节可能主要基于意见。

【讨论】:

【解决方案2】:

不打算作为答案,只是为了证明哈希冲突比人类直觉倾向于假设的可能性更大。

以下小程序生成 2^31 个 不同 字符串并检查它们的哈希值是否冲突。它通过为每个可能的哈希值保留一个跟踪位(因此您需要 >512MB 堆来运行它)来做到这一点,以便在遇到每个哈希值时将它们标记为“已使用”。需要几分钟才能完成。

public class TestStringHashCollisions {

    public static void main(String[] argv) {
        long collisions = 0;
        long testcount = 0;
        StringBuilder b = new StringBuilder(64);
        for (int i=0; i>=0; ++i) {
            // construct distinct string
            b.setLength(0);
            b.append("www.");
            b.append(Integer.toBinaryString(i));
            b.append(".com");

            // check for hash collision
            String s = b.toString();
            ++testcount;
            if (isColliding(s.hashCode()))
                ++collisions;

            // progress printing
            if ((i & 0xFFFFFF) == 0) {
                System.out.println("Tested: " + testcount + ", Collisions: " + collisions);
            }
        }
        System.out.println("Tested: " + testcount + ", Collisions: " + collisions);
        System.out.println("Collision ratio: " + (collisions / (double) testcount));
    }

    // storage for 2^32 bits in 2^27 ints
    static int[] bitSet = new int[1 << 27];

    // test if hash code has appeared before, mark hash as "used"
    static boolean isColliding(int hash) {
        int index = hash >>> 5;
        int bitMask = 1 << (hash & 31);
        if ((bitSet[index] & bitMask) != 0)
            return true;
        bitSet[index] |= bitMask;
        return false;
    }

}

您可以轻松调整字符串生成部分以测试不同的模式。

【讨论】: