三十八、檢查參數的有效性:
?? ?? 絕大多數方法和構造器對于傳遞給它們的參數值都會有些限制。比如,索引值必須大于等于0,且不能超過其最大值,對象不能為null等。這樣就可以在導致錯誤的源頭將錯誤捕獲,從而避免了該錯誤被延續到今后的某一時刻再被引發,這樣就是加大了錯誤追查的難度。就如同編譯期能夠報出的錯誤總比在運行時才發現要更好一些。事實上,我們不僅僅需要在函數的內部開始出進行這些通用的參數有效性檢查,還需要在函數的文檔中給予明確的說明,如在參數非法的情況下,會拋出那些異常,或導致函數返回哪些錯誤值等,見如下代碼示例:
?? ??? 是不是我們為所有的方法均需要做出這樣的有效性檢查呢?對于未被導出的方法,如包方法等,你可以控制這個方法將在哪些情況下被調用,因此這時可以使用斷言來幫助進行參數的有效性檢查,如:
1 private static void sort( long a[], int offset, int length) { 2 assert (a != null ); 3 assert (offset >= 0 && offset <= a.length); 4 assert (length >= 0 && length <= a.length - offset); 5 ... // Do the computation 6 }
?? ??? 和通用的檢查方式不同,斷言在其條件為真時,無論外部包得客戶端如何使用它。斷言都將拋出AssertionError。它們之間的另一個差異在于如果斷言沒有起到作用,即-ea命令行參數沒有傳遞給java解釋器,斷言將不會有任何開銷,這樣我們就可以在調試期間加入該命令行參數,在發布時去掉該命令行選項,而我們的代碼則不需要任何改動。
?? ??? 需要強調的是,對于有些函數的參數,其在當前函數內并不使用,而是留給該類其他函數內部使用的,比較明顯的就是類的構造函數,構造函數中的很多參數都不一樣用于構造器內,只是在構造的時候進行有些賦值操作,而這些參數的真正使用者是該類的其他函數,對于這種情況,我們就更需要在構造的時候進行參數的有效性檢查,否則一旦將該問題釋放到域函數的時候,再追查該問題的根源,將不得不付出更大的代價和更多的調試時間。
?? ??? 對該條目的說法確實存在著一種例外情況,在有些情況下有效性檢查工作的開銷是非常大的,或者根本不切實際,因為這些檢查已經隱含在計算過程中完成了,如Collections.sort(List),容器中對象的所有比較操作均在該函數執行時完成,一旦比較操作失敗將會拋出ClassCastException異常。因此對于sort來講,如果我們提前做出有效性檢查將是毫無意義的。
?? ?
?
三十九、必要時進行保護性拷貝:
?? ?? 如果你的對象沒有做很好的隔離,那么對于調用者而言,則有機會破壞該對象的內部約束條件,因此我們需要保護性的設計程序。該破壞行為一般由兩種情況引起,首先就是惡心的破壞,再有就是調用者無意識的誤用,這兩種條件下均有可能給你的類帶來一定的破壞性,見如下代碼:
????? 從表面上看,該類的實現確實對約束性的條件進行了驗證,然而由于Date類本身是可變了,因此很容易違反這個約束,見如下代碼:
1 public void testPeriod() { 2 Date start = new Date(); 3 Date end = new Date(); 4 Period p = new Period(start,end); 5 end.setYear(78); // 該修改將直接影響Period內部的end對象。 6 }
????? 為了避免這樣的攻擊,我們需要對Period的構造函數進行相應的修改,即對每個可變參數進行保護性拷貝。
1 public Period(Date start,Date end) { 2 this .start = new Date(start.getTime()); 3 this .end = new Date(end.getTime()); 4 if (start.compareTo(end) > 0) { 5 throw new IllegalArgumentException(start + "After " + end); 6 }
????? 需要說明的是,保護性拷貝是在堅持參數有效性之前進行的,并且有效性檢查是針對拷貝之后的對象,而不是針對原始對象的。這主要是為了避免在this.start = new Date(start.getTime())到if (start.compareTo(end) > 0)這個時間窗口內,參數start和end可能會被其他線程修改。
?? ?? 現在構造函數已經安全了,后面我們需要用同樣的方式繼續修改另外兩個對象訪問函數。
1 public Date start() { 2 return new Date(start.getTime()); 3 } 4 public Date end() { 5 return new Date(end.getTime()); 6 }
????? 經過這一番修改之后,Period成為了不可變類,其內部的“周期的起始時間不能落后于結束時間”約束條件也不會再被破壞。
?? ?? 參數的保護性拷貝并不僅僅針對不可變類。每當編寫方法或者構造器時,如果它要允許客戶提供的對象進入到內部數據結構中,則有必要考慮一下,客戶提供的對象進入到內部數據結構中,則有必要考慮一下,客戶提供的對象是否有可能是可變的。如果是,就要考慮你的類是否能夠容忍對象進入數據結構之后發生變化。如果答案是否定的,就必須對該對象進行保護性拷貝,并且讓拷貝之后的對象而不是原始對象進入到數據結構中。例如,如果你正在考慮使用有客戶提供的對象引用作為內部Set實例的元素,或者作為內部Map實例的鍵(Key),就應該意識到,如果這個對象在插入之后再被修改,Set或者Map的約束條件就會遭到破壞。
?? ?
四十一、謹慎重載:
?? ?? 見下面一個函數重載的例子:
????? 這里你可能會期望程序打印出
?? ?? //Set
?? ?? //List
?? ?? //Unknown Collection
?? ?? 然而實際上卻不是這樣,輸出的結果是3個"Unknown Collection"。為什么會是這樣呢?因為函數重載后,需要調用哪個函數是在編譯期決定的,這不同于多態的運行時動態綁定。針對此種情形,該條目給出了一個修正的方法,如下:
1 public static String classify(Collection<?> c) { 2 return c instanceof Set ? "Set" : c instanceof List 3 ? "List" : "Unknown Collection"; 4 }
????? 和override不同,重載機制不會像override那樣規范,并且每次都能得到期望的結果。因此在使用時需要非常謹慎,否則一旦出了問題,就會需要更多的時間去調試。該條目給出以下幾種盡量不要使用重載的情形:
?? ?? 1.?? ?函數的參數中包含可變參數;
? ? ? 2.?? ?當函數參數數目相同時,你無法準確的確定哪一個方法該被調用時;
? ? ? 3.?? ?在Java 1.5 之后,需要對自動裝箱機制保持警惕。
?? ?? 我們先簡單說一下第二種情形。比如兩個重載函數均有一個參數,其中一個是整型,另一個是Collection<?>,對于這種情況,int和Collection<?>之間沒有任何關聯,也無法在兩者之間做任何的類型轉換,否則將會拋出ClassCastException的異常,因此對于這種函數重載,我們是可以準確確定的。反之,如果兩個參數分別是int和short,他們之間的差異就不是這么明顯。
? ? ? 對于第三種情形,該條目給出了一個非常典型的用例代碼,如下:
?? ?? 在執行該段代碼前,我們期望的結果是Set和List集合中大于等于的元素均被移除出容器,然而在執行后卻發現事實并非如此,其結果為:
? ? ? [-3,-2,-1] [-2,0,2]
? ? ? 這個結果和我們的期望還是有很大差異的,為什么Set中的元素是正確的,而List則不是,是什么導致了這一結果的發生呢?下面給出具體的解釋:
? ? ? 1. s.remove(i)調用的是Set中的remove(E),這里的E表示Integer,Java的編譯器會將i自動裝箱到Integer中,因此我們得到了想要的結果。
? ? ? 2. l.remove(i)實際調用的是List中的remove(int index)重載方法,而該方法的行為是刪除集合中指定索引的元素。這里分別對應第0個,第1個和第2個。
? ? ? 為了解決這個問題,我們需要讓List明確的知道,我們需要調用的是remove(E)重載函數,而不是其他的,這樣我們就需要對原有代碼進行如下的修改:
? ? ? 該條目還介紹了一種實現函數重載,同時又盡可能避免上述錯誤發生的方式。即其中的一個重載函數,在其內部通過一定的轉換邏輯轉換之后,再通過轉換后的參數類型調用其他的重載函數,從而確保即便使用者在使用過程中出現重載誤用的情況,也因兩者可以得到相同的結果而規避了潛在錯誤的發生。
四十二、慎用可變參數:
? ? ? 可變參數方法接受0個或者多個指定類型的參數??勺儏禉C制通過先創建一個數組,數組的大小為在調用位置所傳遞的參數數量,然后將參數值傳到數組中,最后將數組傳遞給方法,如:
1 static int sum( int ...args) { 2 int sum = 0; 3 for ( int arg : args) 4 sum += arg; 5 retrun sum; 6 }
? ? ? 上面的方法可以正常的工作,但是在有的時候,我們可能需要至少一個或者多個某種類型參數的方法,如:
? ? ? 對于上面的代碼主要存在兩個問題,一是如果調用者沒有傳遞參數是,該函數將會在運行時拋出異常,而不是在編譯期報錯。另一個問題是這樣的寫法也是非常不美觀的,函數內部必須做參數的數量驗證,不僅如此,這也影響了效率。將編譯期可以完成的事情推到了運行期。下面提供了一種較好的修改方式,如下:
? ? ? 由此可見,當你真正需要讓一個方法帶有不定數量的參數時,可變參數就非常有效。
? ? ? 有的時候在重視性能的情況下,使用可變參數機制要特別小心??勺儏捣椒ǖ拿看握{用都會導致進行一次數組分配和初始化。如果確定確實無法承受這一成本,但又需要可變參數的靈活性,還有一種模式可以彌補這一不足。假設確定對某個方法95%的調用會有3個或者更少的參數,就聲明該方法的5個重載,每個重載方法帶有0個至3個普通參數,當參數的數目超過3個時,就使用一個可變參數方法:
1 public void foo() {} 2 public void foo( int a1) {} 3 public void foo( int a1, int a2) {} 4 public void foo( int a1, int a2, int a3) {} 5 public void foo( int a1, int a2, int a3, int ...rest) {}
? ? ? 所有調用中只有5%參數數量超過3個的調用需要創建數組。就像大多數的性能優化一樣,這種方法通常不恰當,但是一旦真正需要它時,還是非常有用處的。
?? ?
四十三、返回零長度的數組或者集合,而不是null:
? ? ? 見如下代碼:
?? ?? 從以上代碼可以看出,當沒有Cheese的時候,getCheeses()函數返回一種特例情況null。這樣做的結果會使所有的調用代碼在使用前均需對返回值數組做null的判斷,如下:
1 public void testGetCheeses(CheesesShop shop) { 2 Cheese[] cheeses = shop.getCheeses(); 3 if (cheese != null && Array.asList(cheeses).contains(Cheese.STILTON)) 4 System.out.println("Jolly good, just the thing."); 5 }
? ? ? 對于一個返回null而不是零長度數組或者集合的方法,幾乎每次用到該方法時都需要這種曲折的處理方式。很顯然,這樣是比較容易出錯的。如果我們使getCheeses()函數在沒有Cheese的時候不再返回null,而是返回一個零長度的數組,那么我的調用代碼將會變得更加簡潔,如下:
1 public void testGetCheeses2(CheesesShop shop) { 2 if (Array.asList(shop.getCheeses()).contains(Cheese.STILTON)) 3 System.out.println("Jolly good, just the thing."); 4 }
? ? ? 相比于數組,集合亦是如此。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
