Java 語言看似簡單,實則暗藏無數「坑」。本文彙總 30+ 高頻錯誤場景,涵蓋字符串處理、集合操作、異常處理、並發編程、日期時間、數值計算、I/O 資源管理、序列化、反射、泛型等核心領域。每個場景提供錯誤代碼、問題分析、正確寫法、底層原理四維度解析,並標註可用 FindBugs/SpotBugs 靜態檢測的規則,助你寫出更健壯、高效、可維護的代碼。
// ❌ 錯誤:循環中使用 + 拼接,產生大量 StringBuilder 臨時對象
String s = "";
for (Person p : persons) {
s += ", " + p.getName();
}
s = s.substring(2);
// ✅ 正確:預估容量,使用 StringBuilder
StringBuilder sb = new StringBuilder(persons.size() * 16);
for (Person p : persons) {
if (sb.length() > 0) sb.append(", ");
sb.append(p.getName());
}
String result = sb.toString();
原理:+ 編譯為 new StringBuilder().append(...).toString(),循環中每次創建新對象,導致 O(N²) 字符複製。預分配容量避免擴容複製。
// ❌ 單線程環境無需同步
StringBuffer sb = new StringBuffer();
sb.append("Name: ").append(name).append("\n!");
// ✅ JDK 5+ 單線程首選 StringBuilder
StringBuilder sb = new StringBuilder(100); // 預分配
sb.append("Name: ").append(name).append("\n!");
// 或簡單場景直接用 +
String s = "Name: " + name + "\n!";
原理:StringBuffer 所有方法 synchronized,單線程下產生無謂鎖開銷。StringBuilder 非線程安全,效能更高。
// ❌ 不推薦
if (name.compareTo("John") == 0) ...
if (name == "John") ... // 引用比較,極易出錯
if (name.equals("John")) ... // name 為 null 拋 NPE
if ("".equals(name)) ... // 可行但語意不清
// ✅ 推薦
if ("John".equals(name)) ... // 常量在前,防 NPE
if (name != null && name.equals("John")) ... // 顯式非空
if (name.isEmpty()) ... // JDK 6+ 判空首選
if (name.length() == 0) ... // 等價寫法
// ❌ 低效
String s = "" + count;
String s = new Integer(count).toString();
// ✅ 高效、語意清晰
String s = String.valueOf(count); // 內部調用 Integer.toString(count)
String s = Integer.toString(count); // 直接調用
// ❌ 每次創建新對象
Integer zero = new Integer(0);
Boolean flag = Boolean.valueOf("true"); // 解析字符串
// ✅ 使用靜態工廠方法,利用緩存池 (-128~127)
Integer zero = Integer.valueOf(0);
Boolean flag = Boolean.TRUE; // 或 Boolean.valueOf(true)
// ❌ 字符串操作 XML:脆弱、不處理轉義、命名空間、編碼
int start = xml.indexOf("") + 6;
int end = xml.indexOf(" ");
String name = xml.substring(start, end);
// ✅ 使用成熟解析器
// JDOM2
SAXBuilder builder = new SAXBuilder();
Document doc = builder.build(new StringReader(xml));
String name = doc.getRootElement().getChildText("name");
// DOM4J / XPath
// JAXB 綁定對象
// ❌ 手工拼接:易產生非法 XML、編碼問題
String xml = "" + name + " ";
// ✅ 使用 XML 輸出器
Element root = new Element("root");
root.setAttribute("att", attr);
root.setText(name);
Document doc = new Document(root);
XMLOutputter out = new XMLOutputter(Format.getPrettyFormat());
String xml = out.outputString(doc);
// ❌ 直接讀文件為 String:忽略 XML 聲明編碼,內存浪費
String xml = FileUtils.readTextFile("data.xml");
// ✅ 使用解析器直接處理 InputStream,自動識別編碼
SAXBuilder builder = new SAXBuilder();
Document doc = builder.build(new FileInputStream("data.xml")); // 自動檢測
// ❌ 依賴平台預設編碼,跨平台亂碼
Reader r = new FileReader(file);
Writer w = new FileWriter(file);
String s = new String(bytes);
byte[] b = str.getBytes();
// ✅ 顯式指定編碼 (推薦 UTF-8)
Reader r = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
Writer w = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8);
String s = new String(bytes, StandardCharsets.UTF_8);
byte[] b = str.getBytes(StandardCharsets.UTF_8);
// ❌ 單字節讀寫:極慢,頻繁 JNI 呼叫
InputStream in = new FileInputStream(file);
while ((b = in.read()) != -1) { ... }
// ✅ 必須加緩衝
InputStream in = new BufferedInputStream(new FileInputStream(file));
OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
// 實測 1MB 讀取:無緩衝 ~1000ms,有緩衝 ~10ms (提升 99%)
// ❌ 文件過大導致 OOM
byte[] pdf = toPdf(file); // 假設 500MB
// ✅ 流式處理,邊讀邊寫
File pdfFile = toPdfFile(file); // 返回臨時文件
// 或使用管道流、異步處理
// ❌ 無限阻塞
Socket socket = new Socket();
socket.connect(address);
InputStream in = socket.getInputStream();
in.read(); // 永久等待
// ✅ 設置連接與讀取超時
socket.connect(address, 20_000); // 20秒連接超時
socket.setSoTimeout(15_000); // 15秒讀取超時
// 文件 I/O 無直接超時 API,建議:異步 + Future.get(timeout) / 線程池監控
// ❌ 循環中重複創建
for (Entity e : entities) {
e.setUpdated(new Date()); // 每次本地調用
}
// ✅ 批量操作共享時間戳
long now = System.currentTimeMillis();
for (Entity e : entities) {
e.setUpdated(new Date(now));
}
// ✅ 高頻場景:定時器更新共享時間戳 (精度 10ms)
volatile long timeStamp = System.currentTimeMillis();
Timer timer = new Timer(true);
timer.scheduleAtFixedRate(new TimerTask() {
public void run() { timeStamp = System.currentTimeMillis(); }
}, 0, 10);
// 使用: new Date(timeStamp) 避免對象創建
// ❌ 依賴 Locale 選擇曆法,可能非 Gregorian
Calendar c = Calendar.getInstance();
c.set(2024, Calendar.JANUARY, 1);
// ✅ 顯式指定曆法與時區
Calendar c = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
c.set(2024, Calendar.JANUARY, 1);
// ❌ 靜態共享:多線程並發解析/格式化會產生錯誤結果
public static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd");
// ✅ 方案 1:ThreadLocal
private static final ThreadLocal TL_SDF = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd"));
// ✅ 方案 2:每次 new (簡單場景性能可接受)
// ✅ 方案 3:JDK 8+ DateTimeFormatter (線程安全)
private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// ❌ 加 8 小時用 Calendar 且設時區:多此一舉
Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("Europe/Zurich"));
cal.setTime(date);
cal.add(Calendar.HOUR_OF_DAY, 8);
date = cal.getTime();
// ✅ 直接毫秒運算 (加小時/分鐘/秒)
date = new Date(date.getTime() + 8L * 3600 * 1000);
// ✅ 加天/月/年需 Calendar (考慮夏令時)
Calendar cal = new GregorianCalendar(userTimeZone);
cal.setTime(date);
cal.add(Calendar.DAY_OF_MONTH, 1); // 正確處理 23/25 小時天
date = cal.getTime();
// ❌ 企圖轉換時區但實際無效
public static Date convertTz(Date date, TimeZone tz) {
Calendar cal = Calendar.getInstance();
cal.setTimeZone(TimeZone.getTimeZone("UTC"));
cal.setTime(date);
cal.setTimeZone(tz); // 只是改變解釋方式,毫秒值不變
return cal.getTime(); // 返回同一毫秒值
}
// ✅ 理解:Date = UTC 毫秒值,無時區信息
// 顯示時才應用時區:DateTimeFormatter.withZone(zone).format(instant)
// ❌ 吞噬所有異常,掩蓋真實錯誤
try { p = q.getSingleResult(); } catch (Exception e) { p = null; }
// ✅ 精確捕獲預期異常
try { p = q.getSingleResult(); } catch (NoResultException e) { p = null; }
// ❌ 記錄無堆棧、繼續執行
try { doStuff(); } catch (Exception e) { log.fatal("Failed"); }
doMoreStuff(); // 危險!
// ✅ 包裝拋出,保留堆棧
try { doStuff(); } catch (Exception e) {
throw new BusinessException("Do stuff failed: " + e.getMessage(), e);
}
// ❌ 雙重包裝
try { ... } catch (Exception e) { throw new RuntimeException(e); }
// ✅ 區分 RuntimeException 與 Checked Exception
try { ... }
catch (RuntimeException e) { throw e; } // 直接重拋
catch (Exception e) { throw new RuntimeException(e.getMessage(), e); } // 包裝
// ❌ 丟失堆棧
throw new RuntimeException();
throw new RuntimeException(e.toString()); // 依賴 toString
throw new RuntimeException(e.getMessage()); // 丟失嵌套原因
// ✅ 保留完整鏈路
throw new RuntimeException(e.getMessage(), e); // cause 鏈路完整
// ❌ 多層捕獲重複記錄
try { ... } catch (ExceptionA e) { log.error(e.getMessage(), e); throw e; }
catch (ExceptionB e) { log.error(e.getMessage(), e); throw e; }
// ✅ 統一在頂層/邊界記錄,中間層只包裝拋出
// ❌ is 關閉失敗導致 os 不關閉
try { is = new FileInputStream(in); os = new FileOutputStream(out); }
finally {
try { is.close(); os.close(); } catch (IOException ignored) {}
}
// ✅ 分別獨立關閉
finally {
try { if (is != null) is.close(); } catch (IOException ignored) {}
try { if (os != null) os.close(); } catch (IOException ignored) {}
}
// ✅ JDK 7+ try-with-resources (自動關閉、異常抑制)
try (InputStream is = new FileInputStream(in);
OutputStream os = new FileOutputStream(out)) {
// 處理邏輯
}
// ❌ 空 catch 塊
try { risky(); } catch (SomeException e) { /* never happens */ }
// ✅ 快速失敗,保留信息
try { risky(); } catch (SomeException e) {
throw new IllegalStateException("Impossible: " + e.getMessage(), e);
}
// ❌ 非靜態 transient 日誌:反序列化後為 null
class A implements Serializable {
private transient Log log = LogFactory.getLog(getClass());
public void f() { log.debug("enter"); } // NPE!
}
// ✅ 靜態 final 或方法局部變量
class A implements Serializable {
private static final Log LOG = LogFactory.getLog(A.class);
public void f() { LOG.debug("enter"); }
}
// 或
public void f() { Log log = LogFactory.getLog(getClass()); log.debug("enter"); }
// ❌ 單線程用 Vector/Hashtable
List list = new Vector<>();
for (...) list.add(item);
// ✅ 非線程安全場景用 ArrayList/HashMap
List list = new ArrayList<>();
// 多線程需同步時:Collections.synchronizedList / ConcurrentHashMap
| 操作 | ArrayList | LinkedList |
|---|---|---|
| 末尾添加 | O(1) 攤銷 | O(1) |
| 中間插入/刪除 | O(n) 需移動 | O(n) 需遍歷定位 |
| 隨機訪問 | O(1) | O(n) |
| 遍歷 | O(n) 緩存友好 | O(n) 緩存不友好 |
結論:95% 場景首選 ArrayList,僅在極頻繁頭尾操作且不需隨機訪問時考慮 LinkedList。
// ❌ 直接用 size:負載因子 0.75 觸發擴容
Map map = new HashMap<>(collection.size());
// ✅ 考慮負載因子:size / 0.75 + 1
Map map = new HashMap<>((int) (collection.size() / 0.75f) + 1);
// 或 Guava: Maps.newHashMapWithExpectedSize(size)
每個 Entry 對象包含 key、value、hash、next 引用,HashSet 內部實為 HashMap。大數據量考慮:
IdentityHashMap:用 == 比較、System.identityHashCode,內部用 Object[],節省 Entry 開銷// ❌ 固定長度、頻繁遍歷、基本類型裝箱
List codes = new ArrayList<>();
codes.add(10); codes.add(20); codes.add(30); codes.add(40);
// ✅ 基本類型數組
int[] codes = {10, 20, 30, 40};
// ✅ 對象固定集合
String[] names = {"A", "B", "C"};
// ✅ volatile 禁止指令重排
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) instance = new Singleton();
}
}
return instance;
}
}
// 或更簡單:靜態內部類 / 枚舉單例
// ❌ float/double 精度丟失
float total = 0f;
for (OrderLine l : lines) total += l.price * l.count; // 29.999997
double a = 1.14 * 75; // 85.4999...
System.out.println(Math.round(a)); // 85 (錯!)
// ✅ BigDecimal 精確計算
BigDecimal total = BigDecimal.ZERO;
for (OrderLine l : lines) {
BigDecimal price = new BigDecimal(String.valueOf(l.price)); // 必須用 String 構造
BigDecimal count = new BigDecimal(l.count);
total = total.add(price.multiply(count));
}
total = total.setScale(2, RoundingMode.HALF_UP);
// ✅ BigDecimal 構造陷阱
BigDecimal d = new BigDecimal(1.14); // 錯!二進制近似值
BigDecimal d = new BigDecimal("1.14"); // 正確
// ❌ 直接截斷
public int getFileSize(File f) { return (int) f.length(); }
// ✅ 檢查溢出
public int getFileSize(File f) {
long len = f.length();
if (len > Integer.MAX_VALUE) throw new IllegalStateException("File > 2GB");
return (int) len;
}
// ❌ 運算優先級陷阱
long a = System.currentTimeMillis();
long b = a + 100;
System.out.println((int) b - a); // 錯:先轉型再減
System.out.println((int) (b - a)); // 正確
// ❌ 依賴 GC 不可預測調用
protected void finalize() { backingStore.close(); }
// ✅ 顯式 close() + try-with-resources / AutoCloseable
public class Resource implements AutoCloseable {
private FileChannel channel;
public void close() throws IOException { if (channel != null) channel.close(); }
}
// 使用:
try (Resource r = new Resource()) { ... }
// ❌ interrupted() 會清除中斷狀態
while (true) { if (Thread.interrupted()) break; }
// ✅ isInterrupted() 不清除
while (true) { if (Thread.currentThread().isInterrupted()) break; }
// ✅ 睡眠中斷正確處理
try { Thread.sleep(1000); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 重置標誌
// ❌ 類加載時由不確定線程創建 Timer 線程
class Cache { private static final Timer evictor = new Timer(); }
// ✅ 延遲初始化,由調用者控制線程上下文
class Cache {
private static Timer evictor;
public static void setupEvictor() { evictor = new Timer(); }
}
// ❌ 內部類引用外部實例,5分鐘後才可 GC
TimerTask task = new TimerTask() { public void run() { callback.timeout(); } };
timer.schedule(task, 300_000);
try { doSomething(); } finally { task.cancel(); }
// ✅ 靜態內部類 + 手動釋放引用
static class Job extends TimerTask {
private MyClass callback;
Job(MyClass c) { callback = c; }
public boolean cancel() { callback = null; return super.cancel(); }
public void run() { if (callback != null) callback.timeout(); }
}
// ❌ 默認值即為 0/null/false
class B { int count = 0; String name = null; boolean flag = false; }
// ✅ 省略賦值
class B { int count; String name; boolean flag; }
// ❌ 接口定義常量:實現類強依賴、污染命名空間
interface Constants { String VERSION = "1.0"; String DATE_FMT = "yyyy-MM-dd"; }
// ✅ 內部類/配置類/枚舉封裝
class AppConstants {
static final String VERSION = "1.0";
static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
}
// ❌ 創建實例判斷:構造器可能拋異常、無無參構造、性能差
if (clazz.newInstance() instanceof Target) ...
// ✅ Class.isAssignableFrom
if (Target.class.isAssignableFrom(clazz)) ...
// ❌ 參數/返回過度具體
void notify(Person p) { sendMail(p.getName(), p.getFirstName(), p.getEmail()); }
String lookup(String id) { return employee.getPhone(); }
// ✅ 面向接口/對象傳遞
void notify(Person p) { sendMail(p); }
Employee lookup(String id) { return employee; } // 返回對象而非單一屬性
// ❌ Setter 含業務邏輯:getter/setter 不一致、副作用隱蔽
void setName(String name) { this.name = name.trim(); }
// ✅ 業務層處理,Setter 純賦值
person.setName(input.getText().trim());
| 類別 | 核心原則 | 推薦工具 |
|---|---|---|
| 字符串 | 循環拼接用 StringBuilder、常量在前防 NPE、預分配容量 | SpotBugs, SonarQube |
| 集合 | 優先 ArrayList/HashMap、指定初始容量、避免裝箱 | SpotBugs, PMD |
| 異常 | 精確捕獲、包裝保留 cause、try-with-resources、不吞異常 | SpotBugs, Error Prone |
| 並發 | 最小化同步、volatile/DCL、ThreadLocal、避免靜態啟動線程 | ThreadSanitizer, jstack |
| 日期 | JDK 8+ java.time、DateTimeFormatter 線程安全、時區顯式 | SpotBugs |
| 數值 | 金額用 BigDecimal(String)、浮點不比較 ==、檢查溢出 | Error Prone |
| I/O | 必須緩衝、顯式編碼、流式處理大文件、設置超時 | SpotBugs |
| 資源 | AutoCloseable、避免 finalize、TimerTask 釋放引用 | SpotBugs |
內化這些最佳實踐,配合靜態分析工具 (SpotBugs/PMD/Error Prone) 集成到 CI 流程,能在編譯期攔截 80%+ 低級錯誤,讓 Code Review 專注於業務邏輯與架構設計。