1. 程式人生 > 程式設計 >從 hash碰撞說到重寫hashCode

從 hash碰撞說到重寫hashCode

雜湊碰撞是什麼?

所謂雜湊(hash),就是將不同的輸入對映成獨一無二的、固定長度的值(又稱"雜湊值")。它是最常見的軟體運算之一。

如果不同的輸入得到了同一個雜湊值,就發生了"雜湊碰撞"(collision)。

舉例來說,很多網路服務會使用雜湊函式,產生一個 token,標識使用者的身份和許可權。

AFGG2piXh0ht6dmXUxqv4nA1PU120r0yMAQhuc13i8
複製程式碼

上面這個字串就是一個雜湊值。如果兩個不同的使用者,得到了同樣的 token,就發生了雜湊碰撞。伺服器將把這兩個使用者視為同一個人,這意味著,使用者 B 可以讀取和更改使用者 A 的資訊,這無疑帶來了很大的安全隱患。

黑客攻擊的一種方法,就是設法制造"雜湊碰撞",然後入侵系統,竊取資訊。

如何防止雜湊碰撞?

防止雜湊碰撞的最有效方法,就是擴大雜湊值的取值空間。

16個二進位制位的雜湊值,產生碰撞的可能性是 65536 分之一。也就是說,如果有65537個使用者,就一定會產生碰撞。雜湊值的長度擴大到32個二進位制位,碰撞的可能性就會下降到 4,294,967,296 分之一。

更長的雜湊值意味著更大的儲存空間、更多的計算,將影響效能和成本。開發者必須做出抉擇,在安全與成本之間找到平衡。

從String的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; }複製程式碼

大概就是

假設 n=3
i=0 -> h = 31 * 0 + val[0]
i=1 -> h = 31 * (31 * 0 + val[0]) + val[1]
i=2
-> h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2] h = 31*31*31*0 + 31*31*val[0] + 31*val[1] + val[2] h = 31^(n-1)*val[0] + 31^(n-2)*val[1] + val[2]複製程式碼

為什麼是質數

選擇質數是為了在雜湊桶中最佳地分佈資料,跟其他數相乘的結果唯一的可能性也很大。

那麼為什麼是31呢?

  1. 31是一個不大不小的質數,是作為 hashCode 乘子的優選質數之一
  2. 31可以被 JVM 優化,31 * i = (i << 5) - 1

那為什麼不是2、7、101 這麼大的質數呢?

以String str = “abcde”為例子,大概計算大致

  • 2的時候 -> 2^5 = 32
  • 101的時候 -> 101^5 = 10,510,100,501

我們從上面一二知道,雜湊值越大產生雜湊碰撞的機率越小,但佔用空間相應地也會變大

最後,我們再來看看質數31的計算結果: 31^5 = 28629151,結果值相對於3210,501來說。是不是很nice,不大不小

另外 effective java裡面有這麼一句話
選擇數字31是因為它是一個奇質數,如果選擇一個偶數會在乘法運算中產生溢位,導致數值資訊丟失,因為乘二相當於移位運算。選擇質數的優勢並不是特別的明顯,但這是一個傳統。同時,數字31有一個很好的特性,即乘法運算可以被移位和減法運算取代,來獲取更好的效能:31 * i == (i << 5) - i,現代的 Java 虛擬機器器可以自動的完成這個優化。

重寫hashCode()

《Effective Java》中提出了一種簡單通用的hashCode演演算法

A、初始化一個整形變數,為此變數賦予一個非零的常數值,比如int result = 17;//31

B、選取equals方法中用於比較的所有域(之所以只選擇equals()中使用的域,是為了保證上述原則的第1條),然後針對每個域的屬性進行計算:

  1. 如果是boolean值,則計算 f ? 1:0
  2. 如果是byte\char\short\int,則計算 (int)f
  3. 如果是long值,則計算 (int)(f ^ (f >>> 32))
  4. 如果是float值,則計算 Float.floatToIntBits(f)
  5. 如果是double值,則計算 Double.doubleToLongBits(f),然後返回的結果是long,再用規則(3)去處理long,得到int;
  6. 如果是物件應用,如果equals方法中採取遞迴呼叫的比較方式,那麼hashCode中同樣採取遞迴呼叫hashCode的方式。否則需要為這個域計算一個正規化,比如當這個域的值為null的時候,那麼hashCode 值為0;
  7. 如果是陣列,那麼需要為每個元素當做單獨的域來處理。java.util.Arrays.hashCode 方法包含了8種基本型別陣列和引用陣列的hashCode計算,演演算法同上。

C、最後,把每個域的雜湊碼合併到物件的雜湊碼中。

public class Person {
    private String name;
    private int age;
    private boolean gender;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public boolean isGender() {
        return gender;
    }

    public void setGender(boolean gender) {
        this.gender = gender;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                gender == person.gender &&
                Objects.equals(name,person.name);
    }

    @Override
    public int hashCode() {
        int hash = 17;
        hash = hash * 31 + getName().hashCode();
        hash = hash * 31 + isGender() ? 1:0;        hash = hash * 31 + getAge();
        return hash;
    }複製程式碼

參考: 

www.ruanyifeng.com/blog/2018/0…

segmentfault.com/a/119000001…