Java拾遗:001 - 重写 equals 和 hashCode 方法
2019獨角獸企業(yè)重金招聘Python工程師標準>>>
重寫equals方法
在Java中Object類是一個具體類,但它設計的主要目的是為了擴展,所以它的所有非final方法,都被設計成可覆蓋(override)的。但任何一個子類在覆蓋這些方法時都應遵守一些通用約定,否則就會在使用中引起各種問題。
equals方法定義于Object類中,用于比較兩個對象是否相等,說起比較相等我們也常用==符號來比較,但兩者有什么區(qū)別呢?
equals方法與==的區(qū)別
一般來說==用于比較基本類型值是否相等,如:int、float等,或者用于比較對象引用地址是否相同(兩個引用指向同一對象),而equals方法則由程序員自己來實現(xiàn)(JDK源碼里的類是由JDK的開發(fā)者實現(xiàn)的,同樣也是程序員自己實現(xiàn)的)來比較兩個對象是否相等(強調(diào)一下,這里說的是相等而非相同)的。后者包含前者,即:使用==比較相同的對象equals方法一定返回true。
什么時候需要重寫equals方法?
通常我們需要在代碼中實現(xiàn)判斷一個對象是否等于另外一個對象,或者需要將對象加入集合時,會需要使用equals方法來提供判斷邏輯(集合中添加元素時會使用contains方法來判斷添加對象是否已存在于集合中,內(nèi)部調(diào)用的判斷方法即為equals方法)。
equals方法的等價關系
重寫equals方法看似很簡單,但很許多方式會導致錯誤,并且造成嚴重后果,所以Java規(guī)范對對重寫equals方法定義了一些約定(非強制,但應盡量遵守),即:equals方法需要實現(xiàn)等價關系(equivalence relation)。
- 自反性(reflexive),對于任何非null的引用值x,x.equals(x)必須返回true。
- 對稱性(symmetric),對于任何非null的引用值x和y,當且僅當y.equals(x)返回true時,x.equals(y)必須返回true。
- 傳遞性(transitive),對于任何非null的引用值x、y和z,如果x.equals(y)返回true且y.equals(z)也返回true,那么x.equals(z)也必須返回true。
- 對于任何非null的引用值x,x.equals(null)必須返回false。 你當然可以無視這些約定,但當你發(fā)現(xiàn)你的程序表現(xiàn)不正常或者未達到預期的時候,你可以會很難找到失敗的根源(出自《Effective Java》)。
重寫equals方法的最佳實踐
如果說上面的條目還不是很具體的話,下面通過一些示例來闡述上面的條目。 首先我們有一個Employee類和Manager類,包含幾個域對象(屬性)
public class Employee {private String name;private Double salary;private Date joinDate;// getter / setter ... }public class Manager extends Employee {private Double bonus;// getter / setter ... }如果我們不重寫equals方法
@Testpublic void equals_1() {// 有兩個Employee對象,我們假定如果姓名與薪資相等即認為兩個對象相等Employee x = new Employee();x.setName("Jane");x.setSalary(3500.0);Employee y = new Employee();y.setName("Jane");y.setSalary(3500.0);// 此時我們沒有重寫equals方法,此時使用的equals方法由Object提供,只簡單比較兩個對象是否相同assertTrue(x.equals(x));assertFalse(x.equals(y));}會看到對象x.equals(x)返回true而x.equals(y)返回false,而根據(jù)假定條件應返回true,所以Object里的equals方法顯然不夠用,我們需要自定義equals方法。 而在實現(xiàn)自定義equals方法時,第一條約定自反性,這一條很難無意識地違反這一條(如果違反了,你在向集合中添加元素時就會重復添加),但通常我們還是實現(xiàn)該約定,這通常是一種性能優(yōu)化的方式(如果兩個比較對象是同一個對象,就返回true,后面的比較邏輯就省略了)。
@Overridepublic boolean equals(Object obj) {// 這里使用==顯示判斷比較對象是否是同一對象if (this == obj) {return true;}// 對于任何非null的引用值x,x.equals(null)必須返回falseif (obj == null) {return false;}// TODO 核心域比較return false;}注意@Override注解,重寫方法時務必加上該注解,IDE會幫我們檢查是否是重寫父類方法,否則可能實現(xiàn)的是重載方法(改變了方法簽名),導致后面運行出錯而找不到問題的原因。
上面實現(xiàn)了自反性,下面繼續(xù)實現(xiàn)對稱性
@Overridepublic boolean equals(Object obj) {// 這里使用==顯示判斷比較對象是否是同一對象if (this == obj) {return true;}// 對于任何非null的引用值x,x.equals(null)必須返回falseif (obj == null) {return false;}// 通過 instanceof 判斷比較對象類型是否合法if (!(obj instanceof Employee)) {return false;}// 對象類型強制轉換,如果核心域比較相等,則返回true,否則返回false// 強制類型轉換前,必須使用instanceof判斷,避免代碼拋出ClassCastException異常Employee other = (Employee) obj;return (this.name == other.name || (this.name != null && this.name.equals(other.name)))&& (this.salary == other.salary || (this.salary != null && this.salary.equals(other.salary)));}測試代碼證明equals方法實現(xiàn)了對稱性
@Testpublic void equals_2() {Employee x = new Employee();x.setName("Jane");x.setSalary(3500.0);Manager y = new Manager();y.setName("Jane");y.setSalary(3500.0);assertTrue(x.equals(y));assertTrue(y.equals(x));}但在使用instanceof的時候需要注意,如果所有子類擁有統(tǒng)一的語義時使用instanceof 檢查,如果要求比較目標類必須與當前類為同一類,可以使用this.getClass() == obj.getClass()來比較。
使用JDK7提供的工具類優(yōu)化代碼
我們在寫equals方法時,經(jīng)常需要判斷屬性值是否為空,非空時才比較目標對象的相同屬性值是否相等,而在JDK8中提供了Objects的工具類,可以幫我們簡化這部分代碼
@Overridepublic boolean equals(Object obj) {// 這里使用==顯示判斷比較對象是否是同一對象if (this == obj) {return true;}// 對于任何非null的引用值x,x.equals(null)必須返回falseif (obj == null) {return false;}// 通過 instanceof 判斷比較對象類型是否合法if (!(obj instanceof Employee)) {return false;}// 對象類型強制轉換,如果核心域比較相等,則返回true,否則返回falseEmployee other = (Employee) obj;// 如果兩者相等,返回true(含兩者皆空的情形),否則比較兩者值是否相等return Objects.equals(this.name, other.name)&& Objects.equals(this.salary, other.salary);}另外該類還提供了深度比較的方法deepEquals,對于屬性為引用類型比較使用。
重寫hashCode方法
通常來說,覆寫equals方法時必須要覆寫hashCode方法,但這是為什么呢?
HashCode(散列碼)是什么?
首先來說一下HashCode是什么,HashCode中文翻譯為哈希碼或散列碼,由哈希算法,將對象映射為一個整型數(shù)值。在Java中一般用于HashMap、HashSet、HashTable集合類中。
為什么重寫equals方法同時需要重寫hashCode方法?
上面說到HashMap等哈希類型集合對類,由于HashMap的底層存儲結構為數(shù)組結構,每個元素又是一個鏈表,而數(shù)組的下標即為HashCode,所以相同HashCode的對象會被存放在同個鏈表中。所以如果重寫equals方法而不重寫hashCode方法時,就會導致將兩個相等的對象(equals判斷相等)加入HashMap時,因為返回不同的HashCode而分在了不同的哈希桶中,造成重復添加元素(同一個哈希桶會通過equals方法判斷是否重復)。
@Testpublic void hashCode_1() {Employee x = new Employee();x.setName("Jane");x.setSalary(3500.0);Employee y = new Employee();y.setName("Jane");y.setSalary(3500.0);// HashSet底層由HashMap實現(xiàn)HashSet<Employee> sets = new HashSet<>();sets.add(x);sets.add(y);assertEquals(2, sets.size());}上述測試代碼證明了這一點,預期添加兩個相等對象,集合中應只有一個元素才對。
怎樣編寫一個好的hashCode方法?
相等的對象必須具有相等的HashCode,但反過來卻不一定,因為存在哈希碰撞,通俗地說就是不同對象(也不相等),可能生成的HashCode是相同的,而發(fā)生哈希碰撞的幾率則是由哈希算法決定的。一般來說發(fā)生哈希碰撞幾率越大,性能就越差,所以一個好的hashCode方法因盡可能的減少哈希碰撞的幾率。
業(yè)界并沒有最佳的哈希碼生成算法(沒有最好,只有最合適),這里參考《Core Java》和《Effective Java》給出一個參考實現(xiàn)
@Overridepublic int hashCode() {int r = 17;r = 31 * r + this.name.hashCode();r = 31 * r + this.salary.hashCode();return r;}使用JDK7中提供的工具類優(yōu)化
同樣Objects類也提供了hashCode的工具方法,底層代碼使用了Arrays類的hashCode生成方法
@Override public int hashCode() {return Objects.hash(this.name, this.salary); }下面是Arrays類的hashCode方法代碼
public static int hashCode(Object a[]) {if (a == null)return 0;int result = 1;for (Object element : a)result = 31 * result + (element == null ? 0 : element.hashCode());return result;}String類中的hashCode方法
public int hashCode() {int h = hash;if (h == 0 && value.length > 0) {char val[] = value;for (int i = 0; i < value.length; i++) {h = 31 * h + val[i];}hash = h;}return h;}JDK中在編寫hashCode方法時,大量使用了31這個魔法數(shù)字,據(jù)《Effective Java》描述該數(shù)字有一個很好的特性:用移位和減法代替乘法,可以得到更好的性能31 * i == (i << 5) - i。
散列碼的性能優(yōu)化
通常不建議會被修改的屬性參與HashCode計算(實際難以避免),因為這會引起HashCode的變化,對于已加入HashMap的對象,不會重新分配存儲位置,而導致一些問題。
對于一些比較復雜的對象,其HashCode的計算是一件非常消耗資源的事,一個簡單的辦法就是對其HashCode進行緩存,比如在類中添加一個屬性,記錄該HashCode,HashCode可以在類初始化時生成,也可以在第一次調(diào)用hashCode方法時生成,這要視具體應用而定。但前提條件是參與計算HashCode的屬性值不能修改。
結語
有很多約定不是強制的,但實際開發(fā)過程中卻應盡量遵循,這些“最佳實踐”會減少很多代碼中潛在的Bug,或者提升代碼性能。
參考資料
- 《Core Java》
- 《Effective Java》
- 《編寫高質(zhì)量代碼:改善Java程序的151個建議》
轉載于:https://my.oschina.net/zhanglikun/blog/1921429
總結
以上是生活随笔為你收集整理的Java拾遗:001 - 重写 equals 和 hashCode 方法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: npm出错的解决方案
- 下一篇: Linux下查看进程IO工具iopp