一.基礎知識:
-
如果一個變量指向的數據是對象類型,這時候涉及兩塊內存,對象本身佔用一塊內存(堆內存),變量也佔用一塊內存。例如:Object o = new Object(); 變量o佔一個內存,new Object()是佔另一個內存。通俗來講:new Object()創造了一個對象,類型是Object,而Object o則創造了一個可以存儲Object類型對象引用的“引用存儲器”,而等號=則把new Object()這個對象的引用存到了o之中,這樣就可以用a來使用這個對象數據和方法了。
-
舉一個案例(圖示):
//構造方法創建 ---new
String s1 = new String("aaa");//這裏把"aaa"代表的對象的引用存到了s1中
s1= new String("bbb");//這裏又把另一個對象"bbb"存到了s1中,所以s1就不是指向原來的"aaa"了,而是指向"bbb"了,新的空間, aaa等着回收
二.equals和==的區別
- 測試的代碼有兩個類,Test類和User類,可以自己粘貼測試一下
- Test類:
import java.util.HashSet;
public class Test {
public static void main(String[] args) {
test_eq();
// test_hashcode();
}
public static void test_eq() {
int a = 1;
int b = 1;
System.out.println(a == b); //true 比較的是值
User u1 = new User("tom",12,1001);
User u2 = new User("tom",12,1001);
System.out.println(u1+","+u2); //User@1b6d3586,User@4554617c
//這裏==比較的是變量(棧)內存中存放的對象的(堆)內存地址,用來判斷兩個對象的地址是否相同,即是否是指相同一個對象
System.out.println(u1 == u2); //false 比較的是地址,不是值
System.out.println(u1.equals(u2));
//String類中對equals方法進行了重寫,字符串與字符串比較的是值
String s1 = "jack"; //字符常量,放在常量池
String s2 = "jack"; //創建流程:先判斷常量池是否存在jack,如果有,就直接指向,如果沒有則再創建一個對象
System.out.println(s1.equals(s2)); //true 字符串 實際調用的是String類中重寫的方法,比較的是值
}
private static void test_hashcode() {
User u1 = new User("tom",12,1001);
User u2 = new User("tom",12,1001);
HashSet<Object> set = new HashSet<>();
set.add(u1);
set.add(u2);
System.out.println(u1.hashCode()); //110571150
System.out.println(u2.hashCode()); //110571150
System.out.println(u1.hashCode()==u2.hashCode()); //true
System.out.println(Integer.toHexString(u1.hashCode())); //6972e8e
System.out.println(u1); // User@6972e8e 包名+類名 + @ + 16進制的hashCode值
System.out.println(u1.equals(u2));
}
}
- User類:
import java.util.Objects;
public class User {
private String name;
private int age;
private int id;
public User(String name, int age, int id) {
this.name = name;
this.age = age;
this.id = id;
}
public User() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
//按快捷鍵自動生成equals和hashcode方法
// @Override
// public boolean equals(Object o) {
// if (this == o) return true;
// if (o == null || getClass() != o.getClass()) return false;
// User user = (User) o;
// return age == user.age &&
// id == user.id &&
// Objects.equals(name, user.name);
// }
//
// @Override
// public int hashCode() {
// return Objects.hash(name, age, id);
// }
//這個equals()方法主要測試equals和==的
@Override
public boolean equals(Object obj) {
//instanceof運算符是用來在運行時指出對象是否是特定類的一個實例,instanceof通過返回一個布爾值指出true或者false
//在這裏是Test類創建的User類的u1和u2是否是User的實例,所以要判斷,通俗來講:你不能在大街上隨便拉一個人做手術,必須要有醫師執照
if(obj instanceof User) {
//屬於引用類型的強制轉換,將父類類型轉爲子類,向下轉後,可以訪問子類特有的屬性和方法
User u = (User)obj;
//這裏equals和==是一樣的,都是比較的是值
if(this.name.equals(u.getName()) && this.age == u.getAge() && this.id==u.getId()) {
return true;
}
}
return false;
}
}
- ==判斷的是是否引用同一個對象(判斷基本類型比較的是值,判斷引用類型比較的是地址)
int a = 1;
int b = 1;
System.out.println(a == b); //true 比較的是值
- 基本類型有8個(byte,short,int,long,float,double,boolean,char),基本形式:數據類型 變量名 = 值,輸出時顯示的是具體的值
User u1 = new User("tom",12,1001);
User u2 = new User("tom",12,1001);
System.out.println(u1+","+u2); //User@1b6d3586,User@4554617c
System.out.println(u1 == u2); //false 比較的是地址,不是值
- 這裏==比較的是變量(棧)內存中存放的對象的(堆)內存地址,用來判斷兩個對象的地址是否相同,即是否是指相同一個對象
- 引用類型:除了基本類型以外都是引用類型,如String 數組 類 接口 枚舉 基本形式:數據類型 名稱 = new 數據類型(),輸出的顯示的是內存地址
User u1 = new User("tom",12,1001);
User u2 = new User("tom",12,1001);
System.out.println(u1+","+u2); //User@1b6d3586,User@4554617c
System.out.println(u1 == u2); //false 比較的是地址,不是值
System.out.println(u1.equals(u2));//false
- 上述代碼增加了一行equals的比較
1. 沒有在User類重寫equals()方法
- 如果equals沒有被重寫,默認和“==”一樣。也就是User類中沒有重寫equals方法,只寫了用戶類的屬性,有參和無參構造,還有get和set方法,其餘沒有。
- 這時候運行出來的結果是false,在User類中沒有重寫equals方法之前,比較的是地址。
2.在User類重寫equals()方法
- User類:
// @Override
// public boolean equals(Object o) {
// if (this == o) return true;
// if (o == null || getClass() != o.getClass()) return false;
// User user = (User) o;
// return age == user.age &&
// id == user.id &&
// Objects.equals(name, user.name);
// }
//
// @Override
// public int hashCode() {
// return Objects.hash(name, age, id);
// }
@Override
public boolean equals(Object obj) {
//instanceof運算符是用來在運行時指出對象是否是特定類的一個實例,instanceof通過返回一個布爾值指出true或者false
//在這裏是Test類創建的User類的u1和u2是否是User的實例,所以要判斷,通俗來講:你不能在大街上隨便拉一個人做手術,必須要有醫師執照
if(obj instanceof User) {
//屬於引用類型的強制轉換,將父類類型轉爲子類,向下轉後,可以訪問子類特有的屬性和方法
User u = (User)obj;
//這裏equals和==是一樣的,都是比較的是值
if(this.name.equals(u.getName()) && this.age == u.getAge() && this.id==u.getId()) {
return true;
}
}
return false;
}
- Test類:
User u1 = new User("tom",12,1001);
User u2 = new User("tom",12,1001);
System.out.println(u1+","+u2); //User@1b6d3586,User@4554617c
System.out.println(u1 == u2); //false 比較的是地址,不是值
System.out.println(u1.equals(u2));//true
- 上述代碼是User類中重寫equals方法後(在重寫equals方法時,系統會自動將equals和hashCode兩個方法都重寫了),這時如果想用系統自動生成的equals方法進行測試,可以先將hashCode方法給註釋掉。這裏我用的是自己寫的equals方法進行的測試,再運行程序時,equals比較出來的是true。
提醒:equals方法的重寫,用系統的或者用自己寫的equals方法,性質都是一樣的,只能使用其中一個equals()方法。
- 源碼equals()方法:
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;
}
- 因爲這時候是按照重寫後的規則進行比較,而源碼equals方法返回的是“==”的值,比較的是值或者內容是否相等。如果將u1中的name值改變,equals返回的就是false,因爲u1和u2實例內容不一樣呢。
String s1 = "jack"; //字符常量,放在常量池
String s2 = "jack"; //創建流程:先判斷常量池是否存在jack,如果有,就直接指向,如果沒有則再創建一個對象
System.out.println(s1.equals(s2)); //true 字符串 實際調用的是String類中重寫的方法,比較的是值
- 看一個知識:因爲String類經常會被用到,所以String類中對equals方法就已經進行了重寫,字符串與字符串比較的是值
- String常量:使用雙引號直接創建的字符串,稱爲字符常量,字符常量放在內存中的常量池,在我的下一篇博客中已經寫了關於常量池的介紹。
- 代碼常量池示意圖
總結:
- == 判斷的是是否引用同一個對象( 判斷基本類型比較的是值,判斷引用類型比較的是地址 )
- 如果equals 沒有被重寫,默認和 == 一樣
String類中對equals 方法進行了重寫,字符串與字符串比較的是值- 如果重寫了equals 方法,則按照重寫後的規則進行比較
- 自定義類可以重寫equals方法,實現對特定屬性的比較
三.equals和hashcode的使用:
- 爲什麼要使用hashCode():
java.lang.Object 類 ,所有類的根,所有類都直接或者間接的繼承了Object類。其中hashcode()和equals()方法,經常會用到。
集合Set中的元素是無序且不可重複,那判斷兩個元素是否重複的依據是什麼呢?
有小夥伴說:比較對象是否相等當然用Object類中的equals()方法啊。但是,如果Set集合中存在大量對象,後添加到集合中Set的對象元素比較次數會逐漸增多,大大的降低了程序運行效率。Java中採用哈希算法(也叫散列算法)來解決這個問題,將對象(或數據)依特定算法直接映射到一個地址上,對象的存取效率大大提高。
User u1 = new User("tom",12,1001);
User u2 = new User("tom",12,1001);
HashSet<Object> set = new HashSet<>();
set.add(u1);
set.add(u2);
System.out.println(u1.hashCode()); //110571150
System.out.println(u2.hashCode()); //110571150
System.out.println(u1.hashCode()==u2.hashCode()); //true
System.out.println(Integer.toHexString(u1.hashCode())); //6972e8e
System.out.println(u1); // User@6972e8e 包名+類名 + @ + 16進制的hashCode值
System.out.println(u1.equals(u2));
1.equals()方法註釋掉,使用hashCode()方法
// @Override
// public boolean equals(Object o) {
// if (this == o) return true;
// if (o == null || getClass() != o.getClass()) return false;
// User user = (User) o;
// return age == user.age &&
// id == user.id &&
// Objects.equals(name, user.name);
// }
//
@Override
public int hashCode() {
return Objects.hash(name, age, id);
}
- 以上這個示例,我們只是重寫了hashCode方法,u1和u2通過equals()方法比較返回的是false
- 從上面的結果可以看出:雖然兩個對象的hashCode相等,但是實際上兩個對象並不是相等;,我們沒有重寫equals方法,那麼就會調用object默認的equals方法,是比較兩個對象的引用是不是相同,顯示這是兩個不同的對象(堆內存),兩個對象的引用肯定是不定的。這裏我們將生成的對象放到了HashSet中,而HashSet中只能夠存放唯一的對象,也就是相同的(適用於equals方法)的對象只會存放一個,但是這裏實際上是兩個對象a,b都被放到了HashSet中,這樣HashSet就失去了他本身的意義了。
2.User類同時使用equals()方法和hashCode()方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age &&
id == user.id &&
Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age, id);
}
-
以上這個示例,我們同時重寫了equals和hashCode方法,這時u1和u2通過equals()方法比較返回的是true
-
從上面的結果可以看出:把User類中的equals方法寫上,比較的就是兩個對象的內容是否相等(也就是比較的是值,不是地址)
-
那集合(Set)如何判重呢?
- 添加元素時,調用該對象的hashCode方法,獲取一個哈希值
- 根據哈希值,使用哈希算法(有好幾種算法,一般用除留取餘法)通過餘數確定一個哈希表中的位置
- 如果該位置沒有元素,說明此對象是第一次存儲到集合Set中,則直接將此對象存儲在此位置中
- 如果該位置有元素了,則繼續調用兩個對象的equals()方法進行比較
- true:則認爲兩個對象爲同一個對象,無法添加,捨棄當前對象
- false:則認爲兩個對象不是同一個對象,以鏈表結構向當前位置進行追加,這屬於哈希表中解決衝突的方法稱爲:鏈地址法。還有一個方法叫線性探查法,這屬於數據結構中哈希表中的知識。
-
通俗來講:有兩個人名字一樣(hashCode()值一樣),你不能說他們是同一個人。名字相同,還要通過equals()判斷兩個人是否相等。
-
下面是我學習數據結構中看視頻截的圖,哈希表解決衝突的線性探查法和鏈地址法講解圖示:
-
這裏首先明白一個問題:
如果兩個對啊ing的equals方法比較爲true時,則兩個對象的hashCode值也一定相同。先判斷hashCode(),然後再判斷equals()。如果兩個對象的equals方法比較爲false,卻不能證明兩個對象的hashCode值可以不相同,也可以相同。
總結:
- 判斷重複的依據,兩個對象hashCode值一致,並且equals也返回true— 同一個對象–集合中只添加一個進入
- 在set結合中防止相同對象被添加,需要hashCode 和equals都重寫
如果對你有幫助,點個贊吧0.0
若有不正之處,請多多諒解並歡迎批評指正,不甚感激
- 參考資料
Java中hashCode的作用