Effective Java 讀書筆記(2)

Object 通用方法

Object是一個具體類,但是設(shè)計它主要是為了擴(kuò)展,其所有的非final方法(equals、hashCode、toString、clone、finalize)都有明確的通用約定,我們可以在遵守通用約定的前提下override這些方法。

注:不遵守通用約定(general contract)復(fù)寫Object類的非final方法,可以會導(dǎo)致依賴于這些約定的類(hashMap之類的)無法正常工作。


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

Object的equals原意:類的實例只與它本身相同,即比較地址相同,不比較值。
在以下任意情況下不應(yīng)覆蓋equals方法:

  • 類的每個實例本質(zhì)上都是唯一的,即比較地址不比較值(如Thread)
  • 不關(guān)心類是否提供了“邏輯相等(值相等)”的測試功能
  • 超類已覆蓋了equals方法,且適用于子類(如Set實現(xiàn)類從AbstractSet繼承equals方法,List實現(xiàn)類從AbstractList繼承equals方法)
  • 類是私有的或是包級私有的,可以確定其equals不會被調(diào)用。

Tip:禁止調(diào)用可以在對應(yīng)的方法體內(nèi)拋出錯誤throw new AssertionError()

如果類具有其特有的“邏輯相等”概念。并且超類沒有實現(xiàn)期待的行為,此時我們需要覆蓋equals方法。這通常屬于“值類(value class)”的情況。值類僅僅是一個表示值的類,如Integer或者Date。當(dāng)我們調(diào)用值類的equals時,往往是為了判斷其值是否相等,而不關(guān)心是否為同一個對象(可以使用==判斷是否為同一實例)。

Tip: Set、List、Map等集合類多使用equals判斷key是否相等,所以我們需要在使用這些類時確保我們的equals方法是符合我們的預(yù)期的。

equals方法的通用約定(JavaSE6):
x、y、z皆為非null引用值

  • 自反性(reflextive):assert x.equals(x);
  • 對稱性(symmetric):if(x.equals(y)) assert y.equals(x);
  • 傳遞性(transitive):if(x.equals(y)&&y.equals(z)) assert x.equals(z);
  • 一致性(consistent):如果x、y未被修改,則調(diào)用x.equals(y)應(yīng)一直返回true或者一直返回false
  • 非空性(Non-nullify):x.equals(null)必須返回false,assert !x.equals(null)

實現(xiàn)高質(zhì)量equals的訣竅:

  • 使用==操作符檢查“參數(shù)是否為這個對象的引用”
  • 使用instanceof操作符檢查“參數(shù)是否為正確的類型”
  • 把參數(shù)轉(zhuǎn)換為正確的類型
  • 對于該類的“關(guān)鍵域”,逐一比較(見以下Tip)
  • 回顧并進(jìn)行單元測試:對稱性、傳遞性、一致性

Tip: 逐一比較的訣竅:
1、對于不是float、double的基本類型域,可以直接使用==進(jìn)行比較;
2、對于對象引用域,可以遞歸地調(diào)用equals方法進(jìn)行比較;
3、對于float域,可以使用Float.compare方法;對于double域,可以使用Double.compare方法;
4、對于數(shù)組域,可以使用Array.equals方法
5、使用field == null ? o.field == null : field.equals(o.field)(field == o.field) || (field != null) && (field.equals(o.field))習(xí)慣寫法
6、優(yōu)化比較順序:優(yōu)先比較最有可能不一致的域、比較開銷最低的域。不比較不屬于對象邏輯狀態(tài)(值)的域,或者冗余域(除非其能代表大量關(guān)鍵域,比較其能節(jié)省大量時間)

最后的告誡:

  • 覆蓋equals時總要覆蓋hashCode(見第9條);
  • 不要企圖讓equals過于智能;
  • 不要將equals聲明中的Object對象替換為其他的類型(這樣是overload而不是override),可以添加@override注解幫助規(guī)避錯誤。

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

在Object類中,hashCode方法是一個native方法,返回值與對象的存儲地址相關(guān),計算方法由jvm決定。

hashCode通用公約(JavaSE6):

  • 在應(yīng)用程序的執(zhí)行期間,只要對象的equals方法的比較操作所用到的信息沒有被修改,那么對這同一個對象調(diào)用多次,hashCode方法都必須始終如一地返回同一個整數(shù)。在同一個程序多次執(zhí)行過程中,每次執(zhí)行所返回的整數(shù)可以不一致;
  • 如果兩個對象根據(jù)equals方法比較是相等的,那么調(diào)用者兩個對象的hashCode方法必須返回同一個整數(shù);
  • 如果兩個對象根據(jù)equals方法比較是不相等的,那么調(diào)用者兩個對象的hashCode方法不一定返回不同整數(shù)。給不相等的對象產(chǎn)生截然不同的整數(shù)結(jié)果,有可能提高散列表(hash table)的性能;

注: HashTable、HashMap、HashSet等散列集合類的散列算法實現(xiàn)往往依賴于hashCode。假設(shè)我們覆蓋了equals方法,但沒有覆蓋hashCode,那么在我們理解中equals的對象,對于散列集合類而言,可能是不相等。

高質(zhì)量的hashCode方法的原則:

  • 對于equals的對象,返回同一個整數(shù);
  • 對于非equals的對象,盡可能返回不一樣的整數(shù);
  • 為不相等的對象均勻產(chǎn)生不相等的散列碼。

編寫好質(zhì)量的hashCode的訣竅:

  • 把某個非零的常數(shù)值(如17)保存在一個名為result的int類型的變量中;
  • 對于對象中的每個關(guān)鍵域f,完成以下步驟:
    1、為該域計算int類型的散列碼c(見以下Tip);
    2、計算result = 31 * result +c,合并c
  • 返回result
  • 編寫單元測試,保證相等的實例能得到同一個散列值

TIp: 計算int類型的散列碼c
1、如果該域是boolean類型,則計算f ? 1 : 0;
2、如果該域是byte、char、short、int類型,則計算(int) f
3、如果該域是long類型,則計算(int)( f ^ (f>>>32))
4、如果該域是float類型,則計算Float.floatToTintBits(f);
5、如果該域是long類型,則計算'Double.doubleToLongBits(f)',然后按照步驟3為long計算散列值;
6、如果該域是一個對象引用,為null則返回0,否則遞歸調(diào)用hashCode;
7、如果該域是一個數(shù)組,調(diào)用Arrays.hashCode;

在計算散列碼時,只能用到覆蓋的equals函數(shù)比較的關(guān)鍵域,否則相等(equals)的對象可以會得到不同的散列值。
初始化常數(shù)值17是任選的,不為0就可以了,目的在于增加散列值為0的關(guān)鍵域的影響。
31是一個比較特殊的數(shù)字,首先它是一個奇素數(shù),如果乘數(shù)為偶數(shù),則相當(dāng)于移位,會增加沖突。再者,其乘法可以被jvm自動優(yōu)化:31 * i == (i << 5 ) - i

Tip:如果一個對象是不可變的,或者其散列值計算開銷比較大,可以考慮將其散列值在實例初始化時計算后緩存在對象內(nèi)部。


第10條:始終要覆蓋toString

在Object類中,toString會返回類名,一個@符號,和散列碼的無符號十六進(jìn)制表示法,這通常不是我們所希望看到的。
toString的通用約定指出,被返回的字符串應(yīng)該是一個簡潔的,信息豐富的,易于閱讀的表達(dá)形式,并建議所有的子類都o(jì)verride這個方法。
提供好的toString實現(xiàn)可以使類用起來更加舒適,特別是在打印對象信息的時候,我們可以之間將對象作為參數(shù)直接傳入print函數(shù),打印出對象信息。
在實際應(yīng)用中,toString方法應(yīng)該返回對象中包含的所有值得關(guān)注的信息
在實現(xiàn)toString時,還應(yīng)考慮是否在文檔中指定返回值的格式。若是指定格式,最好再提供一個靜態(tài)工廠方法或者構(gòu)造器,以便從這種表示法中轉(zhuǎn)換對象;若是不指定返回值的格式,則可以保持toString方法的靈活性,以便在后期改進(jìn)格式。


第11條:謹(jǐn)慎覆蓋clone方法

Cloneable接口的目的在于表明這個對象是允許被clone的,然而它并沒有成功地達(dá)到這個目的。其主要原因在于它并沒有包含任何方法,而Object的clone方法是受保護(hù)的,需要反射調(diào)用,而且反射調(diào)用也不一定會成功。
Cloneable接口的作用在于表明:實現(xiàn)了Cloneable接口的類,應(yīng)該覆蓋了Object的clone方法。
clone方法的通用公約:
創(chuàng)建和返回該對象的一個拷貝。這個拷貝的精確含義由該對象的類決定。一般有以下含義:

  • 對于任何對象x,表達(dá)式x.clone() != x將會是true
  • 對于任何對象x,表達(dá)式x.clone().getClass() == x.getClass將會是true
  • 對于任何對象x,表達(dá)式x.clone().equals(x)將會是true

拷貝對象往往會導(dǎo)致創(chuàng)建它的類的一個新實例,但它同時要求拷貝類內(nèi)部的數(shù)據(jù)結(jié)構(gòu)。這個過程沒有調(diào)用構(gòu)造器。

覆蓋clone方法,必須保證其拷貝是深度拷貝,這往往需要其超類實現(xiàn)了良好的clone方法。其次,對于拷貝對象內(nèi)部的引用對象,我們需要實例化一個新的對象,并修改其值,而不能簡單復(fù)制引用。覆蓋clone方法是一件十分吃力不討好的事。
所有實現(xiàn)了Cloneable接口的類都應(yīng)該用一個公有的方法覆蓋clone。此公有方法首先調(diào)用super.clone(),然后修正任何需要修正的域。
除非你擴(kuò)展了一個實現(xiàn)Cloneable接口的類,否則最好提供某些其他的途徑代替clone方法,或者干脆不提供這樣的功能。
我們可以提供一個拷貝構(gòu)造器或者拷貝工廠來替代clone方法:

// 拷貝構(gòu)造器:以拷貝對象為參數(shù)
public Yum(Yum yum)
// 拷貝工廠
public static Yum newInstance(Yum yum)

更進(jìn)一步,我們可以提供一個轉(zhuǎn)換構(gòu)造器,其參數(shù)是一個超類/接口的對象。按照慣例,所有通用集合實現(xiàn)都提供了一個轉(zhuǎn)換構(gòu)造器,如一個HashSet s,可以通過TreeSet(s)轉(zhuǎn)換類型。

注意:對于一個專門為了繼承而設(shè)計的類,如果你未能提供香味良好的受保護(hù)的clone方法,它的子類就不可能實現(xiàn)Cloneable方法。


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

compareTo方法是Comparable接口唯一的方法,它與Object類的通用方法有很大的相似性。compareTo方法不但允許進(jìn)行簡單的等同性比較,而且允許執(zhí)行順序比較。類實現(xiàn)了Comparable接口,就表明它的實例具有內(nèi)在的排序關(guān)系。對于實現(xiàn)了Comparable接口的對象數(shù)組進(jìn)行排序:Arrays.sort(a)
一旦類實現(xiàn)了Comparable接口,它就可以跟許多泛型算法以及依賴于該接口的集合實現(xiàn)進(jìn)項協(xié)作。事實上,java的所有公共值類都實現(xiàn)了這個接口。假設(shè)你正在編寫一個值類,它具有非常明顯的內(nèi)在排序關(guān)系,那么你就應(yīng)該堅決考慮實現(xiàn)這個接口。

compareTo的通用公約:
將這個對象與指定的對象進(jìn)行比較,當(dāng)對象

  • 小于指定對象時,返回負(fù)整數(shù);
  • 等于指定對象時,返回0;
  • 大于指定對象時,返回正整數(shù);
  • 無法與指定對象進(jìn)行比較,拋出ClassCastException

與equals類似,compareTo同樣有自反型傳遞性、對稱性
編寫compareTo方法與編寫equals方法非常相似,但也有一些區(qū)別:

  • 不用類型檢查與轉(zhuǎn)換,compareTo方法的參數(shù)是靜態(tài)類型,不是Object
  • 當(dāng)參數(shù)為null時,應(yīng)該拋出NullPointException
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,899評論 18 399
  • 文章作者:Tyan博客:noahsnail.com | CSDN | 簡書 CHAPTER3 Method...
    SnailTyan閱讀 779評論 1 4
  • 第三章講了一些通用的方法??吹臅r候很快,記筆記的時候慢了 0x01 equals方法應(yīng)該如何寫 equals 方法...
    MaxZing閱讀 376評論 0 0
  • java筆記第一天 == 和 equals ==比較的比較的是兩個變量的值是否相等,對于引用型變量表示的是兩個變量...
    jmychou閱讀 1,661評論 0 3
  • 對象的創(chuàng)建與銷毀 Item 1: 使用static工廠方法,而不是構(gòu)造函數(shù)創(chuàng)建對象:僅僅是創(chuàng)建對象的方法,并非Fa...
    孫小磊閱讀 2,186評論 0 3

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