程式碼不僅要正確,還要高效。本章探討如何編寫經濟的程式碼,在效能、可維護性和開發成本之間取得平衡。
為什麼需要經濟的程式碼?#
效能的價值#
程式碼的效能直接影響:
- 用戶體驗
- 運營成本
- 系統容量
- 業務競爭力
效能不只是技術問題#
效能問題 = 意識問題 + 見識問題 + 技術問題
80% 15% 5%大多數效能問題不是因為技術難度高,而是因為:
- 沒有意識到需要考慮效能
- 不知道有更好的解決方案
- 沒有養成效能導向的思維
什麼時候考慮效能?#
越早越好,而不是越晚越好。
不要相信:「我們只有一萬個用戶,不需要考慮效能。」
問自己:
- 這一萬用戶會同時訪問嗎?
- 會有惡意攻擊嗎?
- 程式碼帶來的絕對價值有多大?
高效程式碼的思考框架#
效能的本質#
程式碼的效能不是計算速度,而是資源管理:
- 記憶體
- CPU
- 磁碟 I/O
- 網路 I/O
效能最佳化的層次#
架構層面(影響 80%)
│
└── 選擇正確的技術堆疊和架構
演演算法層面(影響 15%)
│
└── 選擇合適的資料結構和演演算法
程式碼層面(影響 5%)
│
└── 微觀最佳化先在高層次最佳化,再考慮低層次最佳化。
避免過度設計#
過度設計的表現#
- 為不存在的需求預先設計
- 過度抽象和封裝
- 引入不必要的中間層
- 盲目應用設計模式
YAGNI 原則#
You Ain’t Gonna Need It - 你不會需要它
// 過度設計:目前只有一種支付方式
interface PaymentStrategy { }
class PaymentStrategyFactory { }
class AbstractPaymentHandler { }
class WechatPaymentStrategy implements PaymentStrategy { }
// 簡單直觀:等需要時再擴展
class PaymentService {
void pay(Order order) {
wechatApi.pay(order);
}
}平衡擴展性與簡單性#
預留擴展點,但不提前實作:
// 使用介面,但不提前實作多個實作類別
public interface ConfigSource {
String getValue(String key);
}
// 目前只有一個實作
public class RedisConfigSource implements ConfigSource {
// ...
}
// 需要時再添加
// public class ZookeeperConfigSource implements ConfigSource { }簡單直觀的設計#
簡單是最高的智慧#
越是能用簡單的方法解決複雜的問題,越能體現一個人的能力。
介面設計原則#
1. 參數少而精
// 不好:參數過多
void createUser(String name, String email, String phone,
String address, String city, String country);
// 好:使用參數物件
void createUser(UserCreateRequest request);2. 命名直觀
// 不好:需要查文件才知道做什麼
void process(int type, String data, int flag);
// 好:自解釋
void sendEmailNotification(String recipientEmail, String message);3. 行為可預測
// 不好:有副作用
int getCount() {
count++; // 副作用!
return count;
}
// 好:純函式
int getCount() {
return count;
}記憶體使用最佳化#
物件創建的成本#
Java 物件創建涉及:
- 記憶體分配
- 建構函式執行
- 垃圾回收壓力
最佳化策略#
1. 物件池化
// 頻繁創建銷毀的物件考慮池化
ExecutorService executor = Executors.newFixedThreadPool(10);
// 資料庫連線池
DataSource dataSource = new HikariDataSource(config);2. 避免不必要的物件創建
// 不好:每次呼叫都創建新物件
public boolean isValidEmail(String email) {
return email.matches("^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$");
// matches() 每次都編譯正則表達式
}
// 好:預編譯
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$");
public boolean isValidEmail(String email) {
return EMAIL_PATTERN.matcher(email).matches();
}3. 使用基本型別
// 不好:不必要的裝箱
Integer count = 0;
for (int i = 0; i < 1000000; i++) {
count = count + 1; // 大量裝箱拆箱
}
// 好:使用基本型別
int count = 0;
for (int i = 0; i < 1000000; i++) {
count++;
}4. 選擇合適的集合
| 場景 | 推薦集合 |
|---|---|
| 隨機存取 | ArrayList |
| 頻繁插入刪除 | LinkedList |
| 去重 | HashSet |
| 有序去重 | TreeSet |
| 鍵值對 | HashMap |
| 有序鍵值對 | TreeMap |
| 執行緒安全 | ConcurrentHashMap |
延遲分配的兩面性#
什麼是延遲分配?#
只在真正需要時才分配資源。
// 立即分配
private List<Item> items = new ArrayList<>();
// 延遲分配
private List<Item> items;
public List<Item> getItems() {
if (items == null) {
items = new ArrayList<>();
}
return items;
}延遲分配的好處#
- 節省資源 - 不使用的物件不創建
- 加快啟動 - 延後初始化時間
延遲分配的問題#
- 執行緒安全 - 需要額外同步
- 增加複雜度 - 空值檢查
- 隱藏問題 - 初始化例外延後發現
除非有明確的效能需求,否則優先使用立即分配。
非同步事件處理#
為什麼使用非同步?#
同步處理:
用戶請求 → 處理 A → 處理 B → 處理 C → 回應
[===============================]
等待時間長
非同步處理:
用戶請求 → 處理 A → 回應
└→ 非同步處理 B
└→ 非同步處理 C
[========]
快速回應常見非同步模式#
1. 執行緒池
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> {
// 非同步任務
sendEmail(user);
});2. 訊息佇列
// 發送訊息
messageQueue.send(new OrderCreatedEvent(orderId));
// 消費者處理
@MessageListener
void handleOrderCreated(OrderCreatedEvent event) {
// 處理訂單創建事件
}3. CompletableFuture
CompletableFuture.supplyAsync(() -> fetchUserInfo(userId))
.thenApply(user -> enrichUserData(user))
.thenAccept(user -> saveToCache(user))
.exceptionally(ex -> {
logger.error("Error", ex);
return null;
});非同步的注意事項#
- 例外處理更複雜
- 除錯困難
- 需要考慮重試和冪等性
效能陷阱#
常見效能陷阱#
1. N+1 查詢
// 不好:N+1 次資料庫查詢
List<Order> orders = orderDao.findAll();
for (Order order : orders) {
User user = userDao.findById(order.getUserId()); // N 次查詢
}
// 好:批次查詢
List<Order> orders = orderDao.findAll();
Set<Long> userIds = orders.stream()
.map(Order::getUserId)
.collect(Collectors.toSet());
Map<Long, User> users = userDao.findByIds(userIds); // 1 次查詢2. 字串拼接
// 不好:大量字串拼接
String result = "";
for (int i = 0; i < 10000; i++) {
result += i; // 每次創建新字串
}
// 好:使用 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String result = sb.toString();3. 同步塊過大
// 不好:整個方法同步
public synchronized void process() {
// 非關鍵程式碼
prepareData();
// 關鍵程式碼
updateSharedState();
// 非關鍵程式碼
cleanup();
}
// 好:最小化同步範圍
public void process() {
prepareData();
synchronized (this) {
updateSharedState();
}
cleanup();
}4. 不當的快取使用
// 不好:無限制快取
Map<String, Object> cache = new HashMap<>();
// 好:使用有容量限制的快取
LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> loadFromDb(key));可持續發展的程式碼#
技術債務#
不良的設計決策會累積「技術債務」,需要在未來「償還」。
當前開發速度 技術債務增加
↓ ↓
快速 ────────→ 長期維護成本高
│ │
└── vs ───────────────┘
│ │
穩健 ────────→ 長期維護成本低
↓ ↓
當前開發較慢 技術債務少平衡策略#
- 持續重構 - 不讓技術債務累積
- 自動化測試 - 保護重構安全
- 程式碼審查 - 及早發現問題
- 文件化決策 - 記錄為什麼這樣設計
盡量不寫程式碼#
最好的程式碼是不需要寫的程式碼。
複用現有解決方案#
- 使用標準庫 - 不要重複造輪子
- 使用開源框架 - 站在巨人肩膀上
- 使用雲服務 - 不自建基礎設施
減少程式碼量的方法#
// 冗長
if (str != null && !str.isEmpty()) {
// ...
}
// 簡潔(使用工具類別)
if (StringUtils.isNotEmpty(str)) {
// ...
}
// 冗長
List<String> names = new ArrayList<>();
for (User user : users) {
names.add(user.getName());
}
// 簡潔(使用 Stream)
List<String> names = users.stream()
.map(User::getName)
.collect(Collectors.toList());經濟程式碼檢查清單#
效能檢查清單
設計層面
- 選擇了合適的架構
- 沒有過度設計
- 預留了擴展點
記憶體
- 避免不必要的物件創建
- 使用合適的集合類型
- 設定合理的初始容量
- 及時釋放大物件
I/O
- 使用批次操作
- 使用快取
- 考慮非同步處理
- 避免 N+1 問題
並行
- 最小化同步範圍
- 使用適當的鎖
- 考慮無鎖方案
程式碼品質
- 使用標準庫而非自己實作
- 程式碼簡潔清晰
- 有適當的測試覆蓋
總結#
經濟的程式碼 = 正確 + 高效 + 可維護
- 效能意識 - 從設計階段就考慮效能
- 適度最佳化 - 不過度設計,不過早最佳化
- 持續改進 - 通過持續重構保持程式碼品質
- 權衡取捨 - 在效能、可讀性、開發成本之間平衡