Java的== 、equals以及hashCode用法淺析

一、背景

在開發或面試的過程中,經常都會遇到數值與數值的比較,對象與對象的比較。看似越基礎的東西,越容易踩坑,也是筆試/面試中常見的一類問題。所以,我總結了一下Java中的==、equals以及hashCode相關的一些東西。

二、"=="

"=="操作符主要比較的是操作符兩端對象的內存地址。如果兩個對象的內存地址是一致的,那麼就返回true。反之,則返回false。

話不多說,先來看看下面這段代碼:

public static void main(String[] args) {
String a = "www.jikeh.cn";
String b = "www.jikeh.cn";
String c= "www" + ".jikeh." + "cn";
String d = new String("www.jikeh.cn");

System.out.println("a == b :" + (a == b));
System.out.println("a == c :" + (a == c));
System.out.println("a == d :" + (a == d));
}

我們看下輸出結果:

a == b :true
a == c :true
a == d :false

和你預想的結果一樣嗎?

解釋一下:

Java創建字符串,有兩種形式:

1、非new形式

String a = "www.jikeh.cn";

在JVM中,為了減少對字符串變量的重複創建,其擁有一段特殊的內存空間,這段內存被稱為

字符串常量池(constant pool)。

  • 在上面程序中,我們首先使用String a = "www.jikeh.cn"創建了一個對象。

當JVM在字符串常量池中沒有找到內容為www.jikeh.cn的對象時,就會創建一個內容為www.jikeh.cn的對象,並將它的引用返回給變量a。

  • 那麼當我們在後面使用String b = "www.jikeh.cn"的時候,會發生什麼情況呢?

首先JVM會在字符串常量池中查詢是否有內容為www.jikeh.cn的對象。因為此時字符串常量池中已經有內容為www.jikeh.cn的對象了,故JVM不會創建新的地址空間,而是將原有的www.jikeh.cn對象的引用返回給b。所以此時變量a和變量b擁有的是同一個對象引用,即指向的是同一塊內存地址,因此使用 == 操作符對a和b進行比較,結果為true。

2、new 形式

String d = new String("www.jikeh.cn");

然而當我們new一個字符串對象時,不管字符串常量池中是否有內容相同的對象,JVM都會創造一個新的對象,所以地址肯定不一樣,因此使用 == 操作符對a和d進行比較,結果為false。

三、equals

1、基本原理

在Object類中,equals方法源碼如下:

public boolean equals(Object obj) {
return (this == obj);
}

我們可以看到,在Object類中的equals方法其實在內部也是使用了==操作符:如果兩個對象地址一樣則返回true,反之則返回false。

既然equals方法裡面也是使用==,那為什麼還要設立一個equals方法,而不是直接用==操作符呢?

別急,這只是Object類裡面的equals方法,而絕大部分Object的子類對equals方法進行了改寫:

我們先看一下Integer這個類的equals實現

public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}

上面這個類中的equals方法比較的就是對象所包含的內容。如果兩個對象所包含的內容相同,equals方法就會返回true。

其實在很多時候,我們都需要改寫equals方法以適應我們的實際情況:

2、舉個例子

下面是一個Person類,包含name和age兩個屬性。

public class Person {

private String name;
private String age;

public Person() {
}

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

public String getName() {
return name;
}

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

public String getAge() {
return age;
}

public void setAge(String age) {
this.age = age;
}
}

現在我們創建兩個Person對象,但是name和age分別都設置一樣的值,同時使用equals方法對這兩個對象進行比較:

public static void main(String[] args) {
Person one = new Person("www.jikeh.cn", 18);
Person two = new Person("www.jikeh.cn", 18);
System.out.println("one.equals(two):" + one.equals(two));
}

我們看下輸出結果:

one.equals(two):false

雖然這從JVM的角度來看這個程序是對的,可是結果並不是我們想要的:對象one和對象two雖然是兩個不同的對象,但是它們包含的元素的值是相同的,也就是說one和two應該都表示的是同一個人(name 為 www.jikeh.cn,年齡為18)。

可是為什麼調用equals方法之後輸出的卻是false呢?

原因:由於我們沒有對equals方法進行改寫,所以當我們在調用equals方法的時候實際上調用的是Object類的equals方法。從前面我們可以得知,Object的equals方法在內部是直接使用==操作符對對象進行比較,這樣當然會返回false啦!

下面我將對equals方法進行改寫,以滿足我們的實際需求:

public boolean equals(Object obj) {
if(!(obj instanceof Person)){
return false;
}
Person other = (Person) obj;
return (other.getAge() == this.getAge()) && (other.getName() == this.getName());
}

我們再看下輸出結果:

one.equals(two):true

四、使用"=="操作符和equals方法時發生的自動裝箱與自動拆箱現象

1、什麼是自動拆箱和自動裝箱?

自動裝箱:將Java的基本類型轉換成包裝器類型;

自動拆箱:將Java的包裝器類型轉換成基本類型。

Java的== 、equals以及hashCode用法淺析

public static void main(String[] args) {
Integer i = 1;//自動裝箱
int a = i; //自動拆箱
}

這是一個非常簡單的程序,但是實際上Java自動幫我們完成了拆裝箱的操作。

2、底層原理

在JDK1.5之前,如果我們要生成一個數值為1的Integer對象,那麼我們必須使用下面這行代碼:

Integer i = new Integer(1);

可是引入了自動裝箱功能之後,我們只需使用這樣一行代碼就能完成了:

Integer i = 1;

底層是如何實現的呢?下面我們將上面那段代碼的class文件反編譯一下:

public static void main(String[] paramArrayOfString) {
Integer localInteger = Integer.valueOf(1);//自動裝箱
int i = localInteger.intValue();//自動拆箱
}

我們可以看到,其實Java是使用了 Integer的valueOf(int)方法來完成自動裝箱的,而在拆箱過程當中調用的是Integer的intValue方法。

對於其他的包裝器類型來說,其實這兩個過程也是類似的。

裝箱過程是通過調用包裝器類型的valueOf方法實現的,拆箱過程是通過調用包裝器類型的xxValue方法實現的(其中xx代表的是對應的基本類型)。

3、拓展

讓我們來看看下面這段代碼:

public static void main(String[] paramArrayOfString) {
Integer a = 100;
Integer b = 100;
Integer c = 200;
Integer d = 200;
System.out.println(a == b);//true
System.out.println(c == d);//false
System.out.println(c.equals(d));//true
}

為什麼上面兩條打印代碼會輸出不同的結果?

其實原因也很簡單:

與字符串常量池類似,這其實也是JVM節省內存的一個方法,對於Integer類型的對象來說,如果我們要創建的Integer對象的數值在 [-128,127]的區間之內,那麼JVM就會在緩存中查找,看看有沒有已經存放在緩存中的數值一樣的Integer對象。如果有,就返回已經存在的對象的引用。

五、hashCode詳解

1、官方解讀

官方文檔中隊hashCode方法的描述:

Java的== 、equals以及hashCode用法淺析

我們翻譯一下上面這段文字:

public int hashCode() 返回該對象的哈希碼值。支持此方法是為了提高哈希表(例如java.util.Hashtable 提供的哈希表)的性能。

hashCode的常規協定是:

  • 在 Java 應用程序執行期間,在對同一對象多次調用hashCode方法時,必須一致地返回相同的整數,前提是將對象進行equals比較時所用的信息沒有被修改。從某一應用程序的一次執行到同一應用程序的另一次執行,該整數無需保持一致。
  • 如果根據equals(Object)方法,兩個對象是相等的,那麼對這兩個對象中的每個對象調用hashCode方法都必須生成相同的整數結果。
  • 如果根據equals(java.lang.Object)方法,兩個對象不相等,那麼對這兩個對象中的任一對象上調用hashCode方法不要求一定生成不同的整數結果。但是,程序員應該意識到,為不相等的對象生成不同整數結果可以提高哈希表的性能。

實際上,由Object類定義的 hashCode 方法確實會針對不同的對象返回不同的整數。(這一般是通過將該對象的內部地址轉換成一個整數來實現的。)

2、通俗解讀

hashCode就是用來在散列存儲結構(如:Hashtable、HashMap)中確定對象存儲的位置的。

  • hashCode可以提高在散列存儲結構(Hashtable、HashMap)中查找的快捷性
  • 如果兩個對象相同,即調用equals方法返回的是true,那麼它倆的hashCode值也要相同;
  • 如果equals方法被改寫了,那麼hashCode方法也儘量要改寫,並且產生hashCode的對象也要和equals的對象保持一致;
  • 兩個對象的hashCode相同並不代表兩個對象就一定相同,也就是不一定適用於equals(java.lang.Object)方法,只能夠說明這兩個對象在散列存儲結構(Hashtable、HashMap)中,是存放在同一個位置的。

3、大白話解讀

  • hashcode是用來查找的

hashcode在散列存儲結構(Hashtable、HashMap)中確定對象存儲位置的過程是怎樣的呢?我們通過一個例子來說明一下:

內存中加入有8個存儲位置:01234567,而我有個對象,這個對象有個字段叫ID,我要把這個對象存放在以上8個位置之一,如果不用hashcode而任意存放,那麼當查找時就需要到這八個位置裡挨個去找,或者用二分法一類的算法。但如果用hashcode那就會使效率提高很多。

思路分析:我們這個對象中有個字段叫ID,那麼我們就定義我們的hashcode為ID%8,然後把我們的對象存放在取得得餘數的那個位置。

如果ID是9,9除8的餘數為1,那麼我們就把該對象存在1這個位置,如果ID是13,求得的餘數是5,那麼我們就把該對象放在5這個位置。這樣,以後在查找該對象時就可以通過ID除8求餘數直接找到存放的位置了。

  • 如果兩個對象有相同的hashcode怎麼辦呢?

例如9除以8和17除以8的餘數都是1。其存儲形式是怎樣的呢?當然這樣也是合法的,如果你瞭解hashmap的底層原理,就更容易理解了,詳情參考這篇文章:

4、雙劍合璧:hashcode+equals,比較對象

我們以往hashset(Set中的元素是不重複的)裡面存放對象為例,來詳細說明這一過程:

  • 兩個對象的hashcode不同

這個不同多說,hashcode不同,類肯定也就不同

  • 兩個對象的hashcode相同

在這個時候,我們就需要用到equals了。 也就是說,我們先通過hashcode來判斷兩個對象是否存放某個桶裡,但這個桶裡的很多對象的hashcode可能相等,那麼我們就需要再通過equals來在這個桶裡找到我們真正要的對象。

  • 那麼,有人又問了,既然重寫了equals()可以判定對象的不同,為什麼還要重寫hashCode()呢?

想想,你如果進行兩個對象進行比較,首先得把對象找到才可以,首先我們必須採用一種查找算法來找到這個對象,然後才能通過equals進行對象的比較,光重寫equals()是沒有什麼鳥用的哦!

  • 實戰測試

下面是代碼示例: 我新建了一個Person類,裡面定義了一個屬性為age,並改寫了hashCode方法:

public class Person {
private int age;

public Person() {
}

public Person(int age) {
this.age = age;

}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public int hashCode() {
return age % 3;
}
}

現在我new兩個對象,這兩個對象的age我都賦予相同的值,並將它們兩個存入一個Set(Set中的元素是不重複的)當中,然後分別輸出它們兩個的hashCode以及使用equals方法比較的結果以及將這個Set的輸出:

public static void main(String[] args) {
Person one = new Person(18);
Person two = new Person(18);
Set<person> hashSetPerson = new HashSet<>();
hashSetPerson.add(one);
hashSetPerson.add(two);
System.out.println(one.hashCode() == two.hashCode());
System.out.println(one.equals(two));
System.out.println(hashSetPerson);
}
/<person>

我們看下輸出結果:

true
false
[com.jikeh.Person@0, com.jikeh.Person@0]

以上這個示例,我們只是重寫了hashCode方法,從上面的結果可以看出,雖然兩個對象的hashCode相等,但是實際上兩個對象並不是相等;

因為我們沒有重寫equals方法,那麼就會調用object默認的equals方法,是比較兩個對象的引用是不是相同,顯示這是兩個不同的對象,兩個對象的引用肯定是不同的。

這裡我們將生成的對象放到了hashSet中,而hashSet中只能夠存放唯一的對象,也就是相同的(適用於equals方法)的對象只會存放一個,但是這裡實際上是兩個對象a,b都被放到了HashSet中,這樣hashSet就失去了他本身的意義了。

我們現在把equals重現實現一下:我們讓ideal幫我們自動生成

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;
}

我們看下輸出結果:

true
true
[com.jikeh.Person@12]

現在我們可以看到,這兩個對象已經完全相等了,並且hashSet中也只存放了一份對象。


分享到:


相關文章: