程式碼層面的最佳化是效能最佳化的基礎,本章涵蓋字串、正則、集合、迴圈等常見最佳化技巧。

字串最佳化#

String 的實現演進#

JDK 版本實現方式特點
Java 6 及之前char[] + offset + count可能記憶體洩漏
Java 7-8char[]解決洩漏問題
Java 9+byte[] + coder節省記憶體(Latin-1 字元只需 1 byte)

String 的不可變性#

String 被 final 修飾,且內部字元陣列也是 final。這種不可變性帶來三個好處:

  1. 安全性:防止字串被惡意修改
  2. Hash 穩定:保證 HashMap 等容器正常工作
  3. 字串常數池:減少重複物件創建

字串拼接最佳化#

// 不推薦:迴圈中使用 + 拼接
String str = "";
for (int i = 0; i < 1000; i++) {
    str = str + i;  // 每次創建新的 StringBuilder
}

// 推薦:顯式使用 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i);
}
String str = sb.toString();

雖然編譯器會自動將 + 最佳化為 StringBuilder,但在迴圈中每次都會創建新實體。顯式使用 StringBuilder 可避免此問題。

String.intern() 最佳化記憶體#

適用場景:大量重複字串(如地理位置資訊)

// Twitter 案例:32GB → 幾百 MB
SharedLocation location = new SharedLocation();
location.setCity(city.intern());          // 使用常數池
location.setCountryCode(countryCode.intern());
location.setRegion(region.intern());

intern() 使用的是 HashTable 結構,資料量過大會影響查詢效能。需結合實際場景評估。

字串分割最佳化#

// 不推薦:直接使用 split()(內部使用正則)
String[] parts = str.split(",");

// 推薦:簡單分隔符使用 indexOf
int idx = str.indexOf(",");
String part1 = str.substring(0, idx);
String part2 = str.substring(idx + 1);

// 或使用 StringTokenizer
StringTokenizer st = new StringTokenizer(str, ",");

正則表達式最佳化#

正則的效能陷阱#

正則表達式使用 NFA(非確定有限狀態自動機)引擎,可能引發回溯問題

// 問題案例:貪婪匹配導致回溯
String text = "abbc";
String regex = "ab{1,3}c";  // 匹配 abc, abbc, abbbc

// 匹配過程:
// 1. a 匹配 a ✓
// 2. b{1,3} 貪婪匹配 bb ✓,繼續嘗試匹配 c
// 3. b{1,3} 嘗試匹配 c ✗,發生回溯
// 4. 回退一個 b,b{1,3} 匹配 bb
// 5. c 匹配 c 

三種匹配模式#

模式語法特點
貪婪模式X{1,3}盡可能多匹配,可能回溯
懶惰模式X{1,3}?盡可能少匹配,也可能回溯
獨占模式X{1,3}+盡可能多匹配,不回溯

正則最佳化技巧#

// 1. 使用獨占模式避免回溯
String regex = "ab{1,3}+c";

// 2. 減少分支選擇
// 不推薦
String regex1 = "(abcd|abef)";
// 推薦:提取共用前綴
String regex2 = "ab(cd|ef)";

// 3. 使用非捕獲組
// 不需要引用的組使用 (?:)
String regex = "(?:<input.*?>)(.*?)(?:</input>)";

// 4. 預編譯正則表達式
private static final Pattern PATTERN = Pattern.compile("\\d+");

生產環境中因正則回溯導致 CPU 100% 的案例屢見不鮮。能不用正則就不用,必須用時要做好效能測試。

集合選擇與最佳化#

ArrayList vs LinkedList#

操作ArrayListLinkedList
隨機訪問O(1)O(n)
頭部插入O(n)O(1)
尾部插入O(1)*O(1)
中間插入O(n)O(n)

* 無擴容情況下

實測結論

// 遍歷 LinkedList
// 不推薦:for 循環(每次都從頭遍歷)
for (int i = 0; i < list.size(); i++) {
    list.get(i);  // O(n) 操作
}

// 推薦:使用迭代器
for (String s : list) {  // 或使用 Iterator
    // O(1) 操作
}

ArrayList 最佳化技巧#

// 1. 預設初始容量(避免擴容)
List<String> list = new ArrayList<>(1000);

// 2. 批量操作優於單條操作
list.addAll(anotherList);  // 優於循環 add

// 3. 刪除元素時使用迭代器
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (shouldRemove(it.next())) {
        it.remove();  // 安全刪除
    }
}

HashMap 最佳化#

// 1. 設置合理的初始容量和負載因子
// 初始容量 = 預計資料量 / 負載因子
Map<String, Object> map = new HashMap<>(16, 0.75f);

// 2. 良好的 hashCode 實現
// - 均勻分布,減少衝突
// - 計算快速
@Override
public int hashCode() {
    return Objects.hash(field1, field2);
}

HashMap 結構演進(Java 8+)

  • 陣列 + 鏈結串列 + 紅黑樹
  • 鏈結串列長度 > 8 時轉換為紅黑樹
  • 查詢複雜度從 O(n) 降為 O(log n)

Stream API 最佳化#

何時使用 Stream#

場景建議
資料量小傳統迭代更快
資料量大 + 多核 CPU並行 Stream 優勢明顯
單核 CPU傳統迭代更快

Stream 效能實測#

// 串行 Stream
list.stream()
    .filter(s -> s.startsWith("A"))
    .map(String::toUpperCase)
    .collect(Collectors.toList());

// 並行 Stream(大資料量 + 多核有優勢)
list.parallelStream()
    .filter(s -> s.startsWith("A"))
    .map(String::toUpperCase)
    .collect(Collectors.toList());

並行 Stream 注意事項:

  1. 避免使用非線程安全的集合作為目標
  2. 避免在 forEach 中修改共享變數
  3. I/O 密集型操作不適合使用並行 Stream

Stream 最佳化原理#

Stream 採用延遲執行機制:

資料源 → filter → map → sorted → collect
   │                                  │
   └── 中間操作(懶執行)──────────────┘
                               終結操作觸發執行

物件創建最佳化#

減少物件創建#

// 1. 重用不可變物件
Boolean flag = Boolean.TRUE;  // 不要 new Boolean(true)
Integer num = Integer.valueOf(100);  // 使用快取 (-128~127)

// 2. 使用物件池
// StringBuilder、Connection 等重量級物件

// 3. 避免在迴圈中創建物件
// 不推薦
for (int i = 0; i < 1000; i++) {
    String s = new String("test");
}

// 推薦
String s = "test";
for (int i = 0; i < 1000; i++) {
    // 使用 s
}

單例模式最佳化#

// 推薦:使用枚舉實現單例
public enum Singleton {
    INSTANCE;

    public void doSomething() {
        // ...
    }
}

// 使用
Singleton.INSTANCE.doSomething();

迴圈最佳化#

常見迴圈最佳化技巧#

// 1. 減少迴圈內計算
// 不推薦
for (int i = 0; i < list.size(); i++) { }

// 推薦
int size = list.size();
for (int i = 0; i < size; i++) { }

// 2. 提前退出
for (Item item : items) {
    if (found(item)) {
        break;  // 找到就退出
    }
}

// 3. 避免在迴圈中進行例外處理
// 不推薦
for (String s : list) {
    try {
        process(s);
    } catch (Exception e) {
        // ...
    }
}

// 推薦
try {
    for (String s : list) {
        process(s);
    }
} catch (Exception e) {
    // ...
}

總結#

最佳化領域核心要點
字串StringBuilder、intern()、避免 split()
正則獨占模式、預編譯、非捕獲組
集合選對容器、預設容量、迭代器遍歷
Stream大資料量+多核用並行,否則傳統迭代
物件減少創建、重用、物件池
迴圈減少計算、提前退出、外移例外處理
程式碼最佳化檢查清單
  • 字串拼接是否使用 StringBuilder
  • 正則表達式是否有回溯風險
  • 集合是否選擇了合適的類型
  • 集合是否設置了初始容量
  • 是否有不必要的物件創建
  • 迴圈是否有最佳化空間