一、背景
在開發或面試的過程中,經常都會遇到數值與數值的比較,對象與對象的比較。看似越基礎的東西,越容易踩坑,也是筆試/面試中常見的一類問題。所以,我總結了一下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用法淺析](http://p2.ttnews.xyz/loading.gif)
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用法淺析](http://p2.ttnews.xyz/loading.gif)
我們翻譯一下上面這段文字:
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中也只存放了一份對象。
閱讀更多 極客慧 的文章