絕大多數(shù)方法和構(gòu)造器對于傳遞給它們的參數(shù)值都會有些限制。比如,索引值必須大于等于0,且不能超過其最大值,對象不能為null等。這樣就可以在導(dǎo)致錯誤的源頭將錯誤捕獲,從而避免了該錯誤被延續(xù)到今后的某一時刻再被引發(fā),這樣就是加大了錯誤追查的難度。就如同編譯期能夠報出的錯誤總比在運行時才發(fā)現(xiàn)要更好一些。事實上,我們不僅僅需要在函數(shù)的內(nèi)部開始出進行這些通用的參數(shù)有效性檢查,還需要在函數(shù)的文檔中給予明確的說明,如在參數(shù)非法的情況下,會拋出那些異常,或?qū)е潞瘮?shù)返回哪些錯誤值等,見如下代碼示例:
/** * Returns a BigInteger whose value is(this mod m). This method * differs from the remainder method in that it always returns a * non-negative BigInteger. * @param m the modulus, which must be positive. * @return this mod m. * @throws ArithmeticException if m is less than or equal to 0.*/ public BigInteger mod(BigInteger m) { if (m.signum() <= 0) throw new ArithmeticException("Modulus <= 0: " + m); ... //Do the computation. }是不是我們?yōu)樗械姆椒ň枰龀鲞@樣的有效性檢查呢?對于未被導(dǎo)出的方法,如包方法等,你可以控制這個方法將在哪些情況下被調(diào)用,因此這時可以使用斷言來幫助進行參數(shù)的有效性檢查,如:
PRivate static void sort(long a[],int offset,int length) { assert(a != null); assert(offset >= 0 && offset <= a.length); assert(length >= 0 && length <= a.length - offset); ... //Do the computation }和通用的檢查方式不同,斷言在其條件為真時,無論外部包得客戶端如何使用它。斷言都將拋出AssertionError。它們之間的另一個差異在于如果斷言沒有起到作用,即-ea命令行參數(shù)沒有傳遞給java解釋器,斷言將不會有任何開銷,這樣我們就可以在調(diào)試期間加入該命令行參數(shù),在發(fā)布時去掉該命令行選項,而我們的代碼則不需要任何改動。
需要強調(diào)的是,對于有些函數(shù)的參數(shù),其在當(dāng)前函數(shù)內(nèi)并不使用,而是留給該類其他函數(shù)內(nèi)部使用的,比較明顯的就是類的構(gòu)造函數(shù),構(gòu)造函數(shù)中的很多參數(shù)都不一樣用于構(gòu)造器內(nèi),只是在構(gòu)造的時候進行有些賦值操作,而這些參數(shù)的真正使用者是該類的其他函數(shù),對于這種情況,我們就更需要在構(gòu)造的時候進行參數(shù)的有效性檢查,否則一旦將該問題釋放到域函數(shù)的時候,再追查該問題的根源,將不得不付出更大的代價和更多的調(diào)試時間。
對該條目的說法確實存在著一種例外情況,在有些情況下有效性檢查工作的開銷是非常大的,或者根本不切實際,因為這些檢查已經(jīng)隱含在計算過程中完成了,如Collections.sort(List),容器中對象的所有比較操作均在該函數(shù)執(zhí)行時完成,一旦比較操作失敗將會拋出ClassCastException異常。因此對于sort來講,如果我們提前做出有效性檢查將是毫無意義的。
如果你的對象沒有做很好的隔離,那么對于調(diào)用者而言,則有機會破壞該對象的內(nèi)部約束條件,因此我們需要保護性的設(shè)計程序。該破壞行為一般由兩種情況引起,首先就是惡意的破壞,再有就是調(diào)用者無意識的誤用,這兩種條件下均有可能給你的類帶來一定的破壞性,見如下代碼:
public final class Period { private final Date start; private final Date end; public Period(Date start,Date end) { if (start.compareTo(end) > 0) { throw new IllegalArgumentException(start + "After " + end); this.start = start; this.end = end; } public Date start() { return start; } public Date end() { return end; } }從表面上看,該類的實現(xiàn)確實對約束性的條件進行了驗證,然而由于Date類本身是可變了,因此很容易違反這個約束,見如下代碼:
public void testPeriod() { Date start = new Date(); Date end = new Date(); Period p = new Period(start,end); end.setYear(78); //該修改將直接影響Period內(nèi)部的end對象。 }為了避免這樣的攻擊,我們需要對Period的構(gòu)造函數(shù)進行相應(yīng)的修改,即對每個可變參數(shù)進行保護性拷貝。
public Period(Date start,Date end) { this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if (start.compareTo(end) > 0) { throw new IllegalArgumentException(start + "After " + end); }需要說明的是,保護性拷貝是在堅持參數(shù)有效性之前進行的,并且有效性檢查是針對拷貝之后的對象,而不是針對原始對象的。這主要是為了避免在this.start = new Date(start.getTime())到if (start.compareTo(end) > 0)這個時間窗口內(nèi),參數(shù)start和end可能會被其他線程修改。
現(xiàn)在構(gòu)造函數(shù)已經(jīng)安全了,后面我們需要用同樣的方式繼續(xù)修改另外兩個對象訪問函數(shù)。
public Date start() { return new Date(start.getTime()); } public Date end() { return new Date(end.getTime()); }經(jīng)過這一番修改之后,Period成為了不可變類,其內(nèi)部的“周期的起始時間不能落后于結(jié)束時間”約束條件也不會再被破壞。
參數(shù)的保護性拷貝并不僅僅針對不可變類。每當(dāng)編寫方法或者構(gòu)造器時,如果它要允許客戶提供的對象進入到內(nèi)部數(shù)據(jù)結(jié)構(gòu)中,則有必要考慮一下,客戶提供的對象進入到內(nèi)部數(shù)據(jù)結(jié)構(gòu)中,則有必要考慮一下,客戶提供的對象是否有可能是可變的。如果是,就要考慮你的類是否能夠容忍對象進入數(shù)據(jù)結(jié)構(gòu)之后發(fā)生變化。如果答案是否定的,就必須對該對象進行保護性拷貝,并且讓拷貝之后的對象而不是原始對象進入到數(shù)據(jù)結(jié)構(gòu)中。
例如,如果你正在考慮使用有客戶提供的對象引用作為內(nèi)部Set實例的元素,或者作為內(nèi)部Map實例的鍵(Key),就應(yīng)該意識到,如果這個對象在插入之后再被修改,Set或者Map的約束條件就會遭到破壞。
下面的例子根據(jù)一個集合是Set、List還是其他的集合類型,來對它進行分類:
public class CollectionClassfier { public static String classify(Set<?> s) { return "Set"; } public static String classify(List<?> l) { return "List"; } public static String classify(Collection<?> c) { return "Unknown collection"; } public static void main(String[] args) { Collection<?>[] collections = {new HashSet<String>(), new ArrayList<BigInteger>(), new HashMap<String,String>().values()}; for (Collection<?> c : collections) System.out.println(classify(c)); } }這里你可能會期望程序打印出Set、List、Unknown Collection,然而實際上卻不是這樣,輸出的結(jié)果是3 個”Unknown Collection”。 因為classify方法被重載了,需要調(diào)用哪個函數(shù)是在編譯期決定的,for中的三次迭代參數(shù)的編譯類型是相同的:
Collection<?>對于重載方法的選擇是靜態(tài)的,而對于被覆蓋的方法的選擇則是動態(tài)的。選擇被覆蓋的方法的正確版本是在運行時進行的,選擇的依據(jù)是被調(diào)用的方法所在對象的運行時類型。這里重新說明一下,當(dāng)一個子類包含的方法聲明與其祖先類中的方法聲明具有同樣的的簽名時,方法就被覆蓋了。如果實例方法在子類中被覆蓋了,并且這個方法是在該子類的實例上被調(diào)用的,那么子類中的覆蓋方法將會執(zhí)行,而不管該子類實例的編譯時類型到底是什么。
class Wine{ String name() {return "wine"; } } class SparklingWine extends Wine{ @Override String name(){return "sparkling wine"; } } class Champagne extends Wine{ @Override String name(){return "Champagne"; } } public class Overriding{ public static void main(String[] args){ Wine[] = { new Wine(), new SparklingWine(), new Champagne() }; } for(Wine wine : wines){ System.out.println(wine.name()); } }正如你所預(yù)期的那樣,這個程序打印出“wine, sparkling wine, champagne”,當(dāng)調(diào)用被覆蓋的方法時,對象的編譯時類型不會影響到哪個方法將被執(zhí)行。最為具體的那個覆蓋版本總是會得到執(zhí)行。
對于開始的集合輸出類的最佳修正方案是,用單個方法來替換這三個重載的classity方法,如下:
public static String classify(Collection<?> c) { return c instanceof Set ? "Set" : c instanceof List ? "List" : "Unknown Collection"; }因此,應(yīng)該避免胡亂地使用重載機制。 一、安全而保守的策略是,永遠(yuǎn)不要導(dǎo)出兩個具有相同參數(shù)數(shù)目的重載方法。比如兩個重載函數(shù)均有一個參數(shù),其中一個是整型,另一個是Collection<?>,對于這種情況,int 和Collection<?>之間沒有任何關(guān)聯(lián),也無法在兩者之間做任何的類型轉(zhuǎn)換,否則將會拋出ClassCastException 的異常,因此對于這種函數(shù)重載,我們是可以準(zhǔn)確確定的。反之,如果兩個參數(shù)分別是int 和short,他們之間的差異就不是這么明顯。 二、如果方法使用可變參數(shù),保守的策略是根本不要重載它。 三、對于構(gòu)造器,你沒有選擇使用不同名稱的機會,一個類的多個構(gòu)造器總是重載的,但是構(gòu)造器也不可能被覆蓋。 四、在Java 1.5 之后,需要對自動裝箱機制保持警惕。 演示如下:
public class SetList { public static void main(String[] args) { Set<Integer> s = new TreeSet<Integer>(); List<Integer> l = new ArrayList<Integer>(); for (int i = -3; i < 3; ++i) { s.add(i); l.add(i); } for (int i = 0; i < 3; ++i) { s.remove(i); l.remove(i); } System.out.println(s + " " + l); } }在執(zhí)行該段代碼前,我們期望的結(jié)果是Set 和List 集合中大于等于的元素均被移除出容器,然而在執(zhí)行后卻發(fā)現(xiàn)事實并非如此,其結(jié)果為:[-3,-2,-1] [-2,0,2]。這個結(jié)果和我們的期望還是有很大差異的,為什么Set 中的元素是正確的,而List 則不是,是什么導(dǎo)致了這一結(jié)果的發(fā)生呢?
下面給出具體的解釋:
s.remove(i)調(diào)用的是Set 中的remove(E),這里的E 表示Integer,Java 的編譯器會將i 自動裝箱到Integer 中,因此我們得到了想要的結(jié)果。
l.remove(i)實際調(diào)用的是List 中的remove(int index)重載方法,而該方法的行為是刪除集合中指定索引的元素。這里分別對應(yīng)第0 個,第1 個和第2 個。
為了解決這個問題,我們需要讓List 明確的知道,我們需要調(diào)用的是remove(E)重載函數(shù),而不是其他的,這樣我們就需要對原有代碼進行如下的修改:
public class SetList { public static void main(String[] args) { Set<Integer> s = new TreeSet<Integer>(); List<Integer> l = new ArrayList<Integer>(); for (int i = -3; i < 3; ++i) { s.add(i); l.add(i); } for (int i = 0; i < 3; ++i) { s.remove(i); l.remove((Integer)i); //or remove(Integer.valueOf(i)); } System.out.println(s + " " + l); } }總結(jié),對于多個具有相同參數(shù)數(shù)目的方法來說,應(yīng)該盡量避免重載方法。我們應(yīng)當(dāng)保證:當(dāng)傳遞同樣的參數(shù)時,所有重載方法的行為必須一致。
可變數(shù)組機制是通過先創(chuàng)建一個數(shù)組,數(shù)組的大小為在調(diào)用位置所傳遞的參數(shù)數(shù)量,然后將參數(shù)值傳到數(shù)組中,最后將數(shù)組傳遞給方法。
有的時候在重視性能的情況下,使用可變參數(shù)機制要特別小心。可變參數(shù)方法的每次調(diào)用都會導(dǎo)致進行一次數(shù)組分配和初始化。如果確定確實無法承受這一成本,但又需要可變參數(shù)的靈活性,還有一種模式可以彌補這一不足。假設(shè)確定對某個方法95%的調(diào)用會有3個或者更少的參數(shù),就聲明該方法的5個重載,每個重載方法帶有0個至3個普通參數(shù),當(dāng)參數(shù)的數(shù)目超過3個時,就使用一個可變參數(shù)方法:
public void foo() {}public void foo(int a1) {}public void foo(int a1,int a2) {}public void foo(int a1,int a2,int a3) {}public void foo(int a1,int a2,int a3,int...rest) {}所有調(diào)用中只有5%參數(shù)數(shù)量超過3個的調(diào)用需要創(chuàng)建數(shù)組。就像大多數(shù)的性能優(yōu)化一樣,這種方法通常不恰當(dāng),但是一旦真正需要它時,還是非常有用處的。
在定義參數(shù)數(shù)目不定的方法時,可變參數(shù)方法是一種很方便的方式,但是它們不應(yīng)該過度濫用。如果使用不當(dāng),會產(chǎn)生混亂的結(jié)果。
有時候會有人認(rèn)為:null返回值比零長度數(shù)據(jù)更好,因為它避免了分配數(shù)組所需要的開銷。 這種觀點是站不住腳的,原因有兩點。
在這個級別上擔(dān)心性能問題是不明智的,除非分析表明這個方法正是造成性能問題的真正源頭。對于不返回任何元素的調(diào)用,每次都返回同一個零長度數(shù)組是有可能的,因為零長度數(shù)組是不可變的,而不可變對象有可能被自由地共享。 private static final Cheese[] EMPTY_CHEESE_ARRAY= new Cheese[0];相比于數(shù)組,集合亦是如此。 在Collections中有專門針對List,Set,Map的空的實現(xiàn)。如:
Collections.emptyList()Collections.emptySet();Collections.emptyMap();略
《Effective Java中文版 第2版》PDF版下載: http://download.csdn.net/detail/xunzaosiyecao/9745699
作者:jiankunking 出處:http://blog.csdn.net/jiankunking
|
新聞熱點
疑難解答