在Java中如何生成具有相同哈希码的字符串?

17

一个用Java编写的现有系统,使用字符串的哈希码作为负载均衡的路由策略。

现在,我无法修改该系统,但需要生成具有相同哈希码的字符串来测试最坏情况。

我从命令行提供这些字符串,并希望系统将所有这些字符串路由到同一目标。

是否可能生成大量具有相同哈希码的字符串?

为了阐明这个问题:

String[] getStringsInSameHashCode(int number){
    //return an array in length "number"
    //Every element of the array share the same hashcode. 
    //The element should be different from each other
}

备注:任何hashCode值都是可接受的。对于字符串本身没有限制,但它们应该彼此不同。

编辑: String类的重写方法不可接受,因为我从命令行中提供了这些字符串。

仪表是也不可接受,因为那会对系统产生一些影响。


使用 equals 字符串不是一个选项吗? - jmj
查看字符串源代码。 - Hovercraft Full Of Eels
它们需要是具有不同值的字符串还是只是不同的字符串对象? - Code-Apprentice
我知道Java如何为字符串生成哈希码,但不知道如何生成具有相同哈希码的不同字符串字面量。我无法覆盖字符串的任何方法。@Code-Guru - StarPinkER
6个回答

35

看到一个测试方法,基本上只要匹配,就是a1*31+b1 = a2*31 +b2, 这意味着(a1-a2)*31=b2-b1。

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

    System.out.println(hash("Aa".hashCode()));
    System.out.println(hash("BB".hashCode()));
    System.out.println(hash("Aa".hashCode()));
    System.out.println(hash("BB".hashCode()));


    System.out.println(hash("AaAa".hashCode()));
    System.out.println(hash("BBBB".hashCode()));
    System.out.println(hash("AaBB".hashCode()));
    System.out.println(hash("BBAa".hashCode()));

}

您将会得到什么

A:65
B:66
a:97
2260
2260
2260
2260
2019172
2019172
2019172
2019172

编辑:有人说这不够直接了当。我添加了以下部分。

    @Test
    public void testN() throws Exception {
        List<String> l = HashCUtil.generateN(3);
        for(int i = 0; i < l.size(); ++i){
            System.out.println(l.get(i) + "---" + l.get(i).hashCode());
        }
    }
AaAaAa---1952508096
AaAaBB---1952508096
AaBBAa---1952508096
AaBBBB---1952508096
BBAaAa---1952508096
BBAaBB---1952508096
BBBBAa---1952508096
BBBBBB---1952508096

下面是源代码,它可能不太高效,但它可行:

public class HashCUtil {

    private static String[] base = new String[] {"Aa", "BB"};

    public static List<String> generateN(int n)
    {
        if(n <= 0)
        {
            return null;
        }

        List<String> list = generateOne(null);
        for(int i = 1; i < n; ++i)
        {
            list = generateOne(list);
        }

        return list;
    }


    public static List<String> generateOne(List<String> strList)
    {   
        if((null == strList) || (0 == strList.size()))
        {
            strList = new ArrayList<String>();
            for(int i = 0; i < base.length; ++i)
            {
                strList.add(base[i]);
            }

            return strList;
        }

        List<String> result = new ArrayList<String>();

        for(int i = 0; i < base.length; ++i)
        {
            for(String str: strList)
            {   
                result.add(base[i]  + str);
            }
        }

        return result;      
    }
}

看一下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;
    }

好的,如果这是SO的规则或文化只提供英文链接那也没关系...我只是想为作者提供更多的帮助;至于问题本身,我认为我已经用演示代码和一些文字解释得足够清楚了... - hetaoblog
  1. 是的,没错。
  2. 演示代码和文字实际上并没有回答这个问题。问题是关于如何生成碰撞。解释碰撞发生的原因和方式并不相关。
- Stephen C
我认为这是一个非常好的答案,尽管如果N非常大,生成的字符串会非常长。 - StarPinkER

9

我认为从一个长字符串中找到一个相等的哈希字符串很难,但是在短字符串(2或3个字符)中找到相等的哈希字符串很容易。

看下面的方程式。(抱歉,我不能发布图片因为我是新成员)

注意,“FB”和“Ea”具有相同的哈希码,并且任何两个类似于s1 + “FB” + s2和s1 + “Ea” + s2的字符串都将具有相同的哈希码。因此,简单的解决方法是找到现有字符串的任何2个字符子字符串,并用具有相同哈希码的2个字符子字符串替换它们。

例如,我们有字符串“helloworld”取得2个字符的子字符串“he”,哈希码(“he”)= 'h' * 31 + 'e' =('h'*31 + 31)+('e' — 31)=('h'+1)* 31 + 'F' = 'i' + 'F'=哈希码(“iF”)。所以期望的字符串是“iFlloworld”我们已经将'h'增加了1,我们可以增加2、3等(但是如果溢出char值就会出错)

以下代码在小层面上运行良好,如果级别很大,使字符值溢出,则会出现错误,如果您想要,我稍后将修复它(此代码更改前2个字符,但我将编辑代码以使前2个字符与最大值计算,因此我将更改为2个最后的字符)

    public static String samehash(String s, int level) {
    if (s.length() < 2)
        return s;
    String sub2 = s.substring(0, 2);
    char c0 = sub2.charAt(0);
    char c1 = sub2.charAt(1);
    c0 = (char) (c0 + level);
    c1 = (char) (c1 - 31 * level);
    String newsub2 = new String(new char[] { c0, c1 });
    String re =  newsub2 + s.substring(2);
    return re;
}

我刚刚编辑了问题。我认为我们朝着正确的方向前进。谢谢。 - StarPinkER
2
我认为最好的问题是“编写一个反向哈希码函数”。 - yelliver

4

我想知道是否有一种“通用”的解决方案,例如某个常量字符串XYZ,这样就可以:

    s.hashCode() == (s + XYZ).hashCode() 

对于任意字符串s,找到这样一个字符串需要解决一个相当复杂的方程...这超出了我的生疏数学能力。但是后来我意识到当hch都为零时,h == 31*h + ch总是true

基于这个洞见,以下方法应该创建一个具有与其参数相同哈希码的不同字符串:

    public String collider(String s) { 
        return "\0" + s;
    }

如果 NUL 字符对你有问题,那么在其前面添加任何哈希码为零的字符串也可以解决问题...尽管这些冲突的字符串比使用零更长。

让我试一下 \0 解决方案是否可行。谢谢。 - StarPinkER

3
给定字符串X,那么字符串Y = "\u0096\0\0ɪ\0ˬ" + X将与X共享相同的哈希码。
解释:
1. String.hashcode()返回整数,而java中的每个整数X都具有属性X = X + 2 *(Integer.MAX_VALUE + 1)。这里,Integer.MAX_VALUE = 2 ^ 31 - 1;
2. 因此,我们只需要找到一个字符串M,该字符串具有以下属性:M的hashcode %(2 *(Integer.MAX_VALUE + 1))= 0;
3. 我找到了"\u0096\0\0ɪ\0ˬ":\u0096的ascii码为150,\0的ascii码为0,ɪ的ascii码为618,ˬ的ascii码为748,因此它的hashcode为150 * 31 ^ 5 + 618 * 31 ^ 2 + 748 = 2 ^ 32 = 0;
你可以选择任何你喜欢的字符串,我选择了这个。

我喜欢你的逻辑和证明,但在这个答案中在前面添加"\0"更简单。 - M. Justin

1

您可以对java.lang.String类进行仪器化,使其方法hashCode()始终返回相同的数字。

我想Javassist是实现这种仪器化的最简单方法。

简而言之:

  • 通过使用Java代理(有关详细信息,请参见{{link1:package java.lang.instrument documentation}}),获取java.lang.instrument.Instrumentation的实例
  • 使用Instrumentation.redefineClasses(ClassDefinition [])方法重新定义java.lang.String类

代码大致如下:

ClassPool classPool = new ClassPool(true);
CtClass stringClass = classPool.get("java.lang.String");
CtMethod hashCodeMethod = stringClass.getDeclaredMethod("hashCode", null);
hashCodeMethod.setBody("{return 0;}");
byte[] bytes = stringClass.toBytecode();
ClassDefinition[] classDefinitions = new ClassDefinition[] {new ClassDefinition(String.class, bytes);
instrumentation.redefineClasses(classDefinitions);// this instrumentation can be obtained via Java-agent

同时不要忘记,代理清单文件必须指定Can-Redefine-Classes: true才能使用redefineClasses(ClassDefinition[])方法。


感谢您的回答。重写hashCode方法是不可接受的,因为它会影响系统。情况是我需要使用这些字符串字面量测试系统。对系统进行修改绝对是不可接受的。 - StarPinkER
@Jermaine Xu,这不是覆盖,而是仪器化。但是,是的,您确实需要能够通过命令行参数重新启动JVM并向JVM添加代理,因为系统已经是用Java编写的。如果您无法做到这一点,那么我的建议就无法使用。在这种情况下,“hetaoblog”的答案应该适合您的情况:) - Valentin Kovalenko
仪器化是一个好主意,但目标是测试,所以我不能修改重新定义String的hashCode方法。感谢您提供的仪器化想法。 - StarPinkER

0
String s = "Some String"
for (int i = 0; i < SOME_VERY_BIG_NUMBER; ++i) {
    String copy = new String(s);

    // Do something with copy.
}

这对你有用吗?它只是创建了许多相同的字符串文字副本,然后您可以在测试中使用它们。

抱歉我没有表达清楚。相同的字符串字面量是不可接受的,因为该字符串是数据库中的主键,我需要不同的字符串字面量。 - StarPinkER

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接