從 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呢?
- 31是一個不大不小的質數,是作為 hashCode 乘子的優選質數之一
- 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
,結果值相對於32
和10,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條),然後針對每個域的屬性進行計算:
- 如果是boolean值,則計算
f ? 1:0
; - 如果是byte\char\short\int,則計算
(int)f
; - 如果是long值,則計算
(int)(f ^ (f >>> 32))
; - 如果是float值,則計算
Float.floatToIntBits(f)
; - 如果是double值,則計算
Double.doubleToLongBits(f)
,然後返回的結果是long,再用規則(3)去處理long,得到int; - 如果是物件應用,如果equals方法中採取遞迴呼叫的比較方式,那麼hashCode中同樣採取遞迴呼叫hashCode的方式。否則需要為這個域計算一個正規化,比如當這個域的值為null的時候,那麼hashCode 值為0;
- 如果是陣列,那麼需要為每個元素當做單獨的域來處理。
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…