1. 讀寫串流時的死結
最惱人的錯誤之一,就是行程卡住而不結束,明明看起來一切都做對了。常見原因是外部行程的輸出或錯誤輸出緩衝區被塞滿。若不讀取兩個串流(stdout 與 stderr),行程在嘗試輸出時可能被「卡住」,因為沒有任何人從它的緩衝區把資料取走。
問題範例
ProcessBuilder builder = new ProcessBuilder("java", "-version");
Process process = builder.start();
// 只讀取 stdout,忽略 stderr!
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
如果指令把資料寫到 stderr(例如,java -version 幾乎都會寫到那裡!),該串流可能被塞滿,行程就會卡住。
正確做法
並行讀取兩個串流(使用各自的執行緒或 ExecutorService):
ProcessBuilder builder = new ProcessBuilder("java", "-version");
Process process = builder.start();
// 讀取 stdout
Thread stdoutThread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("[stdout] " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
});
// 讀取 stderr
Thread stderrThread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.err.println("[stderr] " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
});
stdoutThread.start();
stderrThread.start();
process.waitFor();
stdoutThread.join();
stderrThread.join();
結論:
不讀取 stderr — 行程可能卡住。
不讀取 stdout — 也可能卡住。
同時讀兩個串流 — 一切順利!
2. 編碼問題
外部行程輸出的文字編碼可能與你的 Java 程式預設值不同。若未指定正確編碼,會得到亂碼(遇到西里爾字母時尤其明顯)。
錯誤示例
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
這段程式碼使用系統預設編碼。但如果外部行程以 UTF-8 輸出,而你的系統是 Windows-1251,西里爾文字就會變成天書。
正確做法
若知道對方的編碼,請明確指定:
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)
);
若不確定,請查閱該程式的文件,或嘗試幾種常見選項。
小撇步:在 Windows 上,主控台工具常用 CP866 或 Windows-1251,而在 Linux 上通常是 UTF-8。
3. 平台差異
某些在一個作業系統可用的指令,在另一個作業系統可能不存在。比如,ls 在 Linux/Mac 上可用,但在 Windows 上沒有(那裡用的是 dir)。指令語法、路徑分隔符與引號也各不相同。
錯誤示例
ProcessBuilder builder = new ProcessBuilder("ls", "-l");
builder.start(); // 在 Windows 上:找不到 "ls"!
正確做法
String os = System.getProperty("os.name").toLowerCase();
ProcessBuilder builder;
if (os.contains("win")) {
builder = new ProcessBuilder("cmd", "/c", "dir");
} else {
builder = new ProcessBuilder("ls", "-l");
}
檔案路徑:請使用 File.separator 取代「/」或「\」,避免路徑相容性問題。
4. 權限問題
某些指令需要管理員權限(刪除系統檔、修改網路設定等)。若權限不足,指令將以錯誤結束,或甚至無法啟動。
範例
ProcessBuilder builder = new ProcessBuilder("rm", "-rf", "/root/secret.txt");
Process process = builder.start();
// ... 將會得到 Permission denied 錯誤
正確做法
- 確認你的指令是否需要較高權限。
- 透過 process.exitValue() 處理行程的傳回碼。
- 讀取 stderr —— 錯誤原因通常在那裡(例如「Permission denied」)。
5. 資源洩漏
如果不關閉行程的串流(InputStream、OutputStream、ErrorStream),它們可能會一直懸著、占用資源,甚至阻礙結束。同樣地,如果不結束行程本身,它可能在系統裡變成「殭屍」。
錯誤示例
Process process = builder.start();
// ... 有在執行,但沒有關閉串流!
正確做法
對於串流請使用 try-with-resources:
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
// 讀取輸出
}
完成與行程的互動後,請正確將其停止:
process.destroy(); // 結束行程(如果它仍在執行)
注意:若不關閉串流,可能導致記憶體洩漏、卡住,以及系統層面的問題(尤其在大量啟動行程時)。
6. 互動式溝通中的死結
在互動式交換資料時,很容易陷入相互鎖死:Java 等待回應,而外部程式在等你的輸入。結果雙方都不動作。或者你送出了資料,卻不讀回應——某個時刻外部程式的緩衝區會被塞滿,於是它停止寫入。
為避免此情況,請分工處理:一個執行緒負責讀取,另一個負責寫入。如此互動可並行進行,彼此不會互卡。另外,在結束後不要留下開啟的串流——請將它們關閉,避免系統占用多餘資源。
GO TO FULL VERSION