1. 簡介
所謂互動式行程,是指不只是自己執行,而是等待你與它「對話」的程式。它會接收使用者輸入並給出回應。
典型例子包括解譯器如 Python、bash 或 PowerShell。它們會耐心等待指令、執行並顯示結果。還有其他類型:像是互動式工具(計算器)、主控台資料庫(psql、sqlite3),甚至像 vi、nano 這類文字編輯器。有時一般腳本在執行過程中若向你詢問參數或答案,也會變成互動式。
從 Java 啟動這類行程並不輕鬆。這遠不只是「送出指令並取得結果」這麼簡單。你需要建立真正的對話:及時送出資料並讀取回應,就像真人對話一樣。
想像你在即時通訊軟體和朋友聊天:你送出訊息、等待回覆,然後再回覆。若正確設定串流,Java 與外部行程的互動大致相同。
如何建立雙向通訊
每個行程都有三個「通道」:輸入(stdin)、輸出(stdout)與錯誤(stderr)。透過第一個你能傳資料給行程,透過第二個取得結果,而第三個則用於錯誤訊息。
關鍵是不要讓行程僵在等待。如果你只讀取標準輸出而忽略錯誤輸出,行程可能會卡住,彷彿在等你注意它的抱怨。反過來說,若你一直寫入輸入端卻不讀取回應,行程也可能因為累積的資料無處可去而停滯。
因此,在處理互動式行程時,必須維持雙向交換——就像正常的對話,雙方都在聽也在回應,而不是對著空氣說話。
互動式通訊示意圖
+---------------------+
| Java program |
+---------------------+
| ^
v |
stdin stdout/stderr
| ^
+---------------------+
| External process |
+---------------------+
- Java 會寫入行程的輸入串流(行程的 stdin)。
- Java 會讀取行程的 stdout 與 stderr。
- 這些動作可以同時發生!
2. 實作:與外部行程進行互動
來看實作範例:我們將啟動外部行程(例如 Python 直譯器或簡單的 echo 腳本),向它傳送字串並讀取回應。
範例 1:啟動等待輸入的 Python 腳本
先建立一個簡單的 Python 腳本(命名為 echo_bot.py):
# echo_bot.py
while True:
try:
line = input()
if line == "exit":
print("Bye!")
break
print("Echo:", line)
except EOFError:
break
此腳本會等待輸入,並對每一行回應 "Echo: ..."。如果輸入 "exit" —— 就會結束。
如何在 Java 中啟動並與此腳本「對話」?
1. 啟動行程
ProcessBuilder builder = new ProcessBuilder("python", "echo_bot.py");
Process process = builder.start();
2. 準備通訊用的串流
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
3. 實作對話
// 將字串送給行程
writer.write("Hello, process!\n");
writer.flush();
// 讀取回應
String response = reader.readLine();
System.out.println("行程回應: " + response);
4. 重複直到不想再做為止
我們把它包成一個簡單的迴圈,讓使用者可在主控台輸入,Java 將其送往行程並顯示回應。
完整範例:與外部行程的 Java 聊天
import java.io.*;
public class InteractiveProcessDemo {
public static void main(String[] args) throws IOException {
ProcessBuilder builder = new ProcessBuilder("python", "echo_bot.py");
Process process = builder.start();
// 與行程通訊用的串流
BufferedWriter toProcess = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
BufferedReader fromProcess = new BufferedReader(new InputStreamReader(process.getInputStream()));
BufferedReader userInput = new BufferedReader(new InputStreamReader(System.in));
System.out.println("輸入要發送給行程的字串 (輸入 exit 以結束):");
while (true) {
// 讀取使用者輸入的一行
String line = userInput.readLine();
if (line == null) break;
// 將它傳送給行程
toProcess.write(line + "\n");
toProcess.flush();
// 讀取行程的回應
String response = fromProcess.readLine();
System.out.println("行程回應: " + response);
if ("exit".equals(line)) break;
}
// 結束行程
process.destroy();
}
}
程式碼說明:
- 我們使用三個串流:從使用者讀取(System.in)、寫給行程(process.getOutputStream())以及讀取回應(process.getInputStream())。
- 每次送出一行給行程後,立刻透過 readLine() 讀取回應。
- 若使用者輸入 "exit",程式會結束迴圈並銷毀行程(process.destroy())。
3. 為什麼同時讀寫很重要?
在實務中,行程可能輸出大量資料,有時還會連續要求輸入。如果沒有及時讀取行程輸出,它的內部緩衝區可能會被塞滿,行程便會「卡住」,等待有人讀走訊息。反之亦然:若你寫入而不讀取回應,行程可能因為累積的資料無處可去而停滯。
Deadlock:當一切都卡住
Deadlock 指的是兩方(或執行緒與行程)互相等待,誰也無法繼續工作的情況。
Deadlock 範例:
- Java 等待行程在 stdout 輸出。
- 行程等待 Java 寫入它的 stdin。
- 雙方都在等待——沒有人繼續工作。
解法:將讀寫分到不同執行緒
為了避免 deadlock,常常會使用獨立的執行緒(Thread)同時讀取行程的 stdout 與 stderr。例如建立兩個執行緒:一個讀 stdout,另一個讀 stderr,而主執行緒則負責寫入 stdin。
最小示例:為 stderr 啟用獨立執行緒
// 在獨立執行緒中讀取 stderr
Thread errorThread = new Thread(() -> {
try (BufferedReader errorReader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = errorReader.readLine()) != null) {
System.err.println("stderr: " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
});
errorThread.start();
4. 實作:與計算器互動
不一定隨處都有 Python。我們可以試著啟動幾乎到處都有的指令列計算器。例如在 Linux/Mac 上是 bc(basic calculator),在 Windows 上則是 cmd 或 powershell。
範例:與 bc 進行互動(Linux/Mac)
ProcessBuilder builder = new ProcessBuilder("bc");
Process process = builder.start();
BufferedWriter toProcess = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
BufferedReader fromProcess = new BufferedReader(new InputStreamReader(process.getInputStream()));
toProcess.write("2 + 2\n");
toProcess.flush();
String result = fromProcess.readLine();
System.out.println("計算器回應: " + result);
toProcess.write("quit\n");
toProcess.flush();
process.destroy();
在 Windows 上:可以嘗試啟動 cmd 並傳遞指令,但這會更複雜(語法不同)。通常為了示範互動性,會選擇兩邊都有的工具,或自己撰寫小型腳本。
5. 問題與陷阱
串流的緩衝
有時行程不會立即輸出,而會把資料累積在內部緩衝區,只有當緩衝滿了或者收到換行字元("\n")時才輸出。這可能導致你看不到回應,即便行程其實已經「準備好了」。
建議:
- 寫入行程的 stdin 時,務必以 "\n" 結尾。
- 如果你撰寫自己的腳本,請在每次輸出後呼叫 flush()。
串流處理不當導致的 deadlock
若不讀取 stderr,而行程又大量寫入其中,行程可能會卡住,等待有人讀取錯誤輸出。
建議:
務必同時讀取行程的 stdout 與 stderr,最好分別置於不同的執行緒中。
行程已結束——你卻還在寫入
如果行程已經結束,而你仍持續寫入它的輸入端,會得到 IOException:"Stream closed"。
7. 最佳實務:互動式通訊
使用 ExecutorService 進行並行讀取
為了避免手動打造讀取輸出與錯誤的執行緒,使用 ExecutorService 會更方便(例如建立兩個執行緒的池):
import java.util.concurrent.*;
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> {
try (BufferedReader out = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = out.readLine()) != null) {
System.out.println("stdout: " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
});
executor.submit(() -> {
try (BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = err.readLine()) != null) {
System.err.println("stderr: " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
});
正確關閉串流
在結束與行程的互動後,請務必關閉所有串流(stdin、stdout、stderr),以避免資源外洩。
toProcess.close();
fromProcess.close();
process.destroy();
executor.shutdown();
8. 綜合範例:與外部行程的互動式通訊
讓我們把一切整合。下面的範例就像使用者與外部行程(例如 Python 腳本或計算器)之間的互動式「聊天」。
import java.io.*;
import java.util.concurrent.*;
public class InteractiveProcessUniversal {
public static void main(String[] args) throws IOException {
// 請替換為你的指令——例如 "python echo_bot.py" 或 "bc"
ProcessBuilder builder = new ProcessBuilder("python", "echo_bot.py");
Process process = builder.start();
ExecutorService executor = Executors.newFixedThreadPool(2);
// 讀取行程的 stdout
executor.submit(() -> {
try (BufferedReader out = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = out.readLine()) != null) {
System.out.println("[process] " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
});
// 讀取行程的 stderr
executor.submit(() -> {
try (BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = err.readLine()) != null) {
System.err.println("[process-err] " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
});
// 主執行緒:寫入行程的 stdin
try (BufferedWriter toProcess = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
BufferedReader userInput = new BufferedReader(new InputStreamReader(System.in))) {
System.out.println("輸入要傳給行程的字串 (輸入 exit 以結束):");
String line;
while ((line = userInput.readLine()) != null) {
toProcess.write(line + "\n");
toProcess.flush();
if ("exit".equals(line)) break;
}
}
process.destroy();
executor.shutdown();
}
}
9. 互動式處理行程的常見錯誤
錯誤 1:沒有讀取行程的 stderr。 若行程將大量錯誤寫到 stderr,但你沒有讀取,它可能會卡住。即使你確信不會有錯誤——也請讀取 stderr!
錯誤 2:沒有關閉串流。 若不關閉行程的串流,可能造成資源外洩,有時甚至會阻礙行程結束。
錯誤 3:不同平台的指令差異。 指令與語法在 Windows 與類 Unix 系統間有所差異。請檢查作業系統並選擇適合的指令。
錯誤 4:程式「卡住」——忘了對輸出呼叫 flush。 若忘了在寫入串流後呼叫 flush(),資料可能卡在緩衝區而無法送到行程。
錯誤 5:外部行程的輸出緩衝。 有時行程在累積到足夠字元或收到 "\n" 前不會輸出。若你撰寫自己的腳本——請使用 flush()。
錯誤 6:同時等待輸入/輸出造成的 deadlock。 若主執行緒在等輸出,而行程在等輸入,雙方可能「卡住」。請將讀寫放到不同的執行緒中。
錯誤 7:對已結束的行程寫入而出現「Stream closed」。 在寫入它的 stdin 前,先確認行程仍在。否則你會得到 IOException:"Stream closed"。
GO TO FULL VERSION