Java I/O 框架雖強大,但缺乏靈活性:壓縮流只能寫入壓縮格式、無法讀取;第三方模塊常只支援寫入輸出流;XML 序列化通常只寫不讀。核心問題:有一個遞增寫入輸出流的數據源,需要一個輸入流透明讀取這些數據。本文為兩部分系列第一篇,分析三種解決方案並確立最佳框架。
OutputStream:寫入數據,可鏈接設備或另一輸出流(過濾器)。InputStream:讀取數據,同樣可鏈接。GZIPOutputStream 是輸出流過濾器,壓縮寫入數據。需求是輸入流過濾器:讀取數據、壓縮、允許讀取結果。java.nio 主要針對 OS I/O 資源高效訪問,不直接解決此問題。
將所有數據讀入內存緩衝區(ByteArrayOutputStream)經轉換器處理,再用 ByteArrayInputStream 讀取。
public static InputStream bruteForceCompress(InputStream in) throws IOException {
ByteArrayOutputStream sink = new ByteArrayOutputStream();
OutputStream out = new GZIPOutputStream(sink);
io(in, out); // 通用複製方法
out.close();
byte[] buffer = sink.toByteArray(); // 複製
return new ByteArrayInputStream(buffer); // 再次複製
}
缺點:全量數據駐留內存,大文件浪費資源。效率上經歷四次緩衝區複製(讀入→壓縮→擴容複製→toByteArray複製→ByteArrayInputStream複製),ByteArrayOutputStream 擴容平均每字節複製兩次。
用 PipedOutputStream/PipedInputStream 連接兩線程:一線程壓縮寫入,另一線程讀取。
private static InputStream pipedCompress(final InputStream in) throws IOException {
PipedInputStream source = new PipedInputStream();
final OutputStream out = new GZIPOutputStream(new PipedOutputStream(source));
new Thread() { public void run() { try { Streams.io(in, out); out.close(); } catch (IOException ex) { ex.printStackTrace(); } } }.start();
return source;
}
理論優勢:雙線程可受益 SMP/SMT,僅兩次緩衝區複製,固定緩衝區流式處理無需全量內存。
實際災難:同步導致劇烈鎖爭用、內部緩衝區太小、持久共享緩衝區阻礙 CPU 緩存親和性、異常處理極難(無法將異常傳遞給讀端)。性能極差,不實用。
設計一個框架:數據源遞增寫入 OutputStream,框架提供 InputStream 讀取。關鍵接口 OutputEngine:
public interface OutputEngine {
void initialize(OutputStream out) throws IOException;
void execute() throws IOException; // 遞增寫入一塊數據
void finish() throws IOException; // 清理資源
}
通過輸入流與 OutputStreamFactory 構造,execute() 讀取一塊數據寫入工廠產生的過濾輸出流。GZIPOutputStreamFactory 示例:
public class GZIPOutputStreamFactory implements IOStreamEngine.OutputStreamFactory {
public OutputStream getOutputStream(OutputStream out) { return new GZIPOutputStream(out); }
}
內部維護讀緩衝區,read() 發現緩衝區空時調用 engine.execute() 重新填充。內部輸出流 OutputStreamImpl 接收引擎寫入的數據,直接複製到讀緩衝區(緩衝區不足時翻倍)。引擎關閉輸出流時設置 eof 標記。
關鍵 writeImpl:
private void writeImpl(byte[] data, int offset, int length) {
if (index >= limit) index = limit = 0; // 緩衝區用完重置
if (limit + length > capacity) { /* 翻倍擴容 */ }
System.arraycopy(data, offset, buffer, limit, length);
limit += length;
}
private static InputStream engineCompress(InputStream in) throws IOException {
return new OutputEngineInputStream(new IOStreamEngine(in, new GZIPOutputStreamFactory()));
}
代碼清晰,良好設計產生可維護代碼。
| 方案 | 15KB | 15MB |
|---|---|---|
| 管道流 | 23 ms | 22100 ms |
| 蠻力 | 0.35 ms | 745 ms |
| 工程方案 | 0.16 ms | 73 ms |
工程方案顯著優於標準 API 兩種方案。若輸出引擎承諾寫入後不修改數組,可進一步降至一次複製,但極少見。
從 Reader 讀取字符,經 OutputStreamWriter 編碼寫入輸出流。Java 1.4 前唯一有效方案,1.4+ java.nio.charset 提供無流編碼但不滿足基於流需求。
遍歷 DOM 節點迭代器,序列化寫入 OutputStreamWriter,可用於部分 DOM 文檔重新解析驗證。
提供的框架高效讀取只能寫入輸出流的系統產生的數據(壓縮、編碼、序列化)。任何寫入 ByteArrayOutputStream 後續處理的應用皆可受益。下一篇將重寫字節數組流與管道流,展示 API 約定稍微放寬即可帶來百倍性能提升。靈感源於洗碗時的思考——跳出代碼审視問題全貌,往往能得出更清晰、高效、可維護的方案。