當前位置:才華齋>IT認證>JAVA認證>

Java中最常見的錯誤盤點

JAVA認證 閱讀(2.38W)

在程式設計時,開發者經常會遭遇各式各樣莫名錯誤。近日,Sushil Das在 Geek On Java上列舉了 Java 開發中常見的 5 個錯誤,一起跟yjbys小編來看看吧!

Java中最常見的錯誤盤點

  1、Null 的過度使用

避免過度使用 null 值是一個最佳實踐。例如,更好的做法是讓方法返回空的 array 或者 collection 而不是 null 值,因為這樣可以防止程式丟擲 NullPointerException。下面程式碼片段會從另一個方法獲得一個集合:

List accountIds = ccountIds();

for (String accountId : accountIds) {

processAccount(accountId);

}

當一個 person 沒有 account 的時候,getAccountIds() 將返回 null 值,程式就會丟擲 NullPointerException 異常。因此需要加入空檢查來解決這個問題。如果將返回的 null 值替換成一個空的 list,那麼 NullPointerException 也不會出現。而且,因為我們不再需要對變數 accountId 做空檢查,程式碼將變得更加簡潔。

當你想避免 null 值的時候,不同場景可能採取不同做法。其中一個方法就是使用 Optional 型別,它既可以是一個空物件,也可以是一些值的封裝。

Optional optionalString = llable(nullableString);

if(esent()) {

tln(());

}

事實上,Java8 提供了一個更簡潔的方法:

Optional optionalString = llable(nullableString);

esent(::println);

Java 是從 Java8 版本開始支援 Optional 型別,但是它在函數語言程式設計世界早已廣為人知。在此之前,它已經在 Google Guava 中針對 Java 的早期版本被使用。

  2、忽視異常

我們經常對異常置之不理。然而,針對初學者和有經驗的 Java 程式設計師,最佳實踐仍是處理它們。異常丟擲通常是帶有目的性的,因此在大多數情況下需要記錄引起異常的事件。別小看這件事,如果必要的話,你可以重新丟擲它,在一個對話方塊中將錯誤資訊展示給使用者或者將錯誤資訊記錄在日誌中。至少,為了讓其它開發者知曉前因後果,你應該解釋為什麼沒有處理這個異常。

selfie = tASelfie();

try {

();

} catch (NullPointerException e) {

// Maybe, invisible man. Who cares, anyway?

}

強調某個異常不重要的一個簡便途徑就是將此資訊作為異常的變數名,像這樣:

try { te(); } catch (NullPointerException unimportant) { }

  3、併發修改異常

這種異常發生在集合物件被修改,同時又沒有使用 iterator 物件提供的方法去更新集合中的內容。例如,這裡有一個 hats 列表,並想刪除其中所有含 ear flaps 的值:

List hats = new ArrayList<>();

(new Ushanka()); // that one has ear flaps

(new Fedora());

(new Sombrero());

for (IHat hat : hats) {

if (arFlaps()) {

ve(hat);

}

}

如果執行此程式碼,ConcurrentModificationException 將會被丟擲,因為程式碼在遍歷這個集合的同時對其進行修改。當多個程序作用於同一列表,在其中一個程序遍歷列表時,另一個程序試圖修改列表內容,同樣的異常也可能會出現。

在多執行緒中併發修改集合內容是非常常見的,因此需要使用併發程式設計中常用的方法進行處理,例如同步鎖、對於併發修改採用特殊的集合等等。Java 在單執行緒和多執行緒情況下解決這個問題有微小的差別。

收集物件並在另一個迴圈中刪除它們

直接的解決方案是將帶有 ear flaps 的 hats 放進一個 list,之後用另一個迴圈刪除它。不過這需要一個額外的集合來存放將要被刪除的 hats。

List hatsToRemove = new LinkedList<>();

for (IHat hat : hats) {

if (arFlaps()) {

(hat);

}

}

for (IHat hat : hatsToRemove) {

ve(hat);

}

使用ve方法

這個方法更簡單,同時並不需要建立額外的集合:

Iterator hatIterator = ator();

while (ext()) {

IHat hat = ();

if (arFlaps()) {

ve();

}

}

使用ListIterator的方法

當需要修改的集合實現了 List 介面時,list iterator 是非常合適的選擇。實現 ListIterator 介面的 iterator 不僅支援刪除操作,還支援add和set操作。ListIterator 介面實現了 Iterator 介面,因此這個例子看起來和Iterator的remove方法很像。唯一的區別是 hat iterator 的型別和我們獲得 iterator 的方式——使用listIterator()方法。下面的片段展示瞭如何使用 ve和方法將帶有 ear flaps 的 hat 替換成帶有sombreros 的。

IHat sombrero = new Sombrero();

ListIterator hatIterator = Iterator();

while (ext()) {

IHat hat = ();

if (arFlaps()) {

ve();

(sombrero);

}

}

使用 ListIterator,呼叫remove和add方法可替換為只調用一個set方法:

IHat sombrero = new Sombrero();

ListIterator hatIterator = Iterator();

while (ext()) {

IHat hat = ();

if (arFlaps()) {

(sombrero); // set instead of remove and add

}

}

使用Java 8中的stream方法

在 Java8 中,開發人員可以將一個 collection 轉換為 stream,並且根據一些條件過濾 stream。這個例子講述了 stream api 是如何過濾 hats 和避免ConcurrentModificationException。 hats = am()er((hat -> !arFlaps()))

ect(llection(ArrayList::new));

llection方法將會建立一個新的 ArrayList,它負責存放被過濾掉的 hats 值。如果過濾條件過濾掉了大量條目,這裡將會產生一個很大的 ArrayList。因此,需要謹慎使用。

使用 Java 8 中的veIf 方法

可以使用 Java 8 中另一個更簡潔明瞭的方法—— removeIf方法:

veIf(IHat::hasEarFlaps);

在底層,它使用 ve來完成這個操作。

使用特殊的集合

如果在一開始就決定使用CopyOnWriteArrayList而不是ArrayList,那就不會出現問題。因為 CopyOnWriteArrayList提供了修改的方法(例如 set,add,remove),它不會去改變原始集合陣列,而是建立了一個新的修改版本。這就允許遍歷原來版本集合的`同時進行修改,從而不會丟擲 ConcurrentModificationException異常。這種集合的缺點也非常明顯——針對每次修改都產生一個新的集合。

還有其他適用於不同場景的集合,比如 CopyOnWriteSet和ConcurrentHashMap。

關於另一個可能可能在併發修改集合時產生的錯誤是,從一個 collection 建立了一個 stream,在遍歷 stream 的時候,同時修改後端的 collection。針對 stream 的一般準則是,在查詢 stream 的時候,避免修改後端的 collection。接下來的例子將展示如何正確地處理 stream:

List filteredHats = am()(hat -> {

if (arFlaps()) {

ve(hat);

}

})ect(llection(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) {

= name;

}

@Override

public boolean equals(Object o) {

if (this == o) return true;

if (o == null || getClass() != lass()) return false;

Boat boat = (Boat) o;

return !(name != null ? !ls() : != null);

}

@Override

public int hashCode() {

return (int) (om() * 5000);

}

}

正如你所見,Boat 類重寫了equals和hashCode方法。然而,它破壞了約定,因為 hashCode 針對每次呼叫的相同物件返回了隨機值。下面的程式碼很可能在 hashset 中找不到一個名為Enterprise的boat,儘管事實上我們提前加入了這種型別的 boat:

public static void main(String[] args) {

Set boats = new HashSet<>();

(new Boat("Enterprise"));

tf("We have a boat named 'Enterprise' : %b/n", ains(new Boat("Enterprise")));

}

另一個約定的例子是finalize 方法。這裡是官方 Java 文件關於它功能描述的引用:

finalize的常規約定是:當 JavaTM 虛擬機器確定任何執行緒都無法再通過任何方式訪問指定物件時,這個方法會被呼叫,此後這個物件只能在某個其他(準備終止的)物件或類終結時被作為某個行為的結果。finalize方法有多個功能,其中包括再次使此物件對其他執行緒可用;不過finalize的主要目的是在不可撤消地丟棄物件之前執行清除操作。例如,表示輸入/輸出連線物件的finalize方法可執行顯式 I/O 事務,以便在永久丟棄物件之前中斷連線。