本章討論方法(構(gòu)造方法趁怔、普通方法)設(shè)計(jì)的幾個(gè)方面:
如何處理參數(shù)和返回值
如何設(shè)計(jì)方法簽名
如何為方法編寫文檔
第38條:檢查參數(shù)的有效性
絕大多數(shù)方法和構(gòu)造器對(duì)于傳遞給它們的參數(shù)值都會(huì)有某些限制。應(yīng)該在文檔中清楚的指明所有這些限制甲棍,并在方法體的開頭處檢查參數(shù)室谚,以強(qiáng)制施加這些限制傻昙。這樣就遵照了應(yīng)該在發(fā)生錯(cuò)誤之后盡快檢測(cè)出錯(cuò)誤這個(gè)原則锄开。
傳遞無(wú)效的參數(shù)值給方法古沥,這個(gè)方法在執(zhí)行之前先對(duì)參數(shù)進(jìn)行校驗(yàn)瘸右,那么就能很清晰的拋出異常;如果跳過(guò)了這個(gè)步驟岩齿,可能會(huì)導(dǎo)致令人費(fèi)解的異常太颤、返回錯(cuò)誤的結(jié)果、或者返回正確的結(jié)果并在某個(gè)狀態(tài)下崩潰盹沈。
對(duì)于公有的方法龄章,對(duì)參數(shù)進(jìn)行檢驗(yàn)并拋出異常,同時(shí)在文檔中使用@throws標(biāo)簽聲明如果傳入?yún)?shù)違反了相關(guān)規(guī)定就會(huì)拋出異常乞封,這樣的寫法是完善的做裙。讓調(diào)用API者很清晰的就能了解。
/**
* @throws NullPointerException if params is null
*/
private void testOne(String s) {
if (s == null) {
throw new NullPointerException("params s can not be null");
}
System.out.print(s.toCharArray());
}
對(duì)于非公有的方法肃晚、即未向外導(dǎo)出的锚贱,因?yàn)槭浅绦騿T自己調(diào)用并使用的,也確認(rèn)參數(shù)的合法性陷揪,可以使用斷言(assertion)來(lái)檢查它們的參數(shù)惋鸥。
這個(gè)看情況吧,因?yàn)閍ssert語(yǔ)句只有在編譯器開啟了-ea后才會(huì)起作用悍缠,在團(tuán)隊(duì)開發(fā)時(shí)并不是好事卦绣;所以還是要做參數(shù)校驗(yàn)并拋出異常。
對(duì)于有些參數(shù)飞蚓,方法本身沒有用到滤港,卻被保存起來(lái)以后使用,檢驗(yàn)這類參數(shù)的有效性尤為重要趴拧。
例如在構(gòu)造方法中對(duì)屬性進(jìn)行初始化時(shí)溅漾,一定要進(jìn)行參數(shù)檢驗(yàn),避免違法了這個(gè)類的約束條件著榴。
在有些情況下添履,有效性檢驗(yàn)非常昂貴,或者是不切實(shí)際的脑又,并且該檢驗(yàn)已隱含在計(jì)算過(guò)程中了暮胧,在這種情況下進(jìn)行檢驗(yàn)就沒必要了锐借。
例如Collection.sort()方法。
對(duì)參數(shù)的進(jìn)行任何限制并不是一件好事往衷。在設(shè)計(jì)方法時(shí)钞翔,要盡可能的通用、并符合實(shí)際需要席舍。
假如方法對(duì)于它能接受的所有參數(shù)值都能夠完成工作布轿,對(duì)參數(shù)的限制就應(yīng)該越少越好。
總之来颤,在寫方法或者構(gòu)造器時(shí)汰扭,應(yīng)該考慮它的參數(shù)有哪些限制。
應(yīng)該把這些限制寫到文檔中脚曾,并且在這個(gè)方法開頭處东且,通過(guò)顯示的檢查來(lái)實(shí)施這些限制。
第39條:必要時(shí)進(jìn)行保護(hù)性拷貝
Java是一門安全的語(yǔ)言本讥,不必考慮緩沖區(qū)溢出珊泳、數(shù)組越界、非法指針等其他的內(nèi)存破壞錯(cuò)誤拷沸,而這些在C色查、C++中確實(shí)要考慮的; 假設(shè)類的客戶端會(huì)盡其所能的來(lái)破壞這個(gè)類的約束條件撞芍,因?yàn)槟惚仨毐Wo(hù)性的設(shè)計(jì)程序秧了。因?yàn)榧幢闶穷愋桶踩恼Z(yǔ)言,也無(wú)法與其他類隔離開來(lái)序无。
// 該類的約束為: start < end
public class Period {
private final Date mStart;
private final Date mEnd;
public Period(Date start, Date end) {
// 對(duì)構(gòu)造器的每個(gè)可變參數(shù)進(jìn)行保護(hù)性拷貝
this.mStart = new Date(start.getTime());
this.mEnd = new Date(end.getTime());
//檢查參數(shù)有效性
if (mStart.compareTo(mEnd) > 0) {
throw new IllegalArgumentException("start must be ahead of end");
}
}
public Date getmStart() {
return new Date(mStart.getTime()); //返回可變內(nèi)部域的保護(hù)性拷貝验毡,可以使用clone方法,因?yàn)閙Start就是Date類型的帝嗡,這是我們自己知道的
}
public Date getmEnd() {
return new Date(mEnd.getTime());
}
}
對(duì)于構(gòu)造器的每個(gè)可變參數(shù)進(jìn)行保護(hù)性拷貝是必要的
getter方法必須返回可變內(nèi)部域的保護(hù)性拷貝
保護(hù)性拷貝是在檢查參數(shù)有效性之前進(jìn)行的晶通,并且有效性檢查是針對(duì)拷貝后的對(duì)象進(jìn)行的。
這樣做可以在危險(xiǎn)階段期間另一個(gè)線程來(lái)改變類的參數(shù)哟玷。危險(xiǎn)階段:從檢查參數(shù)開始狮辽,直到拷貝參數(shù)之間的時(shí)間段。
對(duì)于參數(shù)類型可以被不可信任的子類化的參數(shù)巢寡,不要使用該對(duì)象的clone方法進(jìn)行保護(hù)性拷貝喉脖。
不要使用Date的clone方法來(lái)進(jìn)行保護(hù)性拷貝,因?yàn)閰?shù)Date是非final的抑月,不能保證clone方法一定返回Date對(duì)象:可能是出于惡意目的而設(shè)計(jì)的不可信任的子類树叽。
參數(shù)的保護(hù)性拷貝并不僅僅針對(duì)不可變類。
當(dāng)編寫方法或構(gòu)造方法時(shí)谦絮,如果它要允許客戶端提供的對(duì)象進(jìn)入內(nèi)部數(shù)據(jù)結(jié)構(gòu)時(shí)题诵,就要考慮客戶提供的對(duì)象是否有可能是可變的:如果是不可變的须误,就直接提供保護(hù)性拷貝;如果是可變的仇轻,就要考慮是否能夠容忍對(duì)象進(jìn)入數(shù)據(jù)結(jié)構(gòu)之后發(fā)生的變化:如果不能忍,就提供保護(hù)性拷貝奶甘。
長(zhǎng)度非零的數(shù)組總是可變的篷店。因此把內(nèi)部數(shù)據(jù)返回給客戶端的時(shí)候一定要進(jìn)行保護(hù)性拷貝。
在內(nèi)部組件被返回給客戶端之前臭家,對(duì)它們進(jìn)行保護(hù)性拷貝也是同樣的道理疲陕。認(rèn)真考慮是否應(yīng)該把一個(gè)指向內(nèi)部可變的引用返回給客戶端。
但其實(shí)钉赁,只要有可能蹄殃,都應(yīng)該使用不可變的對(duì)象作為對(duì)象內(nèi)部的組件,這樣就不必考慮保護(hù)性拷貝的問(wèn)題你踩。
例如Period那個(gè)例子诅岩,可以使用long值作為內(nèi)部屬性。
可以在類的文檔中清楚的講明带膜,調(diào)用者不能修改傳入的參數(shù)值或返回值吩谦。這樣可不用使用保護(hù)性拷貝,但這僅僅使用于代碼都是自己寫的的情形下膝藕。
總之式廷,如果類具有可變參數(shù)、并且是可以從外界傳入或返回的(并且在外界修改后違反類的約束的)芭挽,類就必須提供保護(hù)性拷貝滑废;
如果拷貝成本受到限制,并且信任它的客戶端不會(huì)不恰當(dāng)?shù)男薷慕M件袜爪,就可以在文檔中聲明客戶端不能修改類內(nèi)部的組件蠕趁,以此來(lái)替代保護(hù)性拷貝。
第40條:謹(jǐn)慎地設(shè)計(jì)方法簽名
謹(jǐn)慎的選擇方法名 易于理解的饿敲、與同一個(gè)包中其他名稱風(fēng)格一致的妻导、大眾認(rèn)可的
不要過(guò)于追求提供便利的方法 每個(gè)方法都應(yīng)該盡其所能。 方法太多會(huì)使類難以學(xué)習(xí)怀各、使用倔韭、文檔化、測(cè)試和維護(hù)瓢对。接口尤其如此寿酌。
避免過(guò)長(zhǎng)的參數(shù)列表,最多四個(gè) 盡量使用簡(jiǎn)短的參數(shù)列表硕蛹; 相同類型的長(zhǎng)參數(shù)列表格外有害醇疼,弄反了程序依然運(yùn)行硕并,排查困難很難; 有幾個(gè)方法可以嘗試:
把一個(gè)方法分解成多個(gè)方法秧荆,每個(gè)方法只需要這些參數(shù)的一個(gè)子集倔毙。通過(guò)提升這些子方法的正交性來(lái)避免產(chǎn)生的子方法過(guò)多。
創(chuàng)建輔助類乙濒,用來(lái)保存參數(shù)的分組陕赃。 如果一個(gè)頻繁出現(xiàn)的參數(shù)序列可以被看作是代表了某個(gè)獨(dú)特的實(shí)體,則建議使用這種方法颁股。
Build模式
對(duì)于參數(shù)類型么库,優(yōu)先使用接口而不是類 只要有適當(dāng)?shù)慕涌诳捎脕?lái)定義參數(shù),就優(yōu)先使用接口甘有,而不是該接口的實(shí)現(xiàn)類诉儒。
對(duì)于boolean參數(shù),優(yōu)先使用兩個(gè)元素的枚舉類型
第41條:慎用重載
public class CollectionClassifier{
public static String classify(Set<?> s){
return "Set";
}
public static String classify(List<?> list){
return "Set";
}
public static String classify(Collection<?> c){
return "Collection";
}
public static void main(String[] args){
Collection<?>[] collections = {new HashSet<>(), new ArrayList<>(), HashMap<String,String>().values()};
for(Collection<?> c : collections){
System.out.println(classify(c));
}
}
//返回三次Collection
}
程序運(yùn)行時(shí)亏掀,對(duì)于重載方法overloaded method的選擇是靜態(tài)的忱反,即根據(jù)編譯時(shí)期的類型,而對(duì)于復(fù)寫方法overriden method的選擇是動(dòng)態(tài)的滤愕,即根據(jù)運(yùn)行時(shí)期的類型缭受。要注意的是,當(dāng)一個(gè)子類包含的方法聲明與其祖先類中的方法聲明具有同樣的簽名時(shí)该互,方法就被覆蓋了米者。如果實(shí)例方法在子類中被覆蓋了,并且這個(gè)方法是在該子類的實(shí)例上被調(diào)用的宇智,那么子類中的覆蓋方法將會(huì)執(zhí)行蔓搞,而不管子類實(shí)例編譯時(shí)的類型到底是什么。
避免胡亂使用重載機(jī)制
如果用戶根本不知道對(duì)于一組給定的參數(shù)随橘,其中的哪個(gè)重載方法將會(huì)被調(diào)用喂分,那么這樣的API就很可能導(dǎo)致錯(cuò)誤。這些錯(cuò)誤很隱蔽且難以排查机蔗。
怎么避免胡亂使用重載機(jī)制
永遠(yuǎn)不要導(dǎo)出兩個(gè)具有相同參數(shù)數(shù)目的重載方法蒲祈;
也不要重載具有可變參數(shù)的方法;
使用不同的方法名萝嘁;對(duì)于構(gòu)造器梆掸,可以使用靜態(tài)工廠
確定選擇哪個(gè)重載方法的規(guī)則是非常復(fù)雜的,大多數(shù)情況下盡可能謹(jǐn)慎的使用牙言,盡量不用酸钦,除非參數(shù)類型是不相關(guān)的。
總之,能夠重載方法并不意味著應(yīng)該重載方法厕宗。
- 對(duì)于多個(gè)具有相同參數(shù)數(shù)目的方法來(lái)說(shuō),應(yīng)該盡量避免重載方法眨攘。
- 第一條做不到時(shí)欢伏,避免同一組參數(shù)只需經(jīng)過(guò)類型轉(zhuǎn)換就可以傳遞給不同的重載方法入挣;
- 第二條做不到時(shí),就要保證當(dāng)傳遞同樣的參數(shù)時(shí)硝拧,所有重載方法的行為必須一致财岔;
第42條:慎用可變參數(shù)
可匹配不同長(zhǎng)度的變量的方法:variable arity method。
可變參數(shù):接收0個(gè)或多個(gè)指定類型的參數(shù)河爹。
可變參數(shù)機(jī)制通過(guò)先創(chuàng)建一個(gè)數(shù)組,數(shù)組的大小為調(diào)用位置所傳遞的參數(shù)數(shù)量桐款,然后將參數(shù)值傳到數(shù)組中咸这,最后將數(shù)組傳遞給方法。
可變參數(shù)方法的每次調(diào)用都會(huì)導(dǎo)致進(jìn)行一次數(shù)組分配和初始化魔眨。所以在重視性能的情況下媳维,可以考慮在4個(gè)參數(shù)以為不使用可變參數(shù)。
在定義參數(shù)數(shù)目不定的方法時(shí)遏暴,可以使用可變參數(shù)侄刽;但不應(yīng)該過(guò)渡濫用。
第43條:返回零長(zhǎng)度的數(shù)組或者集合朋凉,而不是null
返回null而不是零長(zhǎng)度數(shù)組或空集合會(huì)使調(diào)用者在幾乎用到該方法時(shí)都要進(jìn)行判空操作州丹,相對(duì)來(lái)說(shuō)比較麻煩;而且忘記該操作的話杂彭,就會(huì)出錯(cuò)墓毒。
性能方面
在這個(gè)級(jí)別上擔(dān)心性能問(wèn)題是不明智的,除非分析表明這個(gè)方法正是性能問(wèn)題的真正源頭亲怠。
返回一個(gè)零長(zhǎng)度數(shù)組或空集合是可能的所计,因?yàn)榱汩L(zhǎng)度數(shù)組是不可變的,而不可變對(duì)象是可以被共享的团秽。
Collections.emptyList()等主胧。
可以考慮返回空數(shù)組或空集合,看情況而定吧习勤。
第44條:為所有導(dǎo)出的API元素編寫文檔注釋
使用Javadoc來(lái)根據(jù)源碼的文檔注釋生成相應(yīng)的文檔踪栋。
為了正確的編寫API文檔,必須在每個(gè)被導(dǎo)出的類图毕、接口己英、構(gòu)造器、方法和域聲明之前增加一個(gè)文檔注釋吴旋。
方法的文檔注釋應(yīng)該簡(jiǎn)介的描述出它和客戶端之間的約定
對(duì)于類损肛、接口厢破、域,概要描述應(yīng)該是一個(gè)名詞短語(yǔ)治拿,它描述了該類或者接口的實(shí)例摩泪,或者域本身所代表的事物。
對(duì)于泛型劫谅、枚舉见坑、注解,確保要在文檔中說(shuō)明所有的類型參數(shù)捏检。
對(duì)于枚舉荞驴,一定要確保在文檔中說(shuō)明常量。
對(duì)于注解贯城,要確保在文檔中說(shuō)明所有成員熊楼,以及類型本身。
注意描述類的線程安全性能犯,如果類是可序列化的鲫骗,要說(shuō)明序列化形式
可以查看《How to Write Doc Comments》這本書。