CodeGym /コース /JAVA 25 SELF /プロセスの出力の読み取りと stdin/stdout の扱い

プロセスの出力の読み取りと stdin/stdout の扱い

JAVA 25 SELF
レベル 61 , レッスン 1
使用可能

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() を読みます。ほとんどの他のコマンド(たとえば echolsdir)では 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 は、プロセスとあなたのプログラムが互いに無限に待ち合う状況です。たとえば、プロセスの出力バッファがいっぱいなのにあなたがそのストリームを読まないと、プロセスは停止します。あなたがプロセスの終了を待っている一方で、プロセスがあなたからの入力を書き込み待ちしている場合も、やはりデッドロックです。

回避方法:

  • 常に両方のストリーム(stdoutstderr)を読む。
  • 長時間/大量出力を行うプロセスには、読み取り用の別スレッドを使う。
  • 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 が発生しがちです。例外を適切に処理して、プログラムが異常終了しないようにしましょう。

1
タスク
JAVA 25 SELF, レベル 61, レッスン 1
ロック未解除
秘密のメッセージ 📩
秘密のメッセージ 📩
1
タスク
JAVA 25 SELF, レベル 61, レッスン 1
ロック未解除
宇宙ステーションの管制官 🚀
宇宙ステーションの管制官 🚀
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION