第3章 對于所有對象都通用的方法

第3章 對于所有對象都通用的方法

Object的設定是為了擴展,它的所有非final方法(equals hashCode toString clone finalize)都有明確的通用約定,因為它們被設計是要被覆蓋(override)的
而在覆蓋這些方法時,都有責任遵守這些通用的約定,否則,其他依賴這些約定的類(如HashMap&HashSet)就無法結合該類一起正常運作.

第8條 覆蓋equals時請遵守通用約定

不覆蓋equals

不覆蓋equals的情況下,類的每個實例都與它自身相等,如果滿足以下任何一個條件,就是所期望的結果:

  • 類的每個實例本質上都是唯一的
  • 不關心類是否提供了"邏輯相等"的測試功能
  • 超類已經(jīng)覆蓋了equals,從超類繼承過來的行為對于子類也是合適的(要小心)
  • 類是私有的或是包級私有的,可以確定它的equals方法永遠不會被調用 (不懂為什么)

講得怪怪的

PS: 邏輯相等,就是邏輯上是相等的,比如id一樣,判定它們相等,即使它們是兩個不同的對象

什么時候應該覆蓋equals

當類需要邏輯相等這個概念的時候就應該覆蓋equals
比如要判斷兩個student是否是同一個人,這個時候我們就需要按需重寫equals

通用約定

重寫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
  • 一致性(consistent) 對于任何非null的引用值x和y,只要equals的比較操作在對象中所用的信息沒有被修改,多次調用x.equals(y)就會一致地返回true,或者false
  • 對于任何非null的引用值,x,x.equals(null)必須返回false

感覺又回到了學數(shù)學交換律什么的的時候了~

有些類(如集合,HashMap)與equals方法息息相關,所以重寫的時候要仔細小心

高質量的equals

ej對equals提了幾點建議:

  1. 使用==操作符檢查"參數(shù)是否為這個對象的引用" 如果是,則返回true. 這只不過是一種性能優(yōu)化,如果比較操作有可能很昂貴,就值得這么做 (平時沒有用過,怎么樣的比較操作算是昂貴的呢?)
  2. 使用instanceof操作符檢查"參數(shù)是否為正確的類型" 如果不是,則返回false。
  3. 把參數(shù)裝換成正確的類型。(這個比較好理解,instanceof檢測后,一般都會強轉成所需類型)
  4. 對于該類中的每個『關鍵』域,檢查參數(shù)中的域是否與對象中對應的域相配。(比如學生類有學號,班級,姓名這些重要的屬性,我們都需要去比對)
  5. 當你編寫完成了equals方法之后,應該問自己是哪個問題:它是否是對稱的、傳遞的、一致的?

另外EJ還告誡我們覆蓋equals的時候總要覆蓋hashCode(見第9條)

小結

最后按照上訴建議,用一個Student類來總結一下equals的寫法:

public class Student {
    public String name;
    public String className;
    @Override
    public boolean equals(Object obj) {
        //對于一個null的對象 我們總是返回false
        if (null == obj) {
            return false;
        }
        // 利用instanceof檢查類型后,強轉
        if (obj instanceof Student){
            Student other = (Student) obj;
            //再對關鍵的屬性做比較 得出結論
            if (name.equals(other.name) && className.equals(other.className)) {
                return true;
            }
        }
        return false;
    }
}

equals是一個看上去簡單,實則是個比較容易犯錯的方法,需要小心仔細

第9條 覆蓋equals時總要覆蓋hashCode

覆蓋了equals方法,也必須覆蓋hashCode方法,if not,就違反了hashCode的通用約定,會導致無法跟基于散列的集合正常運作.

Object通用約定(在Object類中的注釋即是):

  • 在應用程序的執(zhí)行期間,只要對象的equals方法的比較操作所用到的信息沒有被修改,那么對這同一個對象調用多次,hashCode方法都必須始終如一地返回同一個整數(shù).在同一個應用程序的多次執(zhí)行過程中,每次執(zhí)行所返回的整數(shù)可以不一致.
  • 如果兩個對象根據(jù)equals方法比較是相等的,那么調用這兩個對象中任意一個對象的hashCode方法都必須產(chǎn)生同樣的整數(shù)結果.(即equals相等,那么hashCode一定相等,需要注意的是,反過來不一定成立,即hashCode相等不代表equals相等)
  • 如果兩個對象根據(jù)equals方法比較是不相等的,那么調用這兩個對象中任意一個對象的hashCode方法,則不一定要產(chǎn)生不同的整數(shù)結果.但是程序員應該知道,給不相等的對象產(chǎn)生截然不同的證書結果,有可能提高散列表(hash table)的性能.

不重寫hashCode帶來的問題

正如之前提到的,hashCode其實主要用于跟基于散列的集合合作
如HashMap會把相同的hashCode的對象放在同一個散列桶(hash bucket)中,那么即使equals相同而hashCode不相等,那么跟HashMap一起使用,則會得到與預期不相同的結果.

具體是怎么樣的不同的效果?來看一段代碼:
PS:Student類是第8條里的類,重寫了equals

public static void main(String[]args) {
    Student lilei = new Student("lilei","class1");
    HashMap<Student, String> hashMap = new HashMap<>();
    hashMap.put(lilei, lilei.className);
    String className = hashMap.get(new Student("lilei","class1"));//值與之前的lilei相同,即equals會為true

    System.out.println(className);
}

className的值為多少呢?
class1?
NO!是null!?。?!(誒?)

為什么呢?因為我們并沒有重寫hashcode,所以即使我們去get的時候傳入的Student的name以及classname與put的時候的對象值是一樣的,也即它們是equals(我重寫了equals!),但是要注意,它們的hashcode是不一樣的,這樣就違反了上面所說的equals相等,hashCode也要相等的原則,所以當我們期望get到的是class1的時候,我們需要重寫hashCode方法,讓它們的hashcode相同!

那么問題來了,如何去重寫hashCode呢?返回一個固定值?比如1?NO!!!
So,how?

如何重寫hashCode

EJ給出的解決辦法:

  1. 把某個非零的常數(shù)值,比如17,保存在一個名為result的int類型的變量中。
  2. 對于對象中每個關鍵域f(指equals方法中涉及的每個域),完成以下步驟:
  • 步驟(a) 為該域計算int類型的散列碼c:
    • 如果f是boolean,則計算 f?1:0
    • 如果是byte,char,short或int,則計算 (int)f
    • 如果是long,則計算(int)(f^(f>>>32))
    • 如果是float,則Float.floatToIntBits(s)
    • 如果是double,則計算Double.doubleToLongBits(f),再按long類型計算一遍
    • 如果是f是個對象引用,并且該類的equals方法通過遞歸地調用equals的方式來比較這個域,則同樣為這個域遞歸調用hashCode。如果需要更復雜的比較,則為這個域計算一個‘范式’,然后針對這個范式調用hashCode。如果這個域的值為null,則返回0(或者其他某個常數(shù),但通常是0)。
    • 如果是個數(shù)組,則需要把每個元素當做單獨的域來處理。也就是說,遞歸地應用上述規(guī)則,對每個重要的元素計算一個散列碼,然后根據(jù)步驟b中的做法把這些散列值組合起來。 如果數(shù)組域中的每個元素都很重要,可以利用發(fā)行版本1.5中增加的其中一個Arrays.hashCode方法。
    • 步驟(b) 按照下面公式,把(a)步驟中計算得到的散列碼c合并到result中:result = 31*result+c (為什么是31呢?)
  1. 返回result
  2. 測試,是否符合『相等的實例是否都具有相等的散列碼』

OK,知道怎么寫之后,我們重寫Student類的hashCode方法:

@Override
public int hashCode() {
    int result = 17;//非0 任選
    result = 31*result + name.hashCode();
    result = 31*result + className.hashCode();
    return result;
}

這下之前的代碼輸出的結果為class1了!?。

為什么要選31?

因為它是個奇素數(shù),另外它還有個很好的特性,即用移位和減法來代替乘法,可以得到更好的性能:31*i == (i<<5)-i

小結

終于學會如何寫hashCode了!
老實說,我并沒有做到這條要求!
因為一般來說我不會把Student這樣的類當做一個Key去處理

PS:書中講到的知識點很多,光看這個筆記是不夠的,如果可以,自己去閱讀書籍吧!

其他資料

dim提供:淺談Java中的hashcode方法

第10條 始終要覆蓋toString

Object類默認toString的實現(xiàn)方法是這樣的:

    public String toString() {
        return getClass().getName() + '@' + Integer.toHexString(hashCode());
    }

它只有類名+'@'+散列值,toString的通用約定指出,被返回的字符串應該是一個『簡潔的,但信息豐富,并且易于閱讀的表達形式』
雖然夠簡單,但是信息并不豐富,而且更多時候我們更希望toString返回對象中包含的所有值得關注的信息,當屬性多了,只顯示信息重要的即可

toString倒沒有特別大的約束

第11條 謹慎地覆蓋clone

clone說到clone(protected)就必須提及一下Cloneable接口,這個接口很奇怪,沒有方法:

public interface Cloneable {
}

Object的clone方法當我們嘗試調用一個沒有實現(xiàn)Cloneable接口的類的clone方法數(shù)時,clone會拋出CloneNotSupportedException,是不是很坑爹?

    protected Object clone() throws CloneNotSupportedException {
        if (!(this instanceof Cloneable)) {
            throw new CloneNotSupportedException("Class " + getClass().getName() +
                                                 " doesn't implement Cloneable");
        }
        return internalClone();
    }

為什么不把clone方法放Cloneable接口里面去卻偏偏塞給了Object?
這個設計我真的想不明白?。。。?!

clone方法自己沒怎么用過,不過可以看看其他優(yōu)秀的庫的設計,比如Retrofit中的OkHttpCall:

  @Override public OkHttpCall<T> clone() {
    return new OkHttpCall<>(serviceMethod, args);
  }

PS:在使用優(yōu)秀的開源庫的時候,如果可以,多看看它的源碼,你會學到很多!相信我!

第12條 考慮實現(xiàn)Comparable接口

注意compareTo不是Object的方法,而是Comparable接口的方法:

public interface Comparable<T>{
    int compareTo(T t);
}

compareTo的約定跟equals類似:

PS:符合sgn(表達式)表示數(shù)學中的signum函數(shù),它根據(jù)表達式(expression)的值為負值、零、和正直,分別返回-1、0或1

  • 確保sgn(x.compareTo(y))== -sgn(y.compareTo(x))
  • 可傳遞:x.compareTo(y)> 0 && y.compareTo(z) 暗示 x.compareTo(z)> 0
  • 確保x.compareTo(y)==0暗示所有z都滿足sgn(x.compareTo(z))== sgn(y.compareTo(z))
  • 強烈建議(x.compareTo(y)==0),但這并非絕對重要
    (個人覺得還是遵守更好一些?。?/li>

如果不想寫compareTo或者類并沒有實現(xiàn)Comparable接口的可以自定義一個Comparator類來進行比較。

需要注意,排序是不允許出現(xiàn)邏輯漏洞的,否則會crash!

本章完結

題外話:Object一共有12個方法,其中7個是native方法

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容