注:本文是《Effective Java》學習的筆記。
本片敘述如何處理參數和返回值,如何設計方法簽名,如何爲方法編寫文檔。
49.檢查參數的有效性
大多數方法和構造器對於傳遞給它們的參數值都會有某些限制。例如,索引值必須是非負數,對象引用不能爲null,等等。
應該在文檔中清楚的指明這些限制,並且在方法體的開頭處檢查參數,以強制施加這些限制。
如上是發生錯誤之後儘快檢測出錯誤的原則。
在Java7中新增了Objects.requireNonNull方法比較靈活且方便,因此不必因手工進行null檢查。也可以用它的重載方法,增加了異常拋出說明。
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
50.必要時進行保護性拷貝
假設類的客戶端會盡其所能來破壞這個類的約束條件,因此你必須保護性的設計程序。
以日期類爲例。date 是一個可變的類。 simpleDateFormat 這個類也是一個多線程存在不安全問題的類。
所以以上這一套日期操作,在java8完全可以被替換掉。使用LocalDateTime(當前時間,沒劃分時區)
Instant(有時區劃分,默認格林尼治時間) ZonedDateTime(有時區劃分) 來替換date
使用 DateTimeFormatter 來代替 SimpleDateFormat 下面是一個小demo
Date已經過時了,不應該在新代碼中使用。
下面是一個Date通過構造方法保護實例,防止被修改的Demo
@ToString
@Getter
@AllArgsConstructor
public final class DateTest { //111111
private final Date start;
private final Date end;
public static void main(String[] args) {
Date end = new Date(1990,1,1);
DateTest dateTest = new DateTest(new Date(1970,1,1),end);
end.setYear(1000);
System.out.println(dateTest);
//DateTest(start=Tue Feb 01 00:00:00 CST 3870, end=Mon Feb 01 00:00:00 CST 2900)
}
}
@Getter
public final class DateTest { //2222222
private final Date start;
private final Date end;
public DateTest(Date start,Date end){
this.start = new Date(start.getTime()); //內層重新創建實例。
this.end = new Date(end.getTime());
}
}
11111是沒修改之前的類,可見end.setYear();就可以輕鬆破壞掉類的實例。
2222是修改之後的類,內層重新創建實例可以保證外層修改end 當前實例不會被破壞
對於構造器的每個可變參數進行保護性拷貝是必要的。
!!注:保護性拷貝是在檢查參數的有效性之前進行的,並且有效性檢查是針對拷貝之後的對象,而不是針對原始的對象。
上述沒有使用clone進行對象的克隆是因爲date不是final的 使用克隆有被Date子類破壞掉的風險。
51.謹慎設計方法簽名
謹慎的選擇方法的名稱,要符合命名規範。每個公司都有命名規範,默認遵循阿里的命名規範了。
不要過於追求提供便利的方法。
避免過長的參數列表。 相同類型的長參數序列格外有害。
對於參數類型,要優先使用接口而不是類。
對於boolean參數,要優先使用兩個元素的枚舉類型。這樣方便後續修改。比如語義是大爲true 小爲false
那麼可以設計一個枚舉 BIG,SMALL 這樣後續需要修改時再在枚舉中添加就好了。親測好用。
52.慎用重載
要調用哪個重載方法是在編譯時做出決定的。
對於重載方法的選擇是靜態的,而對於被重寫的方法的選擇則是動態的。
public static String test(Set<?> s);
public static String test(List<?> s);
public static String test(Collection<?> s); 這三個重載的方法若是調用時泛型時Collection 則只會調用第三個而不會調用前兩個,這算是一個小坑吧。
上述的重載可以用 instanceof代替。
public static String test(Colletion<?> c){ return c instanceof List ? "list" ...... }
安全而保守的策略是,永遠不要導出兩個具有相同參數數目的重載方法。
始終可以給方法起不同的名稱,而不是使用重載機制。
綜上,儘量不用、也沒有太大好處。就是方法名一樣而已。
53.慎用可變參數
這條建議,我幾乎沒有使用過可變參數。沒覺得帶來很多好處。參數不固定時確實是一個可行的使用場景。
54.返回零長度的數組或者集合,而不是null
有的人任務null返回值 比零長度集合或者數組更好,因爲避免額分配零長度的容器所帶來的開銷。我之前就是這個想法。
這個觀點是站不住腳的。原因有兩點。第一:在這個級別上擔心性能問題是不明智的,除非分析表明這個方法正是造成性能問題的真正源頭。第二:不需要分配零長度的集合或者數組,也可以返回它們。
public List<String> get(){ return new ArrayList<>(someList); } 也可以像下面這兒樣的返回空list map啥的、也可以像數組那樣,事先定義好一個空集合供返回。
public static List<String> get(){
return Collections.emptyList();
}
關於數組也是這麼玩的。返回一個零長度的數組而不是null
永遠都不要返回null,而不返回一個零長度的數組或者集合。如果返回null,那樣會使API更難以使用,也更容易出錯,而且沒有任何性能優勢。
55.謹慎返回Optinal
Java8之前,要編寫一個在特定環境下無法返回任何值的方法是,有兩種方法:要麼拋出異常,要麼返回null.
上述兩種異常的代價很高,而返回null就像埋下了一個地雷。客戶端編碼缺少校驗很容易NPE
第三種編寫不能返回值的方法是 使用 Optional
理論上能返回T的方法,實踐中也可能無法返回,因此在某些特定的條件下,可以改爲聲明返回Optional
private static Optional<?> getArr(String arr){
return arr == null? Optional.empty():Optional.of(arr);
}
System.out.println(getArr("asd").isPresent()); //true
System.out.println(getArr("asd").get()); //asd
永遠不要通過返回Optional的方法返回null Optional本質上與受檢異常類似。
Stream有很多終止操作就是返回的Optional
如果方法返回Optional ,客戶端必須做出選擇:如果該方法不能返回值時應該採取什麼動作。可以指定一個缺省值。
Stream.of("a","b","c").max((o1,o2)->o2.length()-o1.length()).orElse("other words");
max().get()就獲取到了這個Stream的結果。
Optional有一個 isPresent 方法可以判斷是否存在值。Boolean類型返回結果。
當使用Stream編程時,經常會遇到Stream<Optional<T>> ,爲了推動進程還需要一個包含了非空optional中所有元素的Stream<T>. 可以通過過濾器 Optional::isPresent 來解決。
容器類型包括集合、映射、Stream、數組和Optional,都不應該被包裝在Optional中。
Optional<T>的使用場景如下。
如果無法返回結果並且當沒有返回結果時客戶端必須執行特殊的處理,那麼就應該聲明該方法返回Optional<T>
optional不適應於注重性能的情況。 永遠不應該返回基本類型的optional (int long double)
幾乎永遠都不適合用optional作爲鍵、值,或者集合或數組中的元素。而是隻作爲一個返回結果。
總之:如果發現自己在編寫的方法始終無法返回值,並且相信該方法的用戶每次在調用它時都要考慮到這種可能性,那麼或許就應該返回一個optional 。但是,應當注意到與返回optional相關的真實的性能影響;對於注重性能的方法,最好是返回一個null,或者拋出異常。最後,不要講optional用作返回值以外的其他用途。
56.爲所有導出的API元素編寫文檔註釋
爲了正確的編寫API文檔,必須在每個被導出的類、接口、構造器、方法和域聲明之間增加一個文檔註釋。
方法的文檔註釋應該簡潔的描述出它和客戶端之間的約定。
當爲泛型或者方法編寫文檔時,確保要在文檔中說明所有的類型參數。
當爲枚舉類型編寫文檔時,要確保在文檔中說明常量。
爲註解類型編寫文檔時,要確保在文檔中說明說要成員。
類或者靜態方法是否線程安全,應該在文檔中對它的線程安全級別進行說明。
57.將局部變量得作用域最小化
要使局部變量的作用域最小化,最有力的方法就是在第一次要使用它的地方進行聲明。
對於局部變量的初始化操作,必須在下面馬上使用,否則會讓程序變得更難以理解。這對內存等資源來說也是提前佔用的。是一個不好的編程習慣。
需要注意一下for和while的循環條件的局部變量有效範圍。for循環的循環變量是在循環條件時聲明的。而while是可以在外部聲明循環變量的。
使方法小而集中。
58.foreach循環優先於傳統的for循環。
foreach形式的循環是要比for以及迭代器iterator更易讀懂的。而性能是差不多的。前期是你正確迭代使用。
59.瞭解和使用類庫。
使用Random. nextInt來代替 random. nextInt 前者是靜態方法,後者是實例方法。
從java7開始就不應該再使用Random了。現在選擇隨機數生成器時,大多使用ThreadLocalRandom 它會產生更高質量的隨機數。
對於併發使用,fork 並行流則使用splittableRandom
對於新特性的學習是很重要的。讓我們更少的重複造輪子。lang util io包中的特性尤其重要。
對於一些常規操作,工具類中已經有了實現。如果上述java包中沒有,那麼可以去牛逼點的第三方類庫尋求方法,比如Guava
60. 如果需要精確的答案,請避免使用float和double
float double尤其不適合用於貨幣計算。
其表示的數值會出現0.7999999999998這種而不是0.8
對於需要精確表示的數值請使用BigDecimal 或者根據數值的長度使用int long
BigDecimal肯定是相對來說有點麻煩的。但是更加精確。BigDecimal也會使性能有一些下降。
61.基本類型優先於裝箱基本類型。
如果你只是想表達一個數,而不使用null及包裝類型纔有的操作,那就使用基本類型。因爲基本類型要比裝箱類型更快。
要明確裝箱類型與基本類型的區別。比==和equals對於這兩者時的不同。
當在一項操作中混合使用基本類型和裝箱基本類型時,裝箱基本類型都會自動拆箱。
而需要注意一下null的自動拆箱會觸發npe
注意!!!一定要避免程序進行多次沒有必要的反覆拆箱裝箱操作。
62.如果其他類型更合適,則儘量避免使用字符串。
能用其他類型表示就使用其他類型表示,字符串表示一些東西會造成程序的複雜混亂。尤其注意不要用字符串來存儲時間!!!非常噁心。
63.瞭解字符串連接的性能。
字符串連接string 大家都知道每個+都意味着新對象的生成。超級佔內存。非常噁心。禁止大量字符串拼接+這麼玩。
字符串string類是final的,保證string的不可變。
此處需要了解string stringbuffer stringbuilder的區別。
如果大量字符串拼接請使用stringbuilder,涉及到資源競爭的字符串拼接使用stringbuffer,其他不需要字符串拼接的場景再使用string
64.通過接口引用對象。
如果有合適的接口類型存在,那麼對於參數,返回值,變量和域來說,就都應該使用接口類型進行聲明。
接口是一種多態的表現形式。這個事情還是要看應用場景。編程過程中就自然而然得寫好了。
65.接口優先於反射機制
反射有一些缺點
損失了編譯時類型檢查的優勢。
執行反射訪問所需要的代碼非常笨拙和冗長。
性能損失。
對於編譯時不能確定的可以使用反射來實現。
66.謹慎的使用本地方法。
本地方法是使用java以外得語言來編寫的。不見得就比java自身的實現性能高,安全。所以除非必要,否則不要使用本地方法。本地方法也有被java重寫過的比如BigInteger在java3之前就是c寫的。後來java重寫性能更好了。
所以本地方法可能會導致預期之外的錯誤。要小心使用。
67.謹慎的進行優化。
不要因爲性能優化而破壞點程序結構。
避免設計有侷限的程序。侷限是要可控的而不是被動被侷限住的。
68.遵守普遍接受的命名慣例。
這個在有些場景是被配置的,你就必須叫這個名稱纔會被注入。
約定高於配置。
對於命名我覺得遵從阿里巴巴的代碼規範就挺不錯的。idea也有對於代碼的format
69.只針對異常的情況才使用異常。
異常應該只用於異常的情況下;他們永遠不應該用於正常的控制流。
考慮使用optional返回值,或者返回一個可識別的值,比如null
70.對可恢復的情況使用受檢異常,對編程錯誤使用運行時異常。
如果期望調用者能夠適當的恢復,對於這種情況就應該使用受檢異常。比如密碼錯誤你想捕獲到這個異常並且想讓程序更加清晰,使用受檢異常,編輯器會時刻提醒你來處理這個異常。
如果程序拋出未受檢異常或者錯誤,往往就屬於不可恢復的情形,繼續執行下去有害無益。
除非不得已,否則不要試圖通過異常來實現邏輯處理。
你實現的所有未受檢的拋出結構都應該是RuntimeException的子類。不僅不應該定義Error的子類,甚至也不應該拋出AssertionError異常。
不要拋出非受檢異常,這隻會困擾API的用戶。
71.避免不必要的使用受檢異常
受檢異常如果使用得當,它們可以改善API和程序。但是大量使用受檢異常會使API使用起來非常不方便。
拋出受檢異常的方法不能直接在Stream流中使用。
一種好的代替拋出受檢異常的方式是使用optional
尤其在stream流中更有效。
此時需要考慮的是optional 返回沒有什麼說明,而異常會附有一些信息。但是異常需要我們每個catch到的時候做出響應。
總而言之,在謹慎使用得前提之下,受檢異常可以提升程序的可讀性,如果過度使用,將會使API使用起來非常痛苦。如果調用者無法恢復失敗,就應該拋出未受檢異常。如果可以恢復,並且想要迫使調用者處理異常的條件,首選應該返回一個optional值。當且僅當萬一失敗時,這些無法提供足夠的信息,才應該拋出受檢異常。
72.優先使用標準的異常。
使用java標準類庫提供的受檢異常來實現代碼複用。這使程序更加輕便也更加易讀。
比如IllegalArgumentException不合法的參數異常。IllegalStateException如果因爲接收到的對象狀態而使調用非法。比如調用未經初始化的對象。
還有IndexOutOfBoundsException, ConcurrentModificationException,UnsupportedOperationException等等
不要直接重用Exception RuntimeException Throwable Error這些頂級父類。因爲可能會導致一些預期之外的問題。因爲類庫中的異常也是繼承於上述頂級父類的。
注意異常類是可以序列化的,因此這也是除非不得已,否則不要自己編寫異常類的原因之一。
73.拋出與抽象對應的異常。
更高層的實現應該捕獲低層的異常,同時拋出可以按照高層抽象進行解釋的異常。這種做法稱作異常轉譯。一種特殊的異常轉譯形式稱爲異常鏈。
總而言之,如果不能阻止或者處理來自更低層的異常,一般的做法是使用異常轉譯。只有在低層方法的規範碰巧可以保證它所拋出非所有異常對於更高層也是合適的情況下。纔可以將異常從低層傳播到高層。異常鏈對高層和低層異常都提供了最佳的性能。他允許拋出適當的高層異常,同時又能捕獲低層的原因進行失敗分析。
74.每個方法拋出非所有異常都要建立文檔。
文檔對於別人理解你的程序很關鍵。寫文檔是一個好的習慣。
75.在細節信息中包含失敗!捕獲信息。
在msg中註明異常參數對於調試起來非常方便。
爲了捕獲異常,異常的細節信息應該包含對該異常有貢獻的所有參數和域的值。
當然在細節信息中不要包含密碼,密鑰以及類似的信息。
一種使異常類很好的打印細節信息的方式是,異常類的構造方法入參可以設定需要傳入的細節信息值,然後再使用super將值format到父類異常。來實現補充異常信息。當然再throw的時候做這種操作也是可以的,只不過沒有構造方法的方式好。要時刻關注代碼的複用來降低程序的複雜性。
76.努力使失敗保持原子性。
一般而言,失敗的方法調用應該使對象保持在被調用之前的狀態。
77.不要忽略異常。
空的catch塊會使異常達不到應有的目的。
如果選擇忽略異常,catch塊中應該包含一條註釋,說明爲什麼可以這麼做,並且變量應該命名爲ignored