程式碼層面的最佳化是效能最佳化的基礎,本章涵蓋字串、正則、集合、迴圈等常見最佳化技巧。
字串最佳化#
String 的實現演進#
| JDK 版本 | 實現方式 | 特點 |
|---|---|---|
| Java 6 及之前 | char[] + offset + count | 可能記憶體洩漏 |
| Java 7-8 | char[] | 解決洩漏問題 |
| Java 9+ | byte[] + coder | 節省記憶體(Latin-1 字元只需 1 byte) |
String 的不可變性#
String 被
final修飾,且內部字元陣列也是final。這種不可變性帶來三個好處:
- 安全性:防止字串被惡意修改
- Hash 穩定:保證 HashMap 等容器正常工作
- 字串常數池:減少重複物件創建
字串拼接最佳化#
// 不推薦:迴圈中使用 + 拼接
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#
| 操作 | ArrayList | LinkedList |
|---|---|---|
| 隨機訪問 | 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 注意事項:
- 避免使用非線程安全的集合作為目標
- 避免在 forEach 中修改共享變數
- 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
- 正則表達式是否有回溯風險
- 集合是否選擇了合適的類型
- 集合是否設置了初始容量
- 是否有不必要的物件創建
- 迴圈是否有最佳化空間