为什么Java中的String.hashCode()有很多冲突?

时间:2012-02-23 03:34:15

标签: java string hashcode

为什么String.hashcode()有这么多冲突?

我正在读jdk1.6中的String.hashCode(),下面是代码

public int hashCode() {
    int h = hash;
    if (h == 0) {
        int off = offset;
        char val[] = value;
        int len = count;

        for (int i = 0; i < len; i++) {
            h = 31*h + val[off++];
        }
        hash = h;
    }
    return h;
}

这对我来说很混乱,因为它有很多冲突;虽然它不需要是唯一的(我们仍然可以依赖于equals()),但是更少的冲突意味着更好的性能,而无需访问链表中的条目。

假设我们有两个字符,那么只要我们找到两个匹配下面的方程的字符串,那么我们就会有相同的hashcode()

a * 31 +b = c * 31 +d

很容易得出结论(a-c) * 31 = d-b 举一个简单的例子是a-c = 1和d-b = 31; 所以我在下面写了简单测试代码

public void testHash() {
    System.out.println("A:" + (int)'A');
    System.out.println("B:" + (int)'B');
    System.out.println("a:" + (int)'a');

    System.out.println("Aa".hashCode() + "," + "BB".hashCode());
    System.out.println("Ba".hashCode() + "," + "CB".hashCode());
    System.out.println("Ca".hashCode() + "," + "DB".hashCode());
    System.out.println("Da".hashCode() + "," + "EB".hashCode());        
}

它将打印在结果下面,这意味着所有字符串都具有相同的hashcode(),并且很容易在循环中完成。

A:65 
B:66
a:97
2112,2112
2143,2143
2174,2174
2205,2205
更糟糕的是,假设我们在字符串中有4个字符,根据算法,假设前2个字符产生a2,第2个字符产生b2; 哈希码仍然是a2 * 31^2 + b2 因此,当a2和b2在2个字符串之间相等时,我们将获得更多具有hashcode()冲突的字符串。 这样的例子是“AaAa”,“BBBB”等; 那么我们将有6个字符,8个字符......

假设大部分时间我们在字符串中使用ascii表中的字符将在hashmap或hashtable中使用,那么这里选择的素数31肯定太小;

一个简单的解决方法是使用更大的素数(幸运的是,257是素数)可以避免这种冲突。当然,选择一个太大的数字会导致返回的int值溢出,如果字符串很长,但我假设大多数时候用作键的字符串不是那么大? 当然,它仍然可以返回一个很长的值来避免这种情况。

下面是我更好的betterhash()版本,它可以轻松解决此类冲突 通过运行代码,它将打印在值以下,这有效地解决了这个问题。

16802,17028
17059,17285
17316,17542
17573,17799

但是为什么jdk没有解决它?谢谢。

@Test
public void testBetterhash() {
    System.out.println(betterHash("Aa") + "," + betterHash("BB"));      
    System.out.println(betterHash("Ba") + "," + betterHash("CB"));
    System.out.println(betterHash("Ca") + "," + betterHash("DB"));
    System.out.println(betterHash("Da") + "," + betterHash("EB"));
}

public static int betterHash(String s) {
    int h = 0;
    int len = s.length();

    for (int i = 0; i < len; i++) {
        h = 257*h + s.charAt(i);
    }
    return h;
}

4 个答案:

答案 0 :(得分:40)

我刚刚练习了58,000个英语单词(找到here),全都是小写的,第一个字母大写。知道有多少相撞?二:“兄弟姐妹”和“德黑兰”(“德黑兰”的替代拼写)。

就像你一样,我拿了一个子域(在我的情况下可能是一个)可能的字符串并分析了它的hashCode冲突率,并发现它是典型的。谁能说你的可选字符串的任意子域是比我的优化更好的选择?

编写此类的人必须这样做,因为他们无法预测(也不会因此优化)其用户将字符串用作键的子域。所以他们选择了一个散列函数,它在整个字符串域上均匀分布。

如果您有兴趣,这是我的代码(它使用Guava):

    List<String> words = CharStreams.readLines(new InputStreamReader(StringHashTester.class.getResourceAsStream("corncob_lowercase.txt")));
    Multimap<Integer, String> wordMap = ArrayListMultimap.create();
    for (String word : words) {
        wordMap.put(word.hashCode(), word);
        String capitalizedWord = word.substring(0, 1).toUpperCase() + word.substring(1);
        wordMap.put(capitalizedWord.hashCode(), capitalizedWord);
    }

    Map<Integer, Collection<String>> collisions = Maps.filterValues(wordMap.asMap(), new Predicate<Collection<String>>() {
        public boolean apply(Collection<String> strings) {
            return strings.size() > 1;
        }
    });

    System.out.println("Number of collisions: " + collisions.size());
    for (Collection<String> collision : collisions.values()) {
        System.out.println(collision);
    }

修改

顺便说一句,如果你很好奇,与你的哈希函数相同的测试与String.hashCode的1相比有13次碰撞。

答案 1 :(得分:12)

对不起,我们需要对这个想法嗤之以鼻。

  1. 您的分析太简单了。你似乎已经挑选了一系列字符串,旨在证明你的观点。这并不是证据表明在所有字符串的范围内,冲突的数量(统计上)高于预期。

  2. 没有人会在期望 String.hashCode高度无冲突。它的设计并没有考虑到这一点。 (如果你想要高度无冲突的散列,那么使用加密散列算法......并支付费用。)String.hashCode()被设计为在所有字符串的域中相当不错......并且快速< /强>

  3. 假设你可以陈述一个更强的案例,这不是陈述它的地方。您需要向重要人员提出此问题 - Oracle的Java工程团队。

  4. Java工程团队将权衡这种变化的优势与实现它的成本,以及其他所有Java用户。最后一点可能就足以杀死这个石头了。


  5. (“高度无冲突散列”,是我为了这个答案而撤出的一个想法/术语。抱歉。但是,要点是2个字符串的哈希码冲突概率因此,例如,“AA”和“bz”由于具有相同的长度而相关。显然,这个想法需要更多的思考。并且在这个意义上也很明显“相关性”我说的是不可测量的...有点像Kolmogorov Complexity。)

答案 2 :(得分:8)

散列时碰撞是不可避免的。 hashCode()方法返回一个整数,该整数用作数组的索引,该数组是具有相同哈希码的所有对象的存储桶。 equals(Object)方法用于将目标对象与存储桶中的每个对象进行比较,以识别完全匹配的对象(如果存在)。

最终,hashCode()方法只需而不是太弱(即造成太多碰撞),其中太弱非常模糊度量。

答案 3 :(得分:1)

效率很高但也很简单。所有可能的小写(ASCII)字最多六个字母或所有数字最多六个数字长都有一个唯一的hashCode()。即,hashCode类似于基数31。使用更大的数字有其自身的问题。 257因子会使每第8位不是特别随机,因为所有ASCII字符都有0个最高位。较大的因素会导致五个和六个数字/字母单词的重复哈希码。

如果无法更改散列算法,可能是最大的问题。无论你采取什么方法,都可能出现这样一个非常糟糕的选择,并且它可能对你的用例来说是次优的。

也许最大的问题是拒绝服务攻击造成病态病例,通常非常罕见很常见。例如,攻击Web服务器的方法是使用具有相同hashCode的密钥填充缓存,例如, 0每次计算。这会导致HashMap退化为链表。

解决这个问题的一个简单方法是使哈希算法未知,可能会改变。作为它的立场,最好的可能是使用TreeMap(它支持自定义比较,但在这种情况下默认会没问题)