java

求json对象的hashCode

2020-09-18  本文已影响0人  修行者12138

业务场景
调用外部接口,把返回的json数据入库,数据可能重复,因此需要去重(可以容忍极少数重复数据),项目使用fastjson解析json

下面列出几种方案,并评估其去重效果

方案一
使用fastjson本身的hashCode方法求哈希值,根据哈希值去重

String jsonStr = "{\"name\":\"ltm\"}";
JSONObject jsonObj = JSON.parseObject(jsonStr);
System.out.println(jsonObj.hashCode());

打断点跟踪源码,发现JSONObject内部维护了一个HashMap用于存储键值对,JSONObject的hashCode()方法直接把HashMap的hashCode()返回了。

HashMap的hashCode()方法继承自AbstractMap
AbstractMap返回了每个Entry的hashCode()累加结果
每个Entry的hashCode()返回了key和value的hashCode()的异或结果
(猜测使用异或是为了提高随机性)

// java.util.AbstractMap
public int hashCode() {
    int h = 0;
    Iterator<Entry<K,V>> i = entrySet().iterator();
    while (i.hasNext())
        h += i.next().hashCode();
    return h;
}
// java.util.HashMap.Entry
public final int hashCode() {
    return Objects.hashCode(key) ^ Objects.hashCode(value);
}
// java.util.Objects
public static int hashCode(Object o) {
    return o != null ? o.hashCode() : 0;
}

也就是说,假如能满足key1.hashCode() ^ value1.hashCode() == key2.hashCode() ^ value2.hashCode()这样的条件,两个Entry的hashCode()就相等。
HashMap中只有一个Entry时,可以很容易的构造出这样的数据

Map map1 = new HashMap();
map1.put("key", "value");

Map map2 = new HashMap();
map2.put("value", "key");

System.out.println(map1.hashCode());
System.out.println(map2.hashCode());

输出结果
112004910
112004910

上面的例子没有真实性,但是拿真实数据测试本方案时,发现重复率确实很高

String str1 = "2016-06-07";
String str2 = "2016-06-08";
Integer n1 = 5;
Integer n2 = 2;

JSONObject obj1 = new JSONObject();
obj1.put("date", str1);
obj1.put("seq_no", n1);

JSONObject obj2 = new JSONObject();
obj2.put("date", str2);
obj2.put("seq_no", n2);

System.out.println(obj1.hashCode());
System.out.println(obj2.hashCode());

输出结果
-1485717490
-1485717490

方案二
直接使用String类的hashCode(),根据json格式字符串的hashCode去重,源码如下

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

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

假设字符串的长度是2,只要满足a * 31 + b == c * 31 + d这样的条件,就能构造出hash冲突的例子。
a * 31 + b == c * 31 + d等价于(a - c) * 31 == d - b

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());

输出结果
2112,2112
2143,2143
2174,2174
2205,2205

可以看出,String的hashCode()冲突率同样不低

方案三(推荐)
对JSONObject排序,然后转换成字符串,再使用散列函数(如MD5、SHA1等)对字符串求hash值
之所以要排序,是因为json无序,如果两个json只是key-value的顺序不同,实际上应该算是同样的数据。

StudentDO studentDO = new StudentDO();
studentDO.setAge(99);
studentDO.setName("ltm");

JSONObject jsonObj1 = JSON.parseObject(JSON.toJSONString(studentDO));
System.out.println("无序: " + jsonObj1);

JSONObject jsonObj2 = JSON.parseObject(JSON.toJSONString(studentDO), Feature.OrderedField);
System.out.println("有序: " + jsonObj2);

输出结果
无序: {"name":"ltm","age":99}
有序: {"age":99,"name":"ltm"}

使用散列函数肯定也会有hash冲突的情况,因为字符串有无限种可能,而hash值的位数有限,但是冲突概率极低。

最终生产环境使用的是方案三

上一篇 下一篇

猜你喜欢

热点阅读