方法
第38條:檢查參數的有效性
在方法體的開頭處檢查參數,對於公有的方法,要用Javadoc的@throws標籤在文檔中說明違反參數值限制時會拋出的異常
/** * @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); }}
非公有的方法通常應該使用斷言來檢查它們的參數
// Private helper function for a recursive sortprivate static void sort(long a[], int offset, int length){ assert a != null; assert offset >= 0 && offset <= a.length;}
斷言一旦失敗將會拋出AssertionError
對於有些參數,方法本身沒有用到,卻被儲存起來供以後使用,檢驗這類參數的有效性尤為重要(如構造器參數的檢驗)
在方法執行它的計算任務之前,應該先檢查它的參數,這一規則也有例外,一個很重要的例外是,在有些情況下,有效性檢查工作非常昂貴,或者根本是不切實際的,而且有效性檢查已隱含在計算過程中完成,例如Collections.sort(List),其中的某個比較操作就會拋出ClassCastException
有時候,某些計算會隱式地執行必要的有效性檢查,但是如果檢查不成功,就會拋出錯誤的異常,這種情況下,應該將計算過程中拋出的異常轉換為正確的異常(文檔中標明的異常) 第39條:必要時進行保護性拷貝
沒有對象的協助時,雖然另一個類不可能修改對象的內部狀態,但是對象很容易在無意識的情況下提供這種協助,如下類
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; }}
這個類似乎是不可變的,然而因為Date類本身是可變的
Date start = new Date();Date end = new Date();Period p = new Period(start, end);end.setYear(78); // Modifies internals of p!
為了保護Period執行個體的內部資訊避免受到這種攻擊,對於構造器的每個可變參數進行保護性拷貝是必要的
public Period(Date start, Date end) { this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if (this.start.compareTo(this.end) > 0) throw new IllegalArgumentException(start +" after "+ end);}
保護性拷貝是在檢查參數的有效性之前進行的,並且有效性檢查是針對拷貝之後的對象,而不是針對原始的對象 第40條:謹慎設計方法簽名 謹慎地選擇方法的名稱。命名規範 不要過於追求提供便利的方法。每個方法都應該盡其所能,方法太多會使類難以學習、使用、文檔化、測試和維護 避免過長的參數列表。目標是四個參數,或者更少,可以採用Builder模式,允許用戶端進行多次“setter”調用 第41條:慎用重載
public class CollectionClassifier { public static String classify(Set<?> s){ return "Set"; } public static String classify(List<?> s){ return "List"; } public static String classify(Collection<?> s){ 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”,實際上卻列印了“Unknown Collection”三次
原因是classify方法被重載了,而要調用哪個重載方法是在編譯時間做出決定的,上例中的for (Collection<?> c : collections) 會導致每次調用的都是classify(Collection<?>)這個重載方法
而被覆蓋的方法(override)的選擇則是依據被呼叫者法所在對象的運行時類型
對於重載,安全而保守的策略是,永遠不要匯出兩個具有相同參數數目的重載方法 第42條:慎用可變參數
可變參數方法接受0個或者多個指定類型的參數,可變參數機制通過先建立一個數組,數組的大小為在調用位置所傳遞的參數數量,然後將參數值傳到數組中,最後將數組傳遞給方法
假設確定對某個方法95%的調用會有3個或者更少的參數,就聲明方法的5個重載,每個重載方法帶有0至3個普通參數,當參數的數目超過3個時,就使用一個可變參數方法
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) {}
第43條:返回零長度的數組或者集合,而不是null
對於一個返回null而不是零長度數組或者集合的方法,幾乎每次用到該方法時都需要額外的代碼來處理null傳回值 第44條:為所有匯出的API元素編寫文檔注釋
為了正確地編寫API文檔,必須在每個被匯出的類、介面、構造器、方法和域聲明之前增加一個文檔注釋
方法的文檔注釋應該簡潔地描述出它和用戶端之間的約定,說明這個方法做了什麼,每個參數都應該有一個@param標籤,以及一個@return標籤(除非傳回型別為void),以及對於該方法拋出的每個異常,無論是受檢的還是未受檢的,都有一個@throws標籤,如果方法啟動了後台線程,文檔中也應該說明這一點 通用程式設計 第45條:將局部變數的範圍最小化
將局部變數的範圍最小化,可以增強代碼的可讀性和可維護性,並降低出錯的可能性
要使局部變數的範圍最小化,最有利的方法就是在第一次使用它的地方聲明 第46條:for-each迴圈優先於傳統的for迴圈
for-each迴圈通過完全隱藏迭代器或者索引變數,避免了混亂和出錯的可能,並且沒有效能損失
有三種常見的情況無法使用for-each迴圈: 1.過濾——如果需要遍曆集合,並刪除選定的元素,就需要使用顯式的迭代器,以便可以調用它的remove方法 2.轉換——如果需要遍曆列表或者數組,並取代它部分或者全部的元素值,就需要列表迭代器或者數組索引,以便設定元素的值 3.平行迭代——如果需要並行地遍曆多個集合,就需要顯式地控制迭代器或者索引變數,以便所有迭代器或者索引變數都可以得到同步前移 第47條:瞭解和使用類庫
通過使用標準類庫,可以充分利用這些編寫標準類庫的專家的知識,以及其他人的使用經驗
程式員應該把時間花在應用程式上,而不是底層的細節上 第48條:如果需要精確的答案,請避免使用float和double
float和double類型主要是為了科學計算和工程計算而設計的,它們執行二進位浮點運算,是為了在廣泛的數值範圍上提供較為精確的快速近似計算而精心設計的。因此,它們並沒有提供完全精確的結果,所以不應該被用於需要精確結果的場合
如
System.out.println(1.00 - 0.42);
的結果為:0.5800000000000001
解決這個問題的正確辦法是使用BigDecimal、int或者long
BigDecimal a = new BigDecimal(1.00);BigDecimal b = new BigDecimal(0.42);System.out.println(a.subtract(b));// 輸出結果為 0.58
使用BigDecimal有兩個缺點:與使用基本運算類型相比,這樣做很不方便,而且很慢
除了使用BigDecimal之外,還有一種辦法是使用int或者long,並自己記錄十進位小數點,如果數值範圍沒有超過9位十進位數字,就可以使用int;如果不超過18位元字,就可以使用long;如果數值可能超過18位元字,就必須使用BigDecimal 第49條:基本類型優先於裝箱基本類型
基本類型(int、double、boolean)和裝箱基本類型(Integer、Double、Boolean)之間有三個主要區別 第一,基本類型只有值,而裝箱基本類型則具有與它們的值不同的同一性 第二,基本類型只有功能完備的值,而每個裝箱基本類型還有個非功能值:null 第三,基本類型通常比裝箱基本類型更節省時間和空間
當在一項操作中混合使用基本類型和裝箱基本類型時,裝箱基本類型就會自動拆箱
應該使用裝箱基本類型的情況: 作為集合中的元素、鍵和值List<Integer> 第50條:如果其他類型更合適,則盡量避免使用字串 字串不適合代替其他的實值型別,如int、boolean 字串不適合代替枚舉類型
字串不適合代替聚集類型
String compoundKey = className + "#" + i.next();// 這種方法有許多缺點,如為了訪問單獨的域,必須解析該字串。更好的做法是,簡單地編寫一個類來描述這個資料集,通常是一個私人的靜態成員類
字串也不適合代替能力表(字串被用於對某種功能進行授權訪問) 第51條:當心字串串連的效能
字串串連操作符(+)是把多個字串合并為一個字串的便利途徑,但它不適合運用在大規模的情境中。為串連n個字串而重複地使用字串串連操作符,需要n的平方級的時間。這是因為兩個字串被串連在一起時,它們的內容都要被拷貝
建議使用StringBuilder替代String 第52條:通過介面引用對象
如果有合適的介面類型存在,那麼對於參數、傳回值、變數和域來說,就都應該使用介面類型進行聲明
如果對象的基本類型是類,不是介面,應該用相關的基類(往往是抽象類別)來引用這個對象,而不是用它的實作類別 第53條:介面優先於反射機制
核心反射機制java.lang.reflect,提供了“通過程式來訪問關於已裝載的類的資訊”的能力。給定一個Class執行個體,可以獲得Constructor、Method和Field執行個體
反射機制(reflection)允許一個類使用另一個類,即使當前者被編譯的時候後者還根本不存在。然而,這種能力也要付出代價: 喪失了編譯時間類型檢查的好處,包括異常檢查 執行反射訪問所需要的代碼非常笨拙和冗長 效能損失,反射方法調用比普通方法調用慢了許多
通常,普通應用程式在運行時不應該以反射方式訪問對象
對於有些程式,它們必須用到在編譯時間無法擷取的類,但是在編譯時間存在適當的介面或者超類,通過它們可以引用這個類。這種情況,就可以以反射方式建立執行個體,然後通過它們的介面或者超類,以正常的方式訪問這些執行個體,如下例
public static void main(String[] args) { // Translate the class name into a Class object Class<?> cl = null; try { cl = Class.forName(args[0]); } catch (ClassNotFoundException e) { System.err.println("Class not found."); System.exit(1); } // Instantiate the class Set<String> s = null; try { s = (Set<String>) cl.newInstance(); } catch (IllegalAccessException e) { System.err.println("Class not accessible."); System.exit(1); } catch (InstantiationException e) { System.err.println("Class not instantiable."); System.exit(1); } // Exercise the set s.addAll(Arrays.asList(args).subList(1, args.length)); System.out.println(s);}
上述程式建立了一個Set<String>執行個體,它的類是由第一個命令列參數指定的,該程式把其餘的命令列參數插入到這個集合中
這個程式可以很容易地變成一個通用的集合測試器,通過侵入式地操作一個或者多個集合執行個體,並檢查是否遵守Set介面的約定,以此來驗證指定的Set實現。絕大多數情況下,使用反射機制時需要的正是這種方法
反射機制是一種功能強大的機制,對於特定的複雜系統編程任務,它是非常必要的,如有可能,就應該僅僅使用反射機制來執行個體化對象,而訪問對象時則使用編譯時間已知的某個介面或者超類 第54條:謹慎地使用本地方法
使用本地方法來提高效能的做法不值得提倡
使用本地方法有一些嚴重的缺點,因為本地語言不是安全的,所以使用本地方法的應用程式也不再能免受記憶體毀壞錯誤的影響,在進入和退出本地代碼時,需要相關的固定開銷 第55條:謹慎地進行最佳化
要努力編寫好的程式而不是快的程式,好的程式體現了資訊隱藏的原則:只要有可能,它們就會把設計決策集中在單個模組中
努力避免那些限制效能的設計決策,模組之間互動關係以及模組與外界互動關係的組件都有可能對系統本該達到的效能產生嚴重的限制
要考慮API設計決策的效能後果,如使公有的類型成為可變的,這可能會導致大量不必要的保護性拷貝
效能剖析工具有助於你決定應該把最佳化的重心放在哪裡,這樣的工具可以為你提供運行時的資訊,比如每個方法大致上花費了多少時間、它被調用多少次,甚至還可以警告你是否需要改變演算法 第56條:遵守普遍接受的命名慣例
| 標識符類型 |
例子 |
| 包 |
com.google.inject |
| 類或者介面 |
Timer,FutureTask |
| 方法或者域 |
remove,isDigit,getCrc |
| 常量域 |
MIN_VALUE |
| 局部變數 |
i,xref |
| 型別參數 |
T,E,K,V |