溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

關(guān)于Java中HashCode方法的深入理解

發(fā)布時間:2020-10-21 03:35:38 來源:腳本之家 閱讀:175 作者:jajian 欄目:編程語言

1、0前言

最近在學(xué)習(xí) Go 語言,Go 語言中有指針對象,一個指針變量指向了一個值的內(nèi)存地址。學(xué)習(xí)過 C 語言的猿友應(yīng)該都知道指針的概念。Go 語言語法與 C 相近,可以說是類 C 的編程語言,所以 Go 語言中有指針也是很正常的。我們可以通過將取地址符&放在一個變量前使用就會得到相應(yīng)變量的內(nèi)存地址。

package main

import "fmt"

func main() {
 var a int= 20 /* 聲明實際變量 */
 var ip *int /* 聲明指針變量 */

 ip = &a /* 指針變量的存儲地址 */
 fmt.Printf("a 變量的地址是: %x\n", &a )

 /* 指針變量的存儲地址 */
 fmt.Printf("ip 變量儲存的指針地址: %x\n", ip )
 /* 使用指針訪問值 */
 fmt.Printf("*ip 變量的值: %d\n", *ip )
}

因為本人主要開發(fā)語言是 Java,所以我就聯(lián)想到 Java 中沒有指針,那么 Java 中如何獲取變量的內(nèi)存地址呢?

如果能獲取變量的內(nèi)存地址那么就可以清晰的知道兩個對象是否是同一個對象,如果兩個對象的內(nèi)存地址相等那么無疑是同一個對象反之則是不同的對象。

很多人說對象的 HashCode 方法返回的就是對象的內(nèi)存地址,包括我在《Java核心編程·卷I》的第5章內(nèi)容中也發(fā)現(xiàn)說是 HashCode 其值就是對象的內(nèi)存地址。

關(guān)于Java中HashCode方法的深入理解

**但是 HashCode 方法真的是內(nèi)存地址嗎?**回答這個問題前我們先回顧下一些基礎(chǔ)知識。

2|0==和equals

在 Java 中比較兩個對象是否相等主要是通過 ==號,比較的是他們在內(nèi)存中的存放地址。Object 類是 Java 中的超類,是所有類默認(rèn)繼承的,如果一個類沒有重寫 Object 的 equals方法,那么通過equals方法也可以判斷兩個對象是否相同,因為它內(nèi)部就是通過==來實現(xiàn)的。

//Indicates whether some other object is "equal to" this one.
public boolean equals(Object obj) {
 return (this == obj);
}

**Tips:**這里額外解釋個疑惑

我們學(xué)習(xí) Java 的時候知道,Java 的繼承是單繼承,如果所有的類都繼承了 Object 類,那么為何創(chuàng)建一個類的時候還可以extend其他的類?

這里涉及到直接繼承和間接繼承的問題,當(dāng)創(chuàng)建的類沒有通過關(guān)鍵字 extend 顯示繼承指定的類時,類默認(rèn)的直接繼承了Object,A --> Object。當(dāng)創(chuàng)建的類通過關(guān)鍵字 extend 顯示繼承指定的類時,則它間接的繼承了Object類,A --> B --> Object。

這里的相同,是說比較的兩個對象是否是同一個對象,即在內(nèi)存中的地址是否相等。而我們有時候需要比較兩個對象的內(nèi)容是否相同,即類具有自己特有的“邏輯相等”概念,而不是想了解它們是否指向同一個對象。

例如比較如下兩個字符串是否相同String a = "Hello" 和 String b = new String("Hello"),這里的相同有兩種情形,是要比較 a 和 b 是否是同一個對象(內(nèi)存地址是否相同),還是比較它們的內(nèi)容是否相等?這個具體需要怎么區(qū)分呢?

如果使用 == 那么就是比較它們在內(nèi)存中是否是同一個對象,但是 String 對象的默認(rèn)父類也是 Object,所以默認(rèn)的equals方法比較的也是內(nèi)存地址,所以我們要重寫 equals方法,正如 String 源碼中所寫的那樣。

public boolean equals(Object anObject) {
 if (this == anObject) {
  return true;
 }
 if (anObject instanceof String) {
  String anotherString = (String)anObject;
  int n = value.length;
  if (n == anotherString.value.length) {
   char v1[] = value;
   char v2[] = anotherString.value;
   int i = 0;
   while (n-- != 0) {
    if (v1[i] != v2[i])
     return false;
    i++;
   }
   return true;
  }
 }
 return false;
}

這樣當(dāng)我們 a == b時是判斷 a 和 b 是否是同一個對象,a.equals(b)則是比較 a 和 b 的內(nèi)容是否相同,這應(yīng)該很好理解。

JDK 中不止 String 類重寫了equals 方法,還有數(shù)據(jù)類型 Integer,Long,Double,F(xiàn)loat等基本也都重寫了 equals 方法。所以我們在代碼中用 Long 或者 Integer 做業(yè)務(wù)參數(shù)的時候,如果要比較它們是否相等,記得需要使用 equals 方法,而不要使用 ==。

因為使用 ==號會有意想不到的坑出現(xiàn),像這種數(shù)據(jù)類型很多都會在內(nèi)部封裝一個常量池,例如 IntegerCache,LongCache 等等。當(dāng)數(shù)據(jù)值在某個范圍內(nèi)時會直接從常量池中獲取而不會去新建對象。

如果要使用==,可以將這些數(shù)據(jù)包裝類型轉(zhuǎn)換為基本類型之后,再通過==來比較,因為基本類型通過==比較的是數(shù)值,但是在轉(zhuǎn)換的過程中需要注意 NPE(NullPointException)的發(fā)生。

3|0Object中的HashCode

equals 方法能比較兩個對象的內(nèi)容是否相等,因此可以用來查找某個對象是否在集合容器中,通常大致就是逐一去取集合中的每個對象元素與需要查詢的對象進行equals比較,當(dāng)發(fā)現(xiàn)某個元素與要查找的對象進行equals方法比較的結(jié)果相等時,則停止繼續(xù)查找并返回肯定的信息,否則,返回否定的信息。

但是通過這種比較的方式效率很低,時間復(fù)雜度比較高。那么我們是否可以通過某種編碼方式,將每一個對象都具有某個特定的碼值,根據(jù)碼值將對象分組然后劃分到不同的區(qū)域,這樣當(dāng)我們需要在集合中查詢某個對象時,我們先根據(jù)該對象的碼值就能確定該對象存儲在哪一個區(qū)域,然后再到該區(qū)域中通過equals方式比較內(nèi)容是否相等,就能知道該對象是否存在集合中。

通過這種方式我們減少了查詢比較的次數(shù),優(yōu)化了查詢的效率同時也就減少了查詢的時間。

這種編碼方式在 Java 中就是 hashCode 方法,Object 類中默認(rèn)定義了該方法, 它是一個 native 修飾的本地方法,返回值是一個 int 類型。

/**
 * 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}.
 * ...
 * 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™ 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();

從注釋的描述可以知道,hashCode 方法返回該對象的哈希碼值。它可以為像 HashMap 這樣的哈希表有益。Object 類中定義的 hashCode 方法為不同的對象返回不同的整形值。具有迷惑異議的地方就是This is typically implemented by converting the internal address of the object into an integer這一句,意為通常情況下實現(xiàn)的方式是將對象的內(nèi)部地址轉(zhuǎn)換為整形值。

如果你不深究就會認(rèn)為它返回的就是對象的內(nèi)存地址,我們可以繼續(xù)看看它的實現(xiàn),但是因為這里是 native 方法所以我們沒辦法直接在這里看到內(nèi)部是如何實現(xiàn)的。native 方法本身非 java 實現(xiàn),如果想要看源碼,只有下載完整的 jdk 源碼,Oracle 的 JDK 是看不到的,OpenJDK 或其他開源 JRE 是可以找到對應(yīng)的 C/C++ 代碼。我們在 OpenJDK 中找到 Object.c 文件,可以看到hashCode 方法指向 JVM_IHashCode 方法來處理。

static JNINativeMethod methods[] = {
 {"hashCode", "()I",     (void *)&JVM_IHashCode},
 {"wait",  "(J)V",     (void *)&JVM_MonitorWait},
 {"notify",  "()V",     (void *)&JVM_MonitorNotify},
 {"notifyAll", "()V",     (void *)&JVM_MonitorNotifyAll},
 {"clone",  "()Ljava/lang/Object;", (void *)&JVM_Clone},
};

而JVM_IHashCode方法實現(xiàn)在 jvm.cpp中的定義為:

JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle)) 
 JVMWrapper("JVM_IHashCode"); 
 // as implemented in the classic virtual machine; return 0 if object is NULL 
 return handle == NULL ? 0 : ObjectSynchronizer::FastHashCode (THREAD, JNIHandles::resolve_non_null(handle)) ; 
JVM_END 

這里是一個三目表達式,真正計算獲得 hashCode 值的是ObjectSynchronizer::FastHashCode,它具體的實現(xiàn)在synchronizer.cpp中,截取部分關(guān)鍵代碼片段。

intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) {
 if (UseBiasedLocking) {
 
 ......
 
 // Inflate the monitor to set hash code
 monitor = ObjectSynchronizer::inflate(Self, obj);
 // Load displaced header and check it has hash code
 mark = monitor->header();
 assert (mark->is_neutral(), "invariant") ;
 hash = mark->hash();
 if (hash == 0) {
 hash = get_next_hash(Self, obj);
 temp = mark->copy_set_hash(hash); // merge hash code into header
 assert (temp->is_neutral(), "invariant") ;
 test = (markOop) Atomic::cmpxchg_ptr(temp, monitor, mark);
 if (test != mark) {
  // The only update to the header in the monitor (outside GC)
  // is install the hash code. If someone add new usage of
  // displaced header, please update this code
  hash = test->hash();
  assert (test->is_neutral(), "invariant") ;
  assert (hash != 0, "Trivial unexpected object/monitor header usage.");
 }
 }
 // We finally get the hash
 return hash;
}

從以上代碼片段中可以發(fā)現(xiàn),實際計算hashCode的是 get_next_hash,還在這份文件中我們搜索get_next_hash,得到他的關(guān)鍵代碼。

static inline intptr_t get_next_hash(Thread * Self, oop obj) {
 intptr_t value = 0 ;
 if (hashCode == 0) {
   // This form uses an unguarded global Park-Miller RNG,
   // so it's possible for two threads to race and generate the same RNG.
   // On MP system we'll have lots of RW access to a global, so the
   // mechanism induces lots of coherency traffic.
   value = os::random() ;
 } else
 if (hashCode == 1) {
   // This variation has the property of being stable (idempotent)
   // between STW operations. This can be useful in some of the 1-0
   // synchronization schemes.
   intptr_t addrBits = cast_from_oop<intptr_t>(obj) >> 3 ;
   value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
 } else
 if (hashCode == 2) {
   value = 1 ;      // for sensitivity testing
 } else
 if (hashCode == 3) {
   value = ++GVars.hcSequence ;
 } else
 if (hashCode == 4) {
   value = cast_from_oop<intptr_t>(obj) ;
 } else {
   // Marsaglia's xor-shift scheme with thread-specific state
   // This is probably the best overall implementation -- we'll
   // likely make this the default in future releases.
   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;
}

從get_next_hash的方法中我們可以看到,如果從0開始算的話,這里提供了6種計算 hash 值的方案,有自增序列,隨機數(shù),關(guān)聯(lián)內(nèi)存地址等多種方式,其中官方默認(rèn)的是最后一種,即隨機數(shù)生成。可以看出 hashCode 也許和內(nèi)存地址有關(guān)系,但不是直接代表內(nèi)存地址的,具體需要看虛擬機版本和設(shè)置。

4|0equals和hashCode

equals 和 hashCode 都是 Object 類擁有的方法,包括 Object 類中的 toString 方法打印的內(nèi)容也包含 hashCode 的無符號十六進制值。

public String toString() {
  return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

由于需要比較對象內(nèi)容,所以我們通常會重寫 equals 方法,但是重寫 equals 方法的同時也需要重寫 hashCode 方法,有沒有想過為什么?

因為如果不這樣做的話,就會違反 hashCode 的通用約定,從而導(dǎo)致該類無法結(jié)合所有基于散列的集合一起正常工作,這類集合包括 HashMap 和 HashSet。

這里的通用約定,從 Object 類的 hashCode 方法的注釋可以了解,主要包括以下幾個方面,

  • 在應(yīng)用程序的執(zhí)行期間,只要對象的 equals 方法的比較操作所用到的信息沒有被修改,那么對同一個對象的多次調(diào)用,hashCode 方法都必須始終返回同一個值。
  • 如果兩個對象根據(jù) equals 方法比較是相等的,那么調(diào)用這兩個對象中的 hashCode 方法都必須產(chǎn)生同樣的整數(shù)結(jié)果。
  • 如果兩個對象根據(jù) equals 方法比較是不相等的,那么調(diào)用者兩個對象中的 hashCode 方法,則不一定要求 hashCode 方法必須產(chǎn)生不同的結(jié)果。但是給不相等的對象產(chǎn)生不同的整數(shù)散列值,是有可能提高散列表(hash table)的性能。

從理論上來說如果重寫了 equals 方法而沒有重寫 hashCode 方法則違背了上述約定的第二條,相等的對象必須擁有相等的散列值。

但是規(guī)則是大家默契的約定,如果我們就喜歡不走尋常路,在重寫了 equals 方法后沒有覆蓋 hashCode 方法,會產(chǎn)生什么后果嗎?

我們自定義一個 Student 類,并且重寫了 equals 方法,但是我們沒有重寫 hashCode 方法,那么當(dāng)調(diào)用 Student 類的 hashCode 方法的時候,默認(rèn)就是調(diào)用超類 Object 的 hashCode 方法,根據(jù)隨機數(shù)返回的一個整型值。

public class Student {

  private String name;

  private String gender;

  public Student(String name, String gender) {
    this.name = name;
    this.gender = gender;
  }

  //省略 Setter,Gettter
  
  @Override
  public boolean equals(Object anObject) {
    if (this == anObject) {
      return true;
    }
    if (anObject instanceof Student) {
      Student anotherStudent = (Student) anObject;

      if (this.getName() == anotherStudent.getName()
          || this.getGender() == anotherStudent.getGender())
        return true;
    }
    return false;
  }
}

我們創(chuàng)建兩個對象并且設(shè)置屬性值一樣,測試下結(jié)果:

public static void main(String[] args) {

  Student student1 = new Student("小明", "male");
  Student student2 = new Student("小明", "male");

  System.out.println("equals結(jié)果:" + student1.equals(student2));
  System.out.println("對象1的散列值:" + student1.hashCode() + ",對象2的散列值:" + student2.hashCode());
}

得到的結(jié)果

equals結(jié)果:true
對象1的散列值:1058025095,對象2的散列值:665576141

我們重寫了 equals 方法,根據(jù)姓名和性別的屬性來判斷對象的內(nèi)容是否相等,但是 hashCode 由于是調(diào)用 Object 類的 hashCode 方法,所以打印的是兩個不相等的整型值。

如果這個對象我們用 HashMap 存儲,將對象作為 key,熟知 HashMap 原理的同學(xué)應(yīng)該知道,HashMap 是由數(shù)組 + 鏈表的結(jié)構(gòu)組成,這樣的結(jié)果就是因為它們 hashCode 不相等,所以放在了數(shù)組的不同下標(biāo),當(dāng)我們根據(jù) Key 去查詢的時候結(jié)果就為 null。

public static void main(String[] args) {

  Student student1 = new Student("小明", "male");
  Student student2 = new Student("小明", "male");
  
  HashMap<Student, String> hashMap = new HashMap<>();
  hashMap.put(student1, "小明");

  String value = hashMap.get(student2);
  System.out.println(value); 
}

輸出結(jié)果

null

得到的結(jié)果我們肯定不滿意,這里的 student1 和 student2 雖然內(nèi)存地址不同,但是它們的邏輯內(nèi)容相同,我們認(rèn)為它們應(yīng)該是相同的。

這里如果不好理解,猿友可以將 Student 類換成 String 類思考下,String 類是我們常常作為 HashMap 的 Key 值使用的,試想如果 String 類只重寫了 equals 方法而沒有重寫 HashCode 方法,這里將某個字符串 new String("s") 作為 Key 然后 put 一個值,但是再根據(jù) new String("s") 去 Get 的時候卻得到 null 的結(jié)果,這是難以讓人接受的。

所以無論是理論的約定上還是實際編程中,我們重寫 equals 方法的同時總要重寫 hashCode 方法,請記住這點。

雖然 hashCode 方法被重寫了,但是如果我們想要獲取原始的 Object 類中的哈希碼,我們可以通過 System.identityHashCode(Object a)來獲取,該方法返回默認(rèn)的 Object 的 hashCode 方法值,即使對象的 hashCode 方法被重寫了也不影響。

public static native int identityHashCode(Object x);

5|0總結(jié)

如果 HashCode 不是內(nèi)存地址,那么 Java 中怎么獲取內(nèi)存地址呢?找了一圈發(fā)現(xiàn)沒有直接可用的方法。

后來想想也許這是 Java 語言編寫者認(rèn)為沒有直接獲取內(nèi)存地址的必要吧,因為 Java 是一門高級語言相對于機器語言的匯編或者 C 語言來說更抽象并隱藏了復(fù)雜性,因為畢竟是在 C 和 C++ 的基礎(chǔ)上進一步封裝的。而且由于自動垃圾回收機制和對象年齡代的問題,Java 中對象的地址是會變化的,因此獲取實際內(nèi)存地址的意義不大。

當(dāng)然以上是博主本人自己的觀點,如果猿友有其他不同的意見或見解也可以留言,大家一起共同探討。

好了,以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,謝謝大家對億速云的支持。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI