重寫 hashcode()真有那麼簡單嘛?

2022-10-13 06:01:02

萬萬沒想到一個 hashcode() 方法,既然會引出一堆的知識盲區,簡直了。

起因:

老八股:為什麼重寫Equals方法要重寫HashCode方法。

大聲告訴我為什麼,閉著眼睛先把答案背出來,啥?這都忘了?沒事,也不是啥大事。這兩句話再Object類的 hashcode 中的註釋上就有,但是一般八股文不會告訴你是出自這裡的。凝聚成兩句話就是:

1 如果兩個物件通過equals()方法比較相等,那麼這兩個物件的hashCode一定相同。

2 如果兩個物件hashCode相同,不能證明兩個物件是同一個物件(不一定相等),只能證明兩個物件在雜湊結構中儲存在同一個地址(不同物件hashCode相同的情況稱為hash衝突)。

  • 標準嘛,很標準,但是還不夠,一般人到這裡已經結束了,但是我還想和麵試官在掰扯一下:從Object類中 hashcode 的註釋上就寫著很明白了,具體如下
    /**
     * Returns a hash code value for the object. This method is
     * supported for the benefit of hash tables such as those provided by
     * {@link java.util.HashMap}.
     * <p>
     * The general contract of {@code hashCode} is:
     * <ul>
     * <li>Whenever it is invoked on the same object more than once during
     *     an execution of a Java application, the {@code hashCode} method
     *     must consistently return the same integer, provided no information
     *     used in {@code equals} comparisons on the object is modified.
     *     This integer need not remain consistent from one execution of an
     *     application to another execution of the same application.
     * <li>If two objects are equal according to the {@code equals(Object)}
     *     method, then calling the {@code hashCode} method on each of
     *     the two objects must produce the same integer result.
     * <li>It is <em>not</em> required that if two objects are unequal
     *     according to the {@link java.lang.Object#equals(java.lang.Object)}
     *     method, then calling the {@code hashCode} method on each of the
     *     two objects must produce distinct integer results.  However, the
     *     programmer should be aware that producing distinct integer results
     *     for unequal objects may improve the performance of hash tables.
     * </ul>
     * <p>
     * As much as is reasonably practical, the hashCode method defined by
     * class {@code Object} does return distinct integers for distinct
     * objects. (This is typically implemented by converting the internal
     * address of the object into an integer, but this implementation
     * technique is not required by the
     * Java&trade; programming language.)
     *
     * @return  a hash code value for this object.
     * @see     java.lang.Object#equals(java.lang.Object)
     * @see     java.lang.System#identityHashCode
     */
    public native int hashCode();
  • 看著很長,其實不然,給你們翻譯一下第一句話---什麼叫中式翻譯,如下

Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by

返回 一個 hash code 值 為 這個 物件。這個 方法 是 支援 的 這個 好處 體現為 hash 表 比如 這些 提供的 {@link java.util.HashMap}.


權威解釋

什麼亂七八糟的,還是來看看比較權威的。

  • Effective Java 第三版 中 描述為什麼重寫equals 方法後必須重寫hashCode 方法

每個覆蓋了equals方法的類中,必須覆蓋hashCode。如果不這麼做,就違背了hashCode的通用約定,也就是上面註釋中所說的。進而導致該類無法結合所以與雜湊的集合一起正常運作,這裡指的是HashMap、HashSet、HashTable、ConcurrentHashMap。

提煉:如果重寫equals不重寫hashCode它與雜湊集合無法正常工作。

總結:恍然大悟,原來是這樣,那我這個物件,不存在這些雜湊集合中不就不用遵守這個規則了嘛。哈哈,大聰明,理論上確實是這樣。但是這篇文章我還想再寫長一些,多佔用一些面試時間,畢竟我還有主動權機會。

hashcode()」偽原始碼「

  • 首先宣告一點:Object 提供的 hashCode() 方法返回值是不會重複的(也就是說每個物件返回的值都不一樣)。

有人說,扯淡嘛,我看了Object類中的hashcode()方法,裡面什麼也沒有啊,只有方法,沒看到實現,哪裡來的返回值呢。

public native int hashCode();

關鍵字 native

首先看到看關鍵字 native,知識盲區來了,很多人不知道 這是啥東西,當然,說起這個,就要從盤古開天闢地開始講了,我們這一節也不著重講這個,篇幅有限,感興趣的去問問度娘吧。

  • 先看概念:
native 用來修飾方法,用 native 宣告的方法表示告知 JVM 呼叫,該方法在外部定義,我們可以用任何語言去實現它。簡單地講,一個native Method就是一個 Java 呼叫非 Java 程式碼的介面。
  • 簡單來說:我們在日常程式設計中看到native修飾的方法,只需要知道這個方法的作用是什麼,至於別的就不用管了,作業系統會給我們實現。

原來是這樣,要是我寫的程式碼也能這樣就好了,我就寫個方法,用 native 修飾,然後作業系統就給我自動實現。

想的美了,其實操作給我們實現,也是其他程式設計師寫好的程式碼提前編譯好了,換句話來說就是:哪有歲月靜好,只是有猿替你負重前行罷了。

  • 作為一個Java程式設計師,我們還是想了解一些底層JDK程式碼的。所以,必須把原始碼扒出來,哪怕看不懂。(這是openjdk中的原始碼)。

JDK目錄

  • 知識盲區,有一說一,能把 JDK 原始碼讀懂的人,真大神也。
openjdk
├ corba:不流行的多語言、分散式通訊介面
├ hotspot:Java 虛擬機器器
├ jaxp:XML 處理
├ jaxws:一組 XML web services 的 Java API
├ jdk:java 開發工具包
├ langtools:Java 語言工具
└ nashorn:JVM 上的 JavaScript 執行時

hotspot目錄

  • 什麼叫原始碼,什麼叫底層,這就叫專業!!!
hotspot
├─agent                            Serviceability Agent的使用者端實現
├─make                             用來build出HotSpot的各種組態檔
├─src                              HotSpot VM的原始碼
│  ├─cpu                            CPU相關程式碼(組合器、模板直譯器、ad檔案、部分runtime函數在這裡實現)
│  ├─os                             操作系相關程式碼
│  ├─os_cpu                         作業系統+CPU的組合相關的程式碼
│  └─share                          平臺無關的共通程式碼
│      ├─tools                        工具
│      │  ├─hsdis                      反組合外掛
│      │  ├─IdealGraphVisualizer       將server編譯器的中間程式碼視覺化的工具
│      │  ├─launcher                   啟動程式「java」
│      │  ├─LogCompilation             將-XX:+LogCompilation輸出的紀錄檔(hotspot.log)整理成更容易閱讀的格式的工具
│      │  └─ProjectCreator             生成Visual Studio的project檔案的工具
│      └─vm                           HotSpot VM的核心程式碼
│          ├─adlc                       平臺描述檔案(上面的cpu或os_cpu裡的*.ad檔案)的編譯器
│          ├─asm                        組合器介面
│          ├─c1                         client編譯器(又稱「C1」)
│          ├─ci                         動態編譯器的公共服務/從動態編譯器到VM的介面
│          ├─classfile                  類檔案的處理(包括類載入和系統符號表等)
│          ├─code                       動態生成的程式碼的管理
│          ├─compiler                   從VM呼叫動態編譯器的介面
│          ├─gc_implementation          GC的實現
│          │  ├─concurrentMarkSweep      Concurrent Mark Sweep GC的實現
│          │  ├─g1                       Garbage-First GC的實現(不使用老的分代式GC框架)
│          │  ├─parallelScavenge         ParallelScavenge GC的實現(server VM預設,不使用老的分代式GC框架)
│          │  ├─parNew                   ParNew GC的實現
│          │  └─shared                   GC的共通實現
│          ├─gc_interface               GC的介面
│          ├─interpreter                直譯器,包括「模板直譯器」(官方版在用)和「C++直譯器」(官方版不在用)
│          ├─libadt                     一些抽象資料結構
│          ├─memory                     記憶體管理相關(老的分代式GC框架也在這裡)
│          ├─oops                       HotSpot VM的物件系統的實現
│          ├─opto                       server編譯器(又稱「C2」或「Opto」)
│          ├─prims                      HotSpot VM的對外介面,包括部分標準庫的native部分和JVMTI實現
│          ├─runtime                    執行時支援庫(包括執行緒管理、編譯器排程、鎖、反射等)
│          ├─services                   主要是用來支援JMX之類的管理功能的介面
│          ├─shark                      基於LLVM的JIT編譯器(官方版裡沒有使用)
│          └─utilities                  一些基本的工具類
└─test                             單元測試

hashcode() 真原始碼

  • 在openjdk8根路徑/hotspot/src/share/vm/runtime路徑下的synchronizer.cpp檔案中,有生成雜湊值的程式碼:
static inline intptr_t get_next_hash(Thread * Self, oop obj) {
  intptr_t value = 0 ;
  if (hashCode == 0) {
     // 返回亂數
     value = os::random() ;
  } else
  if (hashCode == 1) {
     //用物件的記憶體地址根據某種演演算法進行計算
     intptr_t addrBits = cast_from_oop<intptr_t>(obj) >> 3 ;
     value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
  } else
  if (hashCode == 2) {
     // 始終返回1,用於測試
     value = 1 ;            
  } else
  if (hashCode == 3) {
     //從0開始計算雜湊值
     value = ++GVars.hcSequence ;
  } else
  if (hashCode == 4) {
     //輸出物件的記憶體地址
     value = cast_from_oop<intptr_t>(obj) ;
  } else {
     // 預設的hashCode生成演演算法,利用xor-shift演演算法產生偽亂數
     unsigned t = Self->_hashStateX ;
     t ^= (t << 11) ;
     Self->_hashStateX = Self->_hashStateY ;
     Self->_hashStateY = Self->_hashStateZ ;
     Self->_hashStateZ = Self->_hashStateW ;
     unsigned v = Self->_hashStateW ;
     v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
     Self->_hashStateW = v ;
     value = v ;
  }

  value &= markOopDesc::hash_mask;
  if (value == 0) value = 0xBAD ;
  assert (value != markOopDesc::no_hash, "invariant") ;
  TEVENT (hashCode: GENERATE) ;
  return value;
}

先上一首涼涼。不是看不懂啊,畢竟大學學過C,但是腦瓜子嗡嗡的,肯定不能專研這個了,先跳過,回到正題。

為什麼要重寫hashcode我們大概知道了,那如果我偏不重寫呢,先來個鐵頭娃demo看看效果。

    public class Person {
        //使用者Id,唯一確定使用者
        private String id;
        private String name;

        public Person(String id, String name) {
            this.id = id;
            this.name = name;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Person)) return false;
            Person person = (Person) o;
            return Objects.equals(id, person.id) && Objects.equals(name, person.name);
        }

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

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

    @Test
    public void test() {
        HashMap<Person, Integer> map = new HashMap<>();
        //key:Person型別  value:Integer型別
        Person personKey = new Person("1","張三");
        Person getKey = new Person("1","張三");
        map.put(personKey,100);
        System.out.println("原物件map.get():"+map.get(personKey));
        System.out.println("值相同的物件map.get():"+map.get(getKey));
    }

輸出結果:

-------測試前準備-建立物件-------
原物件map.get():100
值相同的物件map.get():null
-------測試結束-------

這裡我們很明顯看到,我們要用之前的物件才能從HashMap中獲取到key。這不是我們想要的效果啊,再也不頭鐵了。


  • 這時有人就會說了,那我用之前的物件不就好了嘛,幹嘛要new個新的物件呢?其實這也是demo和實際專案的差距。如果你做過專案的就知道,我們的實體類每次請求進來都是個新的物件,最最最不濟,你什麼PO轉DO,DO轉DTO不就是要建立新的物件嘛。

吶吶吶,問題又來了,我不用Map不就行了,可以。當然可以,但是現在的專案裡面,如果說整個專案沒有用到Map的,那這個專案估計也不叫專案了,寫java的人,誰能拒絕來個Map呢。

  • OK,最後啊,這hashcode()方法到底應該怎麼寫呢,這也頭大啊,一個hashcode()咋就那麼難呢,來來來思考兩分鐘,你想好怎麼寫了嘛。先別說,我把前提撩在這,你在仔細想想。一個是 」相同的物件「 的 hashcode() 要相同,不同物件的 hashcode() 不能相同。比如 :
        Person key = new Person("1","張三");
        Person sameKey = new Person("1","張三");
        Person unSameKey = new Person("2","張三");

其中 key 和 sameKey 的 hashcode()一定要相同,因為我們的業務員就是認為他們應該是相同的。但是同時 unSame 和 key 的 hashcode() 一定不能相同。

  • 這個時候,使用Object的 hashcode() 很顯然就是不行了,應為它返回每個物件的 hashcode() 都是不相同的。

  • 當然了,所有 hashcode() 都返回 0 ,這也不是不行,只是違背了這句話:如果兩個物件hashCode相同,不能證明兩個物件是同一個物件(不一定相等)。

來吧,開始發散思維。

方法一:

  • 使用String 的hashcode(),你物件裡面的屬性不是String 型別嘛,String 裡面已經重寫了 hashcod() 方法那我直接用現成的就好了。
        @Override
        public int hashCode() {
            return  id.hashCode() + name.hashCode();
        }

但是我們還是要從各個角度出發去想一下的,比如判空,是否會值溢位之類的,這裡我們也參考String 的 hashcode() 套娃一個。優化後:

        @Override
        public int hashCode() {
            //初始值,別問為什麼是 17, 因為我寫 18 你也會這樣問的
            int result = 17;
            // 31 作為基數,不知道啊,String 裡面 的 hashcode() 就是寫的 31
            result = 31 * result + (id == null ? 0 : id.hashCode());
            result = 31 * result + (name == null ? 0 : name.hashCode());
            return result;
        }

方法二:

  • 呼叫JDK提供好的Objects裡面的hashcode()
 @Override
    public int hashCode() {
        return Objects.hash(id,name);
    }

看了這兩個方法,沒錯,這都是站在巨人的肩膀上。其實要是自己動手實現,那難度就直線上升了,至少演演算法這一塊要啃一下。

hashcode() 方重寫規則

寫hashcode()要遵循以下原則:

equals不相等時並不強制要求雜湊值相等,但是一個優秀的雜湊演演算法應儘可能讓元素均勻分佈,降低衝突發生的概率,即在equals不相等時儘量讓雜湊值也不相等,這樣&&或||短路操作一旦生效,會極大提高程式的效率。

重寫equals都是根據業務需求去進行重寫的,想想為什麼String是這麼判斷兩個字串物件相等的。如果拋開業務需求談這個,就是耍流氓。但是大多數情況下,都是不用重寫 equals() 和 hashcode() 的,就是因為有了特殊的業務邏輯,所以我們才需要重寫裡面的邏輯。

知識盲區

不想寫了, 今天就先結束吧,呼應一下開頭,看看都涉及到哪些知識點,下次再遇到這樣的八股文,一開口就能聊幾個小時。

1、hashcode() 方法是 native 關鍵字修飾的,你有了解過嘛、Java方法和本地方法有什麼不同、為什麼要註冊本地方法呢?

2、Object類中並沒有hashcode() 方法原始碼,真正的原始碼應該去哪裡看

3、jdk 原始碼目錄,你真正開啟過jdk原始碼嘛

4、 hashcode() 原始碼上有 @see java.lang.System#identityHashCode。那你知道 這兩個方法有什麼區別嘛

5、hashcode() 方法一定要重寫,強調的是和hash表相關,那你知道原始碼中都是這麼體現的嘛

6、hashcode() 手寫實現,需要有什麼功底呢?

7、呼叫了 String 的 hashcode() 去實現,那你知道 String#hashcode() 中為什麼要用常數 31 嘛

8、如果不從寫 hashcode() 會發生什麼呢

9、Objects#hash()實現和 String 的 hashcode() 有什麼異同呢

10、hashmap 中的 hash演演算法和 hashcode 有什麼關係呢,hahscode會不會影響到 hash演演算法的結果?

11、如果hashcode() 每次返回的數是一個亂數,會發生什麼。

12、為什麼先比較 hashcode() 在比較 equlas能提高效率呢?List 集合比較的弊端、Map key 實現的優勢