1、簡介
不知道大家有沒有在開發中重寫過hashcode方法,或者在面試中遇到相關的問題。比如一些比較基礎的Java工作崗位可能會問:你有使用過對象作為HashMap的key嗎?
這個問題其實考察的就是程式員對應hashcode方法重寫的相關知識點,如下HashMap的put方法截圖可以看出,往容器中添加元素計算hash值時,調用了key對象的hashcode方法。
如何正确的重寫hashcode方法?
這其實是一個非常常見而又看似非常簡單的問題,但是真正能寫的很完善的程式員小捌見得确實不多。(往往越迷人的越危險,越簡單的越複雜!!!)
大家往下瞅,看看自己屬不屬于那個寫的很完善的程式員!
2、正文
2.1 什麼時候重寫
在深入研究如何重寫hashcode方法之前,必須要先明白什麼時候需要重寫hashcode?
關于這個問題,總結起來就一句話:需要重寫equals方法的類,都需要重寫hashcode方法!
那這個時候你肯定會問,什麼時候需要重寫equals方法呢?
關于這個問題小捌已經在上一篇文章中講過啦,需要的兄弟們可以去我的專欄
《Java小知識100例》系列看看,順便點波訂閱,關注小捌學習Java不迷路哦!
2.2 如何重寫
hashcode方法是Java的java.lang.Object提供的本地方法,這個方法在jvm中實作,它能傳回目前對象在記憶體中位址。
// 傳回對象在記憶體中的位址
public native int hashCode();
是以當我們的類未重寫hashcode方法,且類的其餘超類也未重寫;那麼我們在調用hashcode方法時,它将永遠傳回的是對象的記憶體位址。這可能不是你想要的結果,那我們如何來重寫它呢?
思路
首先我們需要知道,我們是通過對象的域來計算hash的,在對象中域無非數組、引用類型、基本資料類型,有這麼多類型的域,我們肯定不能選擇某一個域的hash值來作為對象的hashcode方法的傳回值;是以我們考慮将域的hash值累加起來傳回!
- 基本資料類型,大家可以參考其對應的包裝類型的hashcode方法
- 引用類型則直接調用hashcode()
- 數組類型則需要周遊數組,依次調用hashcode()
通用實作
這是java.util.Objects提供的hash方法,用于計算hashcode。雖然這個不是一個計算hashcode的銀彈,但是我們可以借鑒這種實作,而且Java JDK源碼中大部分類的hashcode都是類似這種實作方式!
public static int hash(Object... values) {
return Arrays.hashCode(values);
}
public static int hashCode(Object a[]) {
if (a == null)
return 0;
int result = 1;
for (Object element : a)
result = 31 * result + (element == null ? 0 : element.hashCode());
return result;
這個方法大緻可以分為兩步:
- 如果a==null,則傳回hashcode為0
- 如果a != null,則周遊每一個域,域不為null,則調用域的hashcode方法并累加
這其中有一個非常顯眼的數字 31,每次循環時會将目前result*31,這是為什麼呢?
其實每次計算result*31的作用是為了,防止hash沖突!因為如果不設定一個乘積因子,result計算的結果比較小,非常容易在累加的過程後出現相同的hash值,這種情況不是我們想見到的!
那為什麼是31呢?31為什麼能成為JDK計算團隊選中的真命天子,就不能是2?不能是1001?
其實使用31作為乘積因子是有原因的,其原因小捌覺得有三點:
- 31是一個不大不小的數,它不會過小導緻hashcode計算的結果容易發生沖突;因為傳回值是一個int整數類型也不至于過大,導緻hashcode傳回值溢出。
- 31是一個奇數,一個數與奇數相乘,不容易丢失低位;因為乘以2相當于無符号左移一位,這樣會在低位補0,這樣的話hashcode計算的值,就非常容易沖突了。
- 31對虛拟機的識别非常友好,對于虛拟機來說31 = 2^5 - 1,他能針對這種數字做優化并轉換為位運算,是以相乘的時候性能較好
小捌在這裡分别用乘積因子2和乘積因子31做個測試:
package com.liziba.part2;
import org.apache.commons.lang3.RandomStringUtils;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
/**
* <p>
* HashCode方法測試
* </p>
*
* @Author: Liziba
* @Date: 2021/10/24 11:54
*/
public class HashCodeMethodDemo {
/**
* 計算hashcode
*
* @param value 需計算hashcode字元串
* @param capacity 乘數因子
* @return
*/
public static int hashCode(String value, int capacity) {
int hash = 0;
if (Objects.nonNull(value) && value.length() > 0) {
char[] chars = value.toCharArray();
for (int i = 0; i < chars.length; i++) {
hash = capacity * hash + chars[i];
}
}
return hash;
}
* hash值沖突比較
* @param capacity
* @param hashValues
public static void conflictCompare(int capacity, List<Integer> hashValues) {
Comparator<Integer> comparator = (x, y) -> (x > y) ? 1 : ((x < y) ? -1 : 0);
Integer max = hashValues.stream().max(comparator).get();
Integer min = hashValues.stream().min(comparator).get();
long conflictNum = hashValues.size() - hashValues.stream().distinct().count();
double conflictRate = conflictNum * 1.0 / hashValues.size() ;
System.out.println(String.format("乘數因子capacity=%d 沖突數=%d 沖突率:%.4f%% 最大值:%d 最小hashCode:%d",
capacity, conflictNum, conflictRate * 100, max, min));
public static void main(String[] args) {
int num = 100000;
int capacity2 = 2;
int capacity31 = 31;
List<Integer> hashValues2 = new ArrayList<>(num);
List<Integer> hashValues31 = new ArrayList<>(num);
for (int i = 0; i < num; i++) {
// 生成随機數 org.apache.commons.lang3.RandomStringUtils
String value = RandomStringUtils.randomAlphabetic(15);
hashValues2.add(hashCode(value, capacity2));
hashValues31.add(hashCode(value, capacity31));
conflictCompare(capacity2, hashValues2);
conflictCompare(capacity31, hashValues31);
一共測試10萬個15位長的随機字元串
- 當乘數因子為2時,沖突率接近4%
- 當乘數因子為31時,沖突率隻有0.0010%
那是不是重寫hashcode方法的時候,都需要乘上31呢?
這肯定不是這樣的啦!乘積因子31隻是為了減小hash沖突的一種解決方案,當你用不上的時候肯定不需要使用乘積因子啦!