記憶體泄漏避雷!你真的了解重寫equals()和hashcode()方法的原因嗎?

基本概念

  • 要比較兩個對象是否相等時需要調用對象的equals() 方法:
    • 判斷對象引用所指向的對象地址是否相等
  • 對象地址相等時, 那麼對象相關的數據也相等,包括:
    • 對象句柄
    • 對象頭
    • 對象實例數據
    • 對象類型數據
  • 可以通過比較對象的地址來判斷對象是否相等

Object源碼

  • 對象在不重寫的情況下使用的是Object中的equals() 方法和hashCode() 方法
    • equals(): 判斷的是兩個對象的引用是否指向同一個對象
    • hashCode(): 根據對象地址生成一個整數數值
  • ObjecthashCode() 方法修飾符為native: 表明該方法是由作業系統實現. Java調用作業系統底層程式碼獲取Hash
public native int hashCode();

重寫equals

  • 重寫equals()方法的場景:
    • 假設現在有很多學生對象
    • 默認情況下,要判斷多個學生對象是否相等,需要根據地址判斷:
      • 若對象地址相等,那麼對象實例的數據一定是一樣的
    • 判斷相等的要求:
      • 當學生的姓名,年齡,性別相等時,認為對象是相等的,
      • 不一定需要對象的地址完全相同
  • 根據需求重寫equals()方法:
public class Student {
	/** 姓名 */
	private String name;
	/** 性別 */
	private String sex;
	/** 年齡 */
	private String age;
	/** 體重 */
	private float weight;
	/** 地址 */
	private String addr;

	/*
	 * 重寫equals()方法
	 */
	@Override
	public boolean equals(Object obj) {
		// instanceof已經處理了obj == null的情況
	  	if (! (Object instanceof Student)) {
	  		
	  		return false;
	  	}
	  	Student stuObj = (Student) obj;
	  	// 地址相等
	  	if (this == stuObj) {
	  		return true;
	  	}
	  	// 如果對象的姓名,年齡,性別相等.則兩個對象相等
	  	if (stuObj.name.equals(this.name) && stuObj.sex.equals(this.sex) && stuObj.age.equals(this.age)) {
	  		return true;
	  	} else {
	  		return false;
	  	}
	 }

	 public String getName() {
	  	return name;
	 }
	 public void setName(String name) {
	  	this.name = name;
	 }
	 public String getSex() {
	  	return sex;
	 }
	 public void setSex(String sex) {
	  	this.sex = sex;
	 }
	  public String getAge() {
	  	return age;
	 }
	 public void setAge(String age) {
	  	this.age = age;
	 }
	 public String getWeight() {
	  	return weight;
	 }
	 public void setName(String weight) {
	  	this.weight = weight;
	 }
	 public String getAddr() {
	  	return addr;
	 }
	 public void setAddr(String addr) {
	  	this.addr = addr;
	 }
}
  • 示例:
public static void main(String[] args) {
	Student s1 = new Student();
	s1.setAddr("earth");
	s1.setAge("20");
	s1.setName("Tom");
	s1.setSex("Male");
	s1.setWeight(60f);

	Student s2 = new Student();
	s2.setAddr("Mars");
	s2.setAge("20");
	s2.setName("Tom");
	s2.setSex("Male");
	s2.setWeight(70f);

	if (s1.equals(s2)) {
		System.out.println("s1 == s2");
	} else {
		System.out.println("s1 != s2");
	}
}
  • 重寫了equals() 方法後,這裡會輸出 [s1==s2]
  • 如果沒有重寫 equals() 方法,那麼必定會輸出 [s1!=s2]

重寫hashCode

  • 根據重寫equals的方法,上述s1和s2認為是相等的
  • Object中的hashCode()方法:
    • equals() 方法沒被修改的前提下,多次調用同一個對象的hashCode() 方法返回的值必須是相同的正數
    • 如果兩個對象互相equals(), 那麼這兩個對象的hashcode值必須相等
    • 為不同的對象生成不同的hashcode可以提升Hash表的性能
  • 重寫hashCode()方法:
```java
public class Student {
	/** 姓名 */
	private String name;
	/** 性別 */
	private String sex;
	/** 年齡 */
	private String age;
	/** 體重 */
	private float weight;
	/** 地址 */
	private String addr;

	/*
	 * 重寫hashCode()方法
	 */
	@Override
	public int hashCode() {
	 	int result = name.hashCode();
	 	result = 17 * result + sex.hashCode();
	 	result = 17 * result + age.hashCode();
	 	return result;
	 }
	 
	 /*
	  * 重寫equals()方法
	  */
	 @Override
	 public boolean equals(Object obj) {
	  	// instanceof已經處理了obj == null的情況
	  	if (! (Object instanceof Student)) {
	  		
	  		return false;
	  	}
	  	Student stuObj = (Student) obj;
	  	// 地址相等
	  	if (this == stuObj) {
	  		return true;
	  	}
	  	// 如果對象的姓名,年齡,性別相等.則兩個對象相等
	  	if (stuObj.name.equals(this.name) && stuObj.sex.equals(this.sex) && stuObj.age.equals(this.age)) {
	  		return true;
	  	} else {
	  		return false;
	  	}
	 }

	 public String getName() {
	  	return name;
	 }
	 public void setName(String name) {
	  	this.name = name;
	 }
	 public String getSex() {
	  	return sex;
	 }
	 public void setSex(String sex) {
	  	this.sex = sex;
	 }
	 public String getAge() {
	  	return age;
	 }
	 public void setAge(String age) {
	  	this.age = age;
	 }
	 public String getWeight() {
	  	return weight;
	 }
	 public void setName(String weight) {
	  	this.weight = weight;
	 }
	 public String getAddr() {
	  	return addr;
	 }
	 public void setAddr(String addr) {
	  	this.addr = addr;
	 }
}
  • 在兩個對象相等的情況下,分別放入Map和Set中:
public static void main(String[] args) {
	Student s1 = new Student();
	s1.setAddr("earth");
	s1.setAge("20");
	s1.setName("Tom");
	s1.setSex("Male");
	s1.setWeight(60f);

	Student s2 = new Student();
	s2.setAddr("Mars");
	s2.setAge("20");
	s2.setName("Tom");
	s2.setSex("Male");
	s2.setWeight(70f);

	if (s1.equals(s2)) {
		System.out.println("s1 == s2");
	} else {
		System.out.println("s1 != s2");
	}
	
	Set set = new HashSet();
	set.add(s1);
	set.add(s2);
	System.out.println(Set);
}
  • 如果沒有重寫ObjecthashCode() 方法,會出現:
[com.oxford.Student@7852e922, com.oxford.Student@4e25154f]
  • 這是不符合預期的,因為Set容器有去重的特性.相等的元素不會重複顯示.這就涉及到Set的底層實現了
  • HashSet底層實現:
    • HashSet底層是通過HashMap實現的
    • 比較Set容器內元素是否相等是通過比較對象的hashcode來判斷是否相等的
  • hashCode()的寫法:
    • 首先整理出判斷對象相等的屬性
    • 然後去一個儘可能小的正整數,防止最終結果超出整型int的取數範圍
    • 然後計算[正整數 * 屬性的hashCode + 其餘某個屬性的hashCode]
    • 重複步驟
/*
 * 重寫hashCode()方法
 */
@Override
public int hashCode() {
	int result = name.hashCode();
	result = 17 * result + sex.hashCode();
	result = 17 * result + age.hashCode();
	return result;
}

原理分析

  • 因為沒有重寫父類的ObjecthashCode() 方法,所以ObjecthashCode() 方法會根據兩個對象的地址生成響應的hashcode
  • 由於兩個對象分別是實體類創建的不同的實例,所以地址肯定是不一樣的,那麼hashcode值也是不一樣的
  • Set區別對象是不是唯一的標準:
    • 兩個對象的hashcode值是否一樣
    • 然後再判定兩個對象是否equals
  • Map區別對象是不是唯一的標準:
    • 先根據Key值的hashcode分配來獲取保存數組下標
    • 然後再根據eaquals區分是否是唯一值

HashMap

HashMap組成結構

  • HashMap: 是由數組鏈表組成的

HashMap的存儲

  • HashMap的存儲:
    • 一個對象存儲到HashMap中的位置是由keyhashcode值決定的
    • HashMap查找key:
      • 查找key,hashMap會先根據key值的hashcode經過取余演算法定位所在數組的位置
      • 然後根據keyequals方法匹配相同的key值獲取相應的對象
  • 存值規則:
    • KeyhashcodeHashMap的容量,進行取余運算得出該Key存儲在數組所在位置的下標
  • HashMap查找key:
    • 得到key在數組中的位置
    • 匹配得到對應key值對象
    • 然後將上述多個對象根據key.equals() 來匹配獲取對應的key的數據對象
  • HashMap中的hashCode:
    • 如果沒有hashcode就意味著HashMap存儲的時候是沒有規律可循的
    • 這樣每次使用map.get() 方法,就要將map里的對象一一進行equals匹配,導致效率低下