首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 開發常見坑點與最佳實踐大全:字符串、集合、異常、並發、日期、數值等 30+ 核心場景深度解析

     編輯:Java 相關知識

前言

Java 語言看似簡單,實則暗藏無數「坑」。本文彙總 30+ 高頻錯誤場景,涵蓋字符串處理、集合操作、異常處理、並發編程、日期時間、數值計算、I/O 資源管理、序列化、反射、泛型等核心領域。每個場景提供錯誤代碼、問題分析、正確寫法、底層原理四維度解析,並標註可用 FindBugs/SpotBugs 靜態檢測的規則,助你寫出更健壯、高效、可維護的代碼。

一、字符串處理篇

1. 字符串拼接濫用 [FindBugs: SBSC_USE_STRINGBUFFER_CONCATENATION]

// ❌ 錯誤:循環中使用 + 拼接,產生大量 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²) 字符複製。預分配容量避免擴容複製。

2. StringBuffer 線程安全開銷 [FindBugs: SS_SHOULD_BE_STRINGBUILDER]

// ❌ 單線程環境無需同步
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 非線程安全,效能更高。

3. 字符串相等判斷最佳實踐

// ❌ 不推薦
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) ...       // 等價寫法

4. 數值轉字符串

// ❌ 低效
String s = "" + count;
String s = new Integer(count).toString();

// ✅ 高效、語意清晰
String s = String.valueOf(count); // 內部調用 Integer.toString(count)
String s = Integer.toString(count); // 直接調用

5. 利用不可變對象緩存

// ❌ 每次創建新對象
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 處理篇

6. 手工拼接/解析 XML [FindBugs: XML_PARSING]

// ❌ 字符串操作 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 綁定對象

7. 手工組裝 XML

// ❌ 手工拼接:易產生非法 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);

8. XML 編碼陷阱

// ❌ 直接讀文件為 String:忽略 XML 聲明編碼,內存浪費
String xml = FileUtils.readTextFile("data.xml");

// ✅ 使用解析器直接處理 InputStream,自動識別編碼
SAXBuilder builder = new SAXBuilder();
Document doc = builder.build(new FileInputStream("data.xml")); // 自動檢測 

三、字符編碼與 I/O 篇

9. 未指定字符編碼 [FindBugs: CHARSET_DEFAULT]

// ❌ 依賴平台預設編碼,跨平台亂碼
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);

10. 未緩衝 I/O [FindBugs: UNBUFFERED_IO]

// ❌ 單字節讀寫:極慢,頻繁 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%)

11. 大對象全量讀入內存

// ❌ 文件過大導致 OOM
byte[] pdf = toPdf(file); // 假設 500MB

// ✅ 流式處理,邊讀邊寫
File pdfFile = toPdfFile(file); // 返回臨時文件
// 或使用管道流、異步處理

12. 網絡/文件操作無超時

// ❌ 無限阻塞
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) / 線程池監控

四、日期時間篇

13. 頻繁創建 Date/Calendar [FindBugs: DMI_INVOKING_SYSTEM_CURRENT_TIME_MILLIS]

// ❌ 循環中重複創建
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) 避免對象創建

14. Calendar.getInstance() 隱患

// ❌ 依賴 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);

15. SimpleDateFormat 非線程安全 [FindBugs: STCAL_STATIC_SIMPLE_DATE_FORMAT]

// ❌ 靜態共享:多線程並發解析/格式化會產生錯誤結果
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");

16. 日期時間運算誤區

// ❌ 加 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();

17. 時區調整誤解

// ❌ 企圖轉換時區但實際無效
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)

五、異常處理篇

18. 捕獲過寬異常

// ❌ 吞噬所有異常,掩蓋真實錯誤
try { p = q.getSingleResult(); } catch (Exception e) { p = null; }

// ✅ 精確捕獲預期異常
try { p = q.getSingleResult(); } catch (NoResultException e) { p = null; }

19. 忽略異常/日誌不全

// ❌ 記錄無堆棧、繼續執行
try { doStuff(); } catch (Exception e) { log.fatal("Failed"); }
doMoreStuff(); // 危險!

// ✅ 包裝拋出,保留堆棧
try { doStuff(); } catch (Exception e) {
    throw new BusinessException("Do stuff failed: " + e.getMessage(), e);
}

20. 重複包裝 RuntimeException

// ❌ 雙重包裝
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); } // 包裝

21. 異常信息丟失

// ❌ 丟失堆棧
throw new RuntimeException();
throw new RuntimeException(e.toString()); // 依賴 toString
throw new RuntimeException(e.getMessage()); // 丟失嵌套原因

// ✅ 保留完整鏈路
throw new RuntimeException(e.getMessage(), e); // cause 鏈路完整

22. 重複記錄異常日誌

// ❌ 多層捕獲重複記錄
try { ... } catch (ExceptionA e) { log.error(e.getMessage(), e); throw e; }
catch (ExceptionB e) { log.error(e.getMessage(), e); throw e; }

// ✅ 統一在頂層/邊界記錄,中間層只包裝拋出

23. 資源釋放不徹底

// ❌ 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)) {
    // 處理邏輯
}

24. 捕獲不可能異常卻不處理

// ❌ 空 catch 塊
try { risky(); } catch (SomeException e) { /* never happens */ }

// ✅ 快速失敗,保留信息
try { risky(); } catch (SomeException e) {
    throw new IllegalStateException("Impossible: " + e.getMessage(), e);
}

六、並發與線程安全篇

25. transient 導致反序列化 NPE

// ❌ 非靜態 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"); }

26. 不必要的同步集合

// ❌ 單線程用 Vector/Hashtable
List list = new Vector<>();
for (...) list.add(item);

// ✅ 非線程安全場景用 ArrayList/HashMap
List list = new ArrayList<>();
// 多線程需同步時:Collections.synchronizedList / ConcurrentHashMap

27. List 類型選擇錯誤

操作ArrayListLinkedList
末尾添加O(1) 攤銷O(1)
中間插入/刪除O(n) 需移動O(n) 需遍歷定位
隨機訪問O(1)O(n)
遍歷O(n) 緩存友好O(n) 緩存不友好

結論:95% 場景首選 ArrayList,僅在極頻繁頭尾操作且不需隨機訪問時考慮 LinkedList。

28. HashMap 初始容量陷阱

// ❌ 直接用 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)

29. HashMap/HashSet 內存開銷

每個 Entry 對象包含 key、value、hash、next 引用,HashSet 內部實為 HashMap。大數據量考慮:

  • IdentityHashMap:用 == 比較、System.identityHashCode,內部用 Object[],節省 Entry 開銷
  • 數組/列表 + 線性查找 (小數據量)
  • 第三方高性能庫 (FastUtil, Koloboke, Goldman Sachs Collections)

30. 固定長度列表用數組替代

// ❌ 固定長度、頻繁遍歷、基本類型裝箱
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"};

31. 雙重檢查鎖單例 (DCL) 正確寫法

// ✅ 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;
    }
}
// 或更簡單:靜態內部類 / 枚舉單例

七、數值計算篇

32. 浮點數比較與金額計算

// ❌ 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"); // 正確

33. 整型溢出檢查

// ❌ 直接截斷
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)); // 正確

八、資源管理與其他篇

34. finalize() 誤用 [FindBugs: FI_EXPLICIT_INVOCATION]

// ❌ 依賴 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()) { ... }

35. Thread.interrupted() 清除標誌

// ❌ 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(); } // 重置標誌

36. 靜態初始化創建線程

// ❌ 類加載時由不確定線程創建 Timer 線程
class Cache { private static final Timer evictor = new Timer(); }

// ✅ 延遲初始化,由調用者控制線程上下文
class Cache {
    private static Timer evictor;
    public static void setupEvictor() { evictor = new Timer(); }
}

37. TimerTask 取消後仍持有外部引用

// ❌ 內部類引用外部實例,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(); }
}

38. 不必要的字段初始化

// ❌ 默認值即為 0/null/false
class B { int count = 0; String name = null; boolean flag = false; }

// ✅ 省略賦值
class B { int count; String name; boolean flag; }

39. 靜態常量接口反模式

// ❌ 接口定義常量:實現類強依賴、污染命名空間
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");
}

40. 反射誤用:實例化判斷類型

// ❌ 創建實例判斷:構造器可能拋異常、無無參構造、性能差
if (clazz.newInstance() instanceof Target) ...

// ✅ Class.isAssignableFrom
if (Target.class.isAssignableFrom(clazz)) ...

41. 方法過度耦合具體類型

// ❌ 參數/返回過度具體
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; } // 返回對象而非單一屬性

42. Setter 擅自修改業務數據

// ❌ 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 專注於業務邏輯與架構設計。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved