天天看點

如何正确的重寫hashcode()

1、簡介

不知道大家有沒有在開發中重寫過hashcode方法,或者在面試中遇到相關的問題。比如一些比較基礎的Java工作崗位可能會問:你有使用過對象作為HashMap的key嗎?

這個問題其實考察的就是程式員對應hashcode方法重寫的相關知識點,如下HashMap的put方法截圖可以看出,往容器中添加元素計算hash值時,調用了key對象的hashcode方法。

如何正确的重寫hashcode()
如何正确的重寫hashcode()

如何正确的重寫hashcode方法?

這其實是一個非常常見而又看似非常簡單的問題,但是真正能寫的很完善的程式員小捌見得确實不多。(往往越迷人的越危險,越簡單的越複雜!!!)

如何正确的重寫hashcode()

大家往下瞅,看看自己屬不屬于那個寫的很完善的程式員!

2、正文

2.1 什麼時候重寫

在深入研究如何重寫hashcode方法之前,必須要先明白什麼時候需要重寫hashcode?

關于這個問題,總結起來就一句話:需要重寫equals方法的類,都需要重寫hashcode方法!

那這個時候你肯定會問,什麼時候需要重寫equals方法呢?

關于這個問題小捌已經在上一篇文章中講過啦,需要的兄弟們可以去我的專欄

《Java小知識100例》系列

看看,順便點波訂閱,關注小捌學習Java不迷路哦!

如何正确的重寫hashcode()

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;

這個方法大緻可以分為兩步:

  1. 如果a==null,則傳回hashcode為0
  2. 如果a != null,則周遊每一個域,域不為null,則調用域的hashcode方法并累加

這其中有一個非常顯眼的數字 31,每次循環時會将目前result*31,這是為什麼呢?

其實每次計算result*31的作用是為了,防止hash沖突!因為如果不設定一個乘積因子,result計算的結果比較小,非常容易在累加的過程後出現相同的hash值,這種情況不是我們想見到的!

那為什麼是31呢?31為什麼能成為JDK計算團隊選中的真命天子,就不能是2?不能是1001?

其實使用31作為乘積因子是有原因的,其原因小捌覺得有三點:

  1. 31是一個不大不小的數,它不會過小導緻hashcode計算的結果容易發生沖突;因為傳回值是一個int整數類型也不至于過大,導緻hashcode傳回值溢出。
  2. 31是一個奇數,一個數與奇數相乘,不容易丢失低位;因為乘以2相當于無符号左移一位,這樣會在低位補0,這樣的話hashcode計算的值,就非常容易沖突了。
  3. 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()

那是不是重寫hashcode方法的時候,都需要乘上31呢?

這肯定不是這樣的啦!乘積因子31隻是為了減小hash沖突的一種解決方案,當你用不上的時候肯定不需要使用乘積因子啦!