1. プロセスの入出力ストリーム
Java から外部プロセスを起動すると、そのプロセスとあなたのプログラムの間に 3 本の通信チャネルからなる小さな対話が生まれます。
1 つ目のチャネルは標準出力です。外部プロセスが「画面」に表示したいすべての内容は、Process.getInputStream() から受け取れます。2 つ目はエラーストリームです。プロセスが不満やエラーメッセージを書き出すときは、Process.getErrorStream() で知ることができます。そして最後に 3 つ目が標準入力です。プロセスがあなたからの入力を待っている場合、Process.getOutputStream() を使ってデータを送れます。
名前が少し紛らわしいのは、プロセス側の InputStream は「あなたが読むもの(プロセスの出力)」で、OutputStream は「あなたが書くもの(プロセスの入力)」だからです。ですが、あなたの Java プログラムの視点で考えると筋が通ります。あなたはプロセスの出力を読み、プロセスに渡したいものを書き込みます。
プロセスのストリーム構成図
+-------------------+ +---------------------+
| Your program | <-------> | External process |
+-------------------+ +---------------------+
| |
| getOutputStream() --------> stdin
| getInputStream() <-------- stdout
| getErrorStream() <-------- stderr
2. プロセスの出力(stdout)を読む
外部プロセスが画面に出力するものを読んでみましょう。コマンドの結果、プログラムのバージョン、成功メッセージなど、なんでも対象になります。
例 1: コマンド java -version の出力を読む
まずプロセスを起動し、その出力を読み取ります。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
public class ProcessOutputExample {
public static void main(String[] args) throws IOException {
// プロセスを作成(コマンドは OS に依存します)
ProcessBuilder pb = new ProcessBuilder("java", "-version");
Process process = pb.start();
// エラーストリームを読む(java -version は通常こちらに出力します!)
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("Process output: " + line);
}
}
}
}
注意! コマンド java -version は通常の stdout ではなく、エラーストリームの stderr に結果を書き出します。これはバグではなく Java の仕様です。そのためここでは process.getErrorStream() を読みます。ほとんどの他のコマンド(たとえば echo、ls、dir)では getInputStream() を使いましょう。
例 2: コマンド echo の出力を読む
ProcessBuilder pb = new ProcessBuilder("echo", "Hello, Java!");
Process process = pb.start();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("Process output: " + line);
}
}
なぜ BufferedReader が必要?
標準の入力ストリームはバイトストリーム(InputStream)です。文字列として読み取るには、InputStreamReader(バイトを文字に変換)と BufferedReader(行単位で読みやすくする)の組み合わせを使います。
3. プロセスの標準入力(stdin)へ書き込む
外部プロセスがこちらのデータを待っていることがあります。たとえば、ユーザー名の入力を求めるスクリプトや、数値を待つ電卓などです。
その場合は process.getOutputStream() を使います。そう、あなたはプロセスの「入力ストリーム」に書き込むのです。
例 3: プロセスへデータを渡す
次のように 1 行を読み取ってそのまま返す簡単な Python スクリプトがあるとします。
echo_input.py:
# echo_input.py
line = input()
print("You said:", line)
これを Java から起動し、1 行送ってみます。
import java.io.*;
public class ProcessStdinExample {
public static void main(String[] args) throws IOException {
ProcessBuilder pb = new ProcessBuilder("python", "echo_input.py");
Process process = pb.start();
// プロセスの stdin に書き込む
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(process.getOutputStream()))) {
writer.write("Java からの挨拶!\n");
writer.flush();
}
// プロセスの stdout を読む
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("プロセスの応答: " + line);
}
}
}
}
flush() を忘れないでください。これを呼ばないと、データがバッファにたまったままプロセスに届かないことがあります。
4. エラー処理: stderr を読む
外部プロセスは結果だけでなく、エラーも書き出します。エラーストリームを読まないと重要な情報を見逃したり、エラーバッファがいっぱいになってプロセスが停止してしまい(deadlock)、プログラムが固まることがあります。
例 4: プロセスの stderr を読む
ProcessBuilder pb = new ProcessBuilder("java", "-version");
Process process = pb.start();
// エラーを読む
try (BufferedReader errorReader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = errorReader.readLine()) != null) {
System.out.println("プロセスのエラー: " + line);
}
}
なぜ両方のストリームを読むことが重要か
プロセスが stdout または stderr に大量のデータを書き出し、あなたがそれらを読まない場合、OS のバッファがいっぱいになり、誰かが空けてくれるまでプロセスが停止します。したがって、良い作法は両方のストリームを並行して読むこと(少なくとも順番にでも)です。
5. 実践: コマンドの起動、出力の読み取り、エラー処理
ここまでをまとめて、外部コマンドを起動し、標準出力とエラーを読み取り、すべてを画面に表示する小さなユーティリティを作ってみます。
例 5: コマンドの汎用起動
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class RunCommand {
public static void main(String[] args) throws Exception {
if (args.length == 0) {
System.out.println("起動するコマンドを指定してください。例: java RunCommand ls -l");
return;
}
var process = new ProcessBuilder(args).start();
// stdout と stderr を別スレッドで読み取る
var t1 = Thread.ofPlatform().start(() -> readStream(process.inputReader(), "[stdout]"));
var t2 = Thread.ofPlatform().start(() -> readStream(process.errorReader(), "[stderr]"));
int exitCode = process.waitFor(); // プロセスの終了を待つ
t1.join();
t2.join();
System.out.println("プロセスは終了コードで終了: " + exitCode);
}
private static void readStream(BufferedReader reader, String prefix) {
try (reader) {
reader.lines().forEach(line -> System.out.println(prefix + " " + line));
} catch (Exception e) {
System.err.println("読み取りエラー " + prefix + ": " + e.getMessage());
}
}
}
この方法ならデッドロックを避けられ、エラーも取りこぼしません。
6. 文字エンコーディングについて簡単に
デフォルトでは InputStreamReader はシステムのエンコーディングを使います。外部プロセスが別のエンコーディング(たとえば UTF-8、あなたの環境が Windows-1251 など)で書き出すと、出力が「文字化け」することがあります。エンコーディングを明示的に指定しましょう。
new InputStreamReader(process.getInputStream(), "UTF-8")
7. 例: Python スクリプトを起動してデータを渡す
複数行の入力を受け取り、その結果を出力する Python スクリプトがあるとします。
adder.py:
# adder.py
a = int(input())
b = int(input())
print(a + b)
Java から数値を送り、結果を読み取ることができます。
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
public class PythonAdder {
public static void main(String[] args) throws Exception {
var process = new ProcessBuilder("python", "adder.py").start();
// Python スクリプトの stdin に 2 つの数値を送る
try (var writer = process.outputWriter(StandardCharsets.UTF_8)) {
writer.println("10");
writer.println("25");
}
// inputReader で stdout の結果を読む
try (BufferedReader reader = process.inputReader(StandardCharsets.UTF_8)) {
reader.lines().forEach(line -> System.out.println("Python からの応答: " + line));
}
int exitCode = process.waitFor();
System.out.println("Python は終了コードで終了: " + exitCode);
}
}
8. なぜデッドロックが起きるのか? どう避けるか
Deadlock は、プロセスとあなたのプログラムが互いに無限に待ち合う状況です。たとえば、プロセスの出力バッファがいっぱいなのにあなたがそのストリームを読まないと、プロセスは停止します。あなたがプロセスの終了を待っている一方で、プロセスがあなたからの入力を書き込み待ちしている場合も、やはりデッドロックです。
回避方法:
- 常に両方のストリーム(stdout と stderr)を読む。
- 長時間/大量出力を行うプロセスには、読み取り用の別スレッドを使う。
- 1 本のスレッドで同時に書き込みと読み取りをブロックしない。
9. プロセス入出力ストリーム取り扱いのよくある誤り
誤り1: 1 つのストリーム(stdout または stderr)しか読まない。
stdout だけを読んでいて、プロセスがエラーを stderr に書き出した場合、エラーバッファがいっぱいになってプロセスが停止することがあります。対策: 両方のストリームを読む(できれば別スレッドで)。
誤り2: エンコーディングの指定ミス。
必要なエンコーディングを指定しないと、出力が読めない場合があります。外部プロセスがどのエンコーディングで書き出しているかを常に確認しましょう(たとえば "UTF-8" を明示する、または StandardCharsets.UTF_8 を使う)。
誤り3: ストリームを閉じない。
開いたままのストリームはリソースリークにつながります。try-with-resources を使うか、すべてのストリームを明示的に閉じましょう。
誤り4: プロセスの stdin に書き込む際に flush() を呼ばない。
flush() を忘れると、データがプロセスに届かず、プロセスが入力待ちのままになることがあります。
誤り5: プラットフォーム差異を無視。
コマンドや文法は Windows と Linux/Mac で異なることがあります。System.getProperty("os.name") で OS を確認し、それに合わせてコマンドを選びましょう。
誤り6: 例外処理をしていない。
プロセス操作では IOException が発生しがちです。例外を適切に処理して、プログラムが異常終了しないようにしましょう。
GO TO FULL VERSION