下面針對每一個錯誤用文字說明結合代碼詳解的方式展示給大家,具體內容如下:
1. Null 的過度使用
避免過度使用null 值是一個最佳實踐。例如,更好的做法是讓方法返回空的array 或者collection 而不是null 值,因為這樣可以防止程序拋出NullPointerException。下面代碼片段會從另一個方法獲得一個集合:
List<String> accountIds = person.getAccountIds(); for (String accountId : accountIds) { processAccount(accountId);}
當一個person 沒有account 的時候,getAccountIds() 將返回null 值,程序就會拋出NullPointerException 異常。因此需要加入空檢查來解決這個問題。如果將返回的null 值替換成一個空的list,那麼NullPointerException 也不會出現。而且,因為我們不再需要對變量accountId 做空檢查,代碼將變得更加簡潔。
當你想避免null 值的時候,不同場景可能採取不同做法。其中一個方法就是使用Optional 類型,它既可以是一個空對象,也可以是一些值的封裝。
Optional<String> optionalString = Optional.ofNullable(nullableString); if(optionalString.isPresent()) { System.out.println(optionalString.get());}
事實上,Java8 提供了一個更簡潔的方法:
Optional<String> optionalString = Optional.ofNullable(nullableString); optionalString.ifPresent(System.out::println);
Java 是從Java8 版本開始支持Optional 類型,但是它在函數式編程世界早已廣為人知。在此之前,它已經在Google Guava 中針對Java 的早期版本被使用。
2. 忽視異常
我們經常對異常置之不理。然而,針對初學者和有經驗的Java 程序員,最佳實踐仍是處理它們。異常拋出通常是帶有目的性的,因此在大多數情況下需要記錄引起異常的事件。別小看這件事,如果必要的話,你可以重新拋出它,在一個對話框中將錯誤信息展示給用戶或者將錯誤信息記錄在日誌中。至少,為了讓其它開發者知曉前因後果,你應該解釋為什麼沒有處理這個異常。
selfie = person.shootASelfie(); try { selfie.show();} catch (NullPointerException e) { // Maybe, invisible man. Who cares, anyway?}
強調某個異常不重要的一個簡便途徑就是將此信息作為異常的變量名,像這樣:
複製代碼代碼如下:
try { selfie.delete(); } catch (NullPointerException unimportant) { }
3. 並發修改異常
這種異常發生在集合對像被修改,同時又沒有使用iterator 對象提供的方法去更新集合中的內容。例如,這裡有一個hats 列表,並想刪除其中所有含ear flaps 的值:
List<IHat> hats = new ArrayList<>(); hats.add(new Ushanka()); // that one has ear flaps hats.add(new Fedora()); hats.add(new Sombrero()); for (IHat hat : hats) { if (hat.hasEarFlaps()) { hats.remove(hat); }}
如果運行此代碼,ConcurrentModificationException 將會被拋出,因為代碼在遍歷這個集合的同時對其進行修改。當多個進程作用於同一列表,在其中一個進程遍歷列表時,另一個進程試圖修改列表內容,同樣的異常也可能會出現。
在多線程中並發修改集合內容是非常常見的,因此需要使用並發編程中常用的方法進行處理,例如同步鎖、對於並發修改採用特殊的集合等等。 Java 在單線程和多線程情況下解決這個問題有微小的差別。
收集對象並在另一個循環中刪除它們
直接的解決方案是將帶有ear flaps 的hats 放進一個list,之後用另一個循環刪除它。不過這需要一個額外的集合來存放將要被刪除的hats。
List<IHat> hatsToRemove = new LinkedList<>(); for (IHat hat : hats) { if (hat.hasEarFlaps()) { hatsToRemove.add(hat); }}for (IHat hat : hatsToRemove) { hats.remove (hat);}
使用Iterator.remove 方法
這個方法更簡單,同時並不需要創建額外的集合:
Iterator<IHat> hatIterator = hats.iterator(); while (hatIterator.hasNext()) { IHat hat = hatIterator.next(); if (hat.hasEarFlaps()) { hatIterator.remove(); }}
使用ListIterator 的方法
當需要修改的集合實現了List 接口時,list iterator 是非常合適的選擇。實現ListIterator 接口的iterator 不僅支持刪除操作,還支持add 和set 操作。 ListIterator 接口實現了Iterator 接口,因此這個例子看起來和Iterator 的remove 方法很像。唯一的區別是hat iterator 的類型和我們獲得iterator 的方式――使用listIterator() 方法。下面的片段展示瞭如何使用ListIterator.remove 和ListIterator.add 方法將帶有ear flaps 的hat 替換成帶有sombreros 的。
IHat sombrero = new Sombrero(); ListIterator<IHat> hatIterator = hats.listIterator(); while (hatIterator.hasNext()) { IHat hat = hatIterator.next(); if (hat.hasEarFlaps()) { hatIterator.remove (); hatIterator.add(sombrero); }}
使用ListIterator,調用remove 和add 方法可替換為只調用一個set 方法:
IHat sombrero = new Sombrero(); ListIterator<IHat> hatIterator = hats.listIterator(); while (hatIterator.hasNext()) { IHat hat = hatIterator.next(); if (hat.hasEarFlaps()) { hatIterator.set (sombrero); // set instead of remove and add }}
使用Java 8中的stream 方法
在Java8 中,開發人員可以將一個collection 轉換為stream,並且根據一些條件過濾stream。這個例子講述了stream api 是如何過濾hats 和避免ConcurrentModificationException 。 hats = hats.stream().filter((hat -> !hat.hasEarFlaps()))
複製代碼代碼如下:
.collect(Collectors.toCollection(ArrayList::new));
Collectors.toCollection 方法將會創建一個新的ArrayList,它負責存放被過濾掉的hats 值。如果過濾條件過濾掉了大量條目,這裡將會產生一個很大的ArrayList。因此,需要謹慎使用。
使用Java 8 中的List.removeIf 方法
可以使用Java 8 中另一個更簡潔明了的方法―― removeIf 方法:
複製代碼代碼如下:
hats.removeIf(IHat::hasEarFlaps);
在底層,它使用Iterator.remove 來完成這個操作。
使用特殊的集合
如果在一開始就決定使用CopyOnWriteArrayList 而不是ArrayList ,那就不會出現問題。因為CopyOnWriteArrayList 提供了修改的方法(例如set,add,remove),它不會去改變原始集合數組,而是創建了一個新的修改版本。這就允許遍歷原來版本集合的同時進行修改,從而不會拋出ConcurrentModificationException 異常。這種集合的缺點也非常明顯――針對每次修改都產生一個新的集合。
還有其他適用於不同場景的集合,比如CopyOnWriteSet 和ConcurrentHashMap 。
關於另一個可能可能在並發修改集合時產生的錯誤是,從一個collection 創建了一個stream,在遍歷stream 的時候,同時修改後端的collection。針對stream 的一般準則是,在查詢stream 的時候,避免修改後端的collection。接下來的例子將展示如何正確地處理stream:
List<IHat> filteredHats = hats.stream().peek(hat -> { if (hat.hasEarFlaps()) { hats.remove(hat); }}).collect(Collectors.toCollection(ArrayList::new)) ;
peek 方法收集所有的元素,並對每一個元素執行既定動作。在這裡,動作即為嘗試從一個基礎列表中刪除數據,這顯然是錯誤的。為避免這樣的操作,可以嘗試一些上面講解的方法。
4. 違約
有時候,為了更好地協作,由標準庫或者第三方提供的代碼必須遵守共同的依賴準則。例如,必須遵守hashCode 和equals 的共同約定,從而保證Java 集合框架中的一系列集合類和其它使用hashCode 和equals 方法的類能夠正常工作。不遵守約定並不會產生exception 或者破壞代碼編譯之類的錯誤;它很陰險,因為它隨時可能在毫無危險提示的情況下更改應用程序行為。
錯誤代碼可能潛入生產環境,從而造成一大堆不良影響。這包括較差的UI 體驗、錯誤的數據報告、較差的應用性能、數據丟失或者更多。慶幸的是,這些災難性的錯誤不會經常發生。在之前已經提及了hashCode 和equals 約定,它出現的場景可能是:集合依賴於將對象進行哈希或者比較,就像HashMap 和HashSet。簡單來說,這個約定有兩個準則:
如果兩個對象相等,那麼hash code 必須相等。
如果兩個對像有相同的hash code,那麼它們可能相等也可能不相等。
破壞約定的第一條準則,當你試圖從一個hashmap 中檢索數據的時候將會導致錯誤。第二個準則意味著擁有相同hash code 的對像不一定相等。
下面看一下破壞第一條準則的後果:
public static class Boat { private String name; Boat(String name) { this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Boat boat = (Boat) o; return !(name != null ? !name.equals(boat.name) : boat.name != null); } @ Override public int hashCode() { return (int) (Math.random() * 5000); }}
正如你所見,Boat 類重寫了equals 和hashCode 方法。然而,它破壞了約定,因為hashCode 針對每次調用的相同對象返回了隨機值。下面的代碼很可能在hashset 中找不到一個名為Enterprise 的boat,儘管事實上我們提前加入了這種類型的boat:
public static void main(String[] args) { Set<Boat> boats = new HashSet<>(); boats.add(new Boat("Enterprise")); System.out.printf("We have a boat named ' Enterprise' : %b/n", boats.contains(new Boat("Enterprise")));}
另一個約定的例子是finalize 方法。這裡是官方Java 文檔關於它功能描述的引用:
finalize 的常規約定是:當JavaTM 虛擬機確定任何線程都無法再通過任何方式訪問指定對象時,這個方法會被調用,此後這個對像只能在某個其他(準備終止的)對像或類終結時被作為某個行為的結果。 finalize 方法有多個功能,其中包括再次使此對像對其他線程可用;不過finalize 的主要目的是在不可撤消地丟棄對象之前執行清除操作。例如,表示輸入/輸出連接對象的finalize 方法可執行顯式I/O 事務,以便在永久丟棄對象之前中斷連接。
你可以決定在諸如文件處理器中使用finalize 方法來釋放資源,但是這種用法是很糟糕的。由於它是在垃圾回收期間被調用的,而GC 的時間並不確定,因此finalize 被調用的時間將無法保證。
5. 使用原始類型而不是參數化的
根據Java 文檔描述:原始類型要么是非參數化的,要么是類R 的(同時也是非繼承R 父類或者父接口的)非靜態成員。在Java 泛型被引入之前,並沒有原始類型的替代類型。 Java 從1.5版本開始支持泛型編程,毫無疑問這是一個重要的功能提升。然而,由於向後兼容的原因,這裡存在一個陷阱可能會破壞整個類型系統。著眼下例:
List listOfNumbers = new ArrayList(); listOfNumbers.add(10); listOfNumbers.add("Twenty"); listOfNumbers.forEach(n -> System.out.println((int) n * 2));
這是一個由數字組成的列表被定義為原始的ArrayList。由於它並沒有指定類型參數,因此可以給它添加任何對象。但是最後一行將其包含的元素映射為int 類型並乘以2,打印出翻倍之後的數據到標準輸出。
此代碼編譯時不會出錯,但是一旦運行就會拋出運行時錯誤,因為這裡試圖將字符類型映射為整形。很顯然,如果隱藏了必要信息,類型系統將不能幫助寫出安全代碼。
為了解決這個問題,需要為存入集合中的對象指定具體類型:
List<Integer> listOfNumbers = new ArrayList<>();listOfNumbers.add(10); listOfNumbers.add("Twenty");listOfNumbers.forEach(n -> System.out.println((int) n * 2)) ;
與之前代碼的唯一差別即是定義集合的那一行:
複製代碼代碼如下:
List<Integer> listOfNumbers = new ArrayList<>();
修改之後的代碼編譯不可能被通過,因為這裡試圖向只期望存儲整形的集合中添加字符串。編譯器將會顯示錯誤信息,並指向試圖向列表中添加Twenty 字符的那一行。參數化泛型類型是個不錯的主意。這樣的話,編譯器就能夠檢查所有可能的類型,從而由於類型不一致而導致的運行時異常機率將大大降低。
主要總結了以上五個Java程序員常犯的錯誤,希望大家能夠喜歡。