資深 Java 開發者總結的五大高發錯誤:Null 濫用、異常吞噬、並發修改異常、違反契約、原始類型誤用。本文深度解析每類錯誤的成因、危害、修正方案與現代 Java 最佳實踐,助你寫出更健壯、可維護的代碼。
List<String> accountIds = person.getAccountIds();
for (String accountId : accountIds) {
processAccount(accountId);
}
當 getAccountIds() 返回 null 時,增強 for 循環拋出 NullPointerException。
1. 返回空集合而非 null
public List<String> getAccountIds() {
return accounts != null ? accounts : Collections.emptyList();
}
2. 使用 Optional (Java 8+)
// 返回 Optional
public Optional<List<String>> getAccountIds() {
return Optional.ofNullable(accounts);
}
// 調用端
person.getAccountIds().ifPresent(ids -> {
ids.forEach(this::processAccount);
});
// 或鏈式處理單值
Optional<String> opt = Optional.ofNullable(nullableString);
opt.ifPresent(System.out::println); // Java 8 風格
// opt.ifPresentOrElse(...); // Java 9+ 支援 else 分支
優勢:編譯期強制處理空值、語義明確、避免 NPE、支援函數式鏈式操作。
try {
selfie.show();
} catch (NullPointerException e) {
// 什麼都不做,假裝沒事發生
}
log.error("操作失敗", e);throw new BusinessException("顯示失敗", e);
try {
selfie.delete();
} catch (NullPointerException unimportant) {
// 允許刪除不存在的對象,無需處理
}
List<IHat> hats = new ArrayList<>();
hats.add(new Ushanka()); // 有耳罩
hats.add(new Fedora());
hats.add(new Sombrero());
// 錯誤:遍歷時直接移除
for (IHat hat : hats) {
if (hat.hasEarFlaps()) {
hats.remove(hat); // ConcurrentModificationException!
}
}
| 方案 | 代碼示例 | 優點 | 缺點 |
|---|---|---|---|
| 收集後再刪除 |
List<IHat> toRemove = new ArrayList<>();
for (IHat hat : hats) {
if (hat.hasEarFlaps()) toRemove.add(hat);
}
hats.removeAll(toRemove);
| 簡單直觀 | 額外記憶體 |
| Iterator.remove() |
Iterator<IHat> it = hats.iterator();
while (it.hasNext()) {
if (it.next().hasEarFlaps()) it.remove();
}
| 標準做法、無額外集合 | 僅支援刪除 |
| ListIterator |
ListIterator<IHat> it = hats.listIterator();
while (it.hasNext()) {
if (it.next().hasEarFlaps()) {
it.set(new Sombrero()); // 替換
}
}
| 支援 add/set/remove 完整操作 | 僅適用 List |
| Stream 過濾 (Java 8+) |
hats = hats.stream()
.filter(hat -> !hat.hasEarFlaps())
.collect(Collectors.toCollection(ArrayList::new));
| 函數式、不可變風格、線程安全 | 生成新列表、大數據量記憶體壓力 |
| List.removeIf (Java 8+) |
hats.removeIf(IHat::hasEarFlaps); | 最簡潔、底層用 Iterator.remove | 僅支援刪除 |
| 並發集合 |
List<IHat> hats = new CopyOnWriteArrayList<>(); // 可安全遍歷時修改 | 無需同步、遍歷不拋異常 | 寫入複製開銷大、適合讀多寫少 |
// 錯誤!
List<IHat> filtered = hats.stream()
.peek(hat -> { if (hat.hasEarFlaps()) hats.remove(hat); })
.collect(Collectors.toList());
// 正確:先過濾收集,再賦值
hats = hats.stream()
.filter(hat -> !hat.hasEarFlaps())
.collect(Collectors.toCollection(ArrayList::new));
核心規則:
a.equals(b) == true,則 a.hashCode() == b.hashCode() 必須成立hashCode() 相等,equals() 可真可偽
public static class Boat {
private String 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 Objects.equals(name, boat.name);
}
@Override
public int hashCode() {
return (int) (Math.random() * 5000); // 每次調用返回隨機值!
}
}
// 測試
Set<Boat> boats = new HashSet<>();
boats.add(new Boat("Enterprise"));
System.out.println(boats.contains(new Boat("Enterprise"))); // 極大概率 false!
結果:HashSet、HashMap、HashTable 等基於雜湊的集合完全失效,無法正確查找、去重。
finalize() 由 GC 調用,調用時間不可預測AutoCloseable,使用 try-with-resources
List list = new ArrayList(); // 原始類型
list.add(10);
list.add("Twenty"); // 編譯通過,運行時炸彈
list.forEach(n -> System.out.println((Integer)n * 2)); // ClassCastException!
List<Integer> list = new ArrayList<>(); // 參數化類型
list.add(10);
list.add("Twenty"); // 編譯錯誤:類型不匹配,提前發現問題
| 錯誤類型 | 核心對策 | 關鍵 API/語法 |
|---|---|---|
| Null 濫用 | 返回空集合/Optional | Collections.emptyList(), Optional |
| 異常吞噬 | 記錄/轉換/說明 | log.error, 自定義異常 |
| 並發修改 | Iterator/Stream/並發集合 | Iterator.remove(), removeIf, CopyOnWriteArrayList |
| 違反契約 | 正確實作 equals/hashCode | Objects.equals, Objects.hash, IDE 自動生成 |
| 原始類型 | 始終使用泛型 | List<T>, Map<K,V> |
避開這五大坑,你的 Java 代碼將更加健壯、專業、易維護。建議配合 SpotBugs、SonarQube 等靜態分析工具,在 CI 流程中自動檢測這類問題。