Effective Java 案例分享(三)

11、重寫Object.equals時(shí),必須重寫Object.hashcode

如果需要重寫Object的equals方法,那么一定要重寫hashCode方法, 否則會(huì)在哈希表相關(guān)的數(shù)據(jù)結(jié)構(gòu)中出現(xiàn)非常嚴(yán)重的問(wèn)題。重寫hashcode方法也要循環(huán)以下幾個(gè)原則:

  • 在Object的equals的結(jié)果未發(fā)生改變的情況下,多次調(diào)用hashcode方法必須返回一樣的結(jié)果,如果equals發(fā)生了變化,hashCode也要變化;
  • 如果兩個(gè)對(duì)象equals相等,他們的hashCode必須也相等;
  • 如果兩個(gè)對(duì)象equals不相等,不要求一定要返回不用的hashCode,但是不同的hashCode在哈希表中表現(xiàn)可能更好;

推薦重寫hashCode方法的步驟:

  1. 聲明一個(gè)int變量,名稱是result;
  2. 通過(guò)每一個(gè)有效的屬性的hashCode,得到變量c。如果該屬性是null,hashCode直接等于0,如果是數(shù)組,可以使用Arrays.hashCode計(jì)算;
  3. 計(jì)算結(jié)果:result = 31 * result + c;
  4. 返回result;

計(jì)算hashCode的經(jīng)典案例:

@Override 
public int hashCode() {
    int result = Short.hashCode(areaCode);
    result = 31 * result + Short.hashCode(prefix);
    result = 31 * result + Short.hashCode(lineNum);
    return result;
}

為什么要使用31計(jì)算hashCode?

選擇31作為一個(gè)主要的元素,是因?yàn)樗瞧鏀?shù)。如果它是偶數(shù),多次相乘之后信息會(huì)被丟失,因?yàn)槊恳淮纬艘?都是會(huì)左移。如果使用31,它的乘法可以被替換成位移+減法:31 * i = (result << 5)- i;這樣hashCode的后幾位都可能是非0的,哈希值散步會(huì)更均勻;

12、總是重寫toString方法

Object的toString方法的默認(rèn)實(shí)現(xiàn)是:Class名稱+@+16位hashCode。默認(rèn)實(shí)現(xiàn)的toString方法的可讀性并不好,所以我們需要重寫toString方法來(lái)提供可讀性,調(diào)試時(shí)可以得到更多的可用信息。Object與字符串拼接時(shí),自動(dòng)使用Obejct的toString方法。

重寫toString的要注意的問(wèn)題:

  • toString應(yīng)該包含所有的有效信息;
  • toString不僅要輸出有效屬性的值,也要添加必要的描述,便于理解值的意義;
  • 無(wú)論是否要使用特定格式輸出String信息,都應(yīng)該對(duì)你關(guān)心的問(wèn)題有清晰的描述;
  • 在制定重寫方案之前,要先思考如何從toString中獲取想要的信息;

重寫toString代碼示例:

/**
* Returns the string representation of this phone number.
* The string consists of twelve characters whose format is
* "XXX-YYY-ZZZZ", where XXX is the area code, YYY is the
* prefix, and ZZZZ is the line number. Each of the capital
* letters represents a single decimal digit.
*
* If any of the three parts of this phone number is too small
* to fill up its field, the field is padded with leading zeros.
* For example, if the value of the line number is 123, the last
* four characters of the string representation will be "0123".
*/
@Override 
public String toString() {
    return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum);
}

13、謹(jǐn)慎的重寫clone方法

如果一個(gè)類可以被克隆,需要實(shí)現(xiàn)Cloneable接口,然后重寫clone方法。請(qǐng)注意clone方法屬于Object,并不屬于Cloneable。如果不實(shí)現(xiàn)Cloneable接口。調(diào)用clone方法會(huì)直接跑出異常。

重寫clone方法需要注意的地方:

  • 推薦重寫clone方法為public;
  • clone方法實(shí)現(xiàn)使用super.clone,不要使用new(書中說(shuō)會(huì)編譯不過(guò),實(shí)測(cè)可以運(yùn)行);
  • 不可復(fù)用的class,不要提供clone方法;
  • clone的對(duì)象是原始Object的副本,內(nèi)部的屬性對(duì)象與原始Object相同,注意使用時(shí)對(duì)原始對(duì)象的影響,可以考慮使用深拷貝(對(duì)每一個(gè)屬性對(duì)象都clone一份);
  • clone方法無(wú)法修改final的屬性對(duì)象;
  • 已經(jīng)實(shí)現(xiàn)的clone方法不應(yīng)該拋出異常;
  • Object的clone方法是線程不安全的,需要自行實(shí)現(xiàn)線程安全;

clone方法的缺點(diǎn):

  • 不會(huì)執(zhí)行構(gòu)造方法;
  • 拷貝原始對(duì)象的所有屬性,實(shí)際上大部分情況只需要拷貝特定幾個(gè)屬性;
  • 無(wú)法修改final屬性對(duì)象;
  • 如果有唯一標(biāo)識(shí),要注意clone的時(shí)候修改;

使用自定義copy方法,讓拷貝更靈活。可以考慮通過(guò)構(gòu)造方法或靜態(tài)方法等形式拷貝需要的原始對(duì)象的內(nèi)容。例如以下代碼:

// Copy constructor
public Yum(Yum yum) { ... };

// Copy factory
public static Yum newInstance(Yum yum) { ... };

使用copy方法的好處:

  • 沒(méi)有使用變成語(yǔ)言外的方式創(chuàng)建對(duì)象(clone使用的是native方法創(chuàng)建對(duì)象);
  • 不需要類型的強(qiáng)制轉(zhuǎn)換;
  • 可以修改final對(duì)象;
  • 不需要類型檢查(clone必須實(shí)現(xiàn)Cloneable接口);

14、考慮實(shí)現(xiàn)Comparable接口

如果類的對(duì)象需要排序,推薦該類實(shí)現(xiàn)Comparable接口的compareTo方法。當(dāng)調(diào)用排序方法:

Arrays.sort(a);

數(shù)組a會(huì)根據(jù)實(shí)現(xiàn)的Comparable進(jìn)行排序,這種排序規(guī)則是默認(rèn)的。重寫compareTo的規(guī)則和重寫equals的規(guī)則是相似的:

  • x.compareTo(y)) == - y.compareTo(x);
  • x.compareTo(y) > 0 && y.compareTo(z) > 0 , 可得 x.compareTo(z) > 0 ;
  • x.compareTo(y) > 0,可得 x.compareTo(z) == y.compareTo(z);

  • 大于0的正整數(shù),表示排序在前;
  • 0,表示保持目前排序;
  • 小于0的負(fù)整數(shù),表示排序在后;

重寫compareTo的建議:

  • 如果你想要給一個(gè)使用了Comparable的類添加自定義的排序規(guī)則,不要繼承它,而是用組合的形式,把該類和排序?qū)崿F(xiàn)的類,組合成一個(gè)新的類,這樣你可以隨時(shí)修改排序規(guī)則;

  • 強(qiáng)烈推薦(x.compareTo(y) == 0) == (x.equals(y)),如果該判斷不成立,要在方法的注釋中強(qiáng)調(diào)compareTo的結(jié)果與equals的結(jié)果不一致。一般集合會(huì)使用equals判斷是否為同一對(duì)象,但是排序集合使用的是compareTo。例如以下代碼:

val hashSet = HashSet();
val b1 = BigDecimal("1.0");
val b2 = BigDecimal("1.00");
// 因?yàn)閎1和b2的equals不相等,所以hashSet會(huì)保存b1和b2.
hashSet.add(b1);
hashSet.add(b2)

val treeSet = TreeSet();
val b1 = BigDecimal("1.0");
val b2 = BigDecimal("1.00");
// TreeSet內(nèi)部使用compareTo判斷是否相等,BigDecimal內(nèi)部重寫了compareTo方法,
// 所以b1.compareTo(b2) == 0, 即相等,所以treeSet只會(huì)有b1.
treeSet.add(b1);
treeSet.add(b2)
  • 在compareTo方法中不要使用and操作,如果你需要判斷多個(gè)關(guān)鍵的屬性,應(yīng)該按照優(yōu)先級(jí)依次判斷。例如下面的代碼:
// Multiple-field Comparable with primitive fields
public int compareTo(PhoneNumber pn) {
    int result = Short.compare(areaCode, pn.areaCode);
    if (result == 0) {
        result = Short.compare(prefix, pn.prefix);
            if (result == 0)
                result = Short.compare(lineNum, pn.lineNum);
    }
    return result;
}
  • Java 8 中Comparator提供了更簡(jiǎn)潔的對(duì)比操作,但是在運(yùn)行速度上Comparable相比
    慢了10%(該數(shù)據(jù)為作者提供)。應(yīng)當(dāng)優(yōu)先使用Comparator的靜態(tài)構(gòu)造方法。例如下面的代碼:
// Comparable with comparator construction methods
private static final Comparator<PhoneNumber> COMPARATOR =
            comparingInt((PhoneNumber pn) -> pn.areaCode)
            .thenComparingInt(pn -> pn.prefix)
            .thenComparingInt(pn -> pn.lineNum);
        
public int compareTo(PhoneNumber pn) {
    return COMPARATOR.compare(this, pn);
}
  • 不要在compareTo或者compare方法返回運(yùn)算的結(jié)果,這種寫法充滿了危險(xiǎn),例如數(shù)字的溢出,小數(shù)精度等等,應(yīng)當(dāng)使用對(duì)應(yīng)類型的compare方法,返回正確的結(jié)果。例如下面的代碼:
// 錯(cuò)誤的寫法
static Comparator<Object> hashCodeOrder = new Comparator<>(){
    public int compare(Object o1, Object o2) {
        return o1.hashCode() - o2.hashCode();
    }
};

// 正確的寫法1
static Comparator<Object> hashCodeOrder = new Comparator<>(){
    public int compare(Object o1, Object o2) {
        return Integer.compare(o1.hashCode(), o2.hashCode());
    }
};
// 正確的寫法2
static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容