1. Process I/O streams
When you start an external process from Java, a real little conversation opens between it and your program — three communication channels.
The first channel is standard output. Everything the external process wants to show “on the screen” can be captured via Process.getInputStream(). The second channel is the error stream. If the process decides to complain or emit an error message, you’ll learn about it via Process.getErrorStream(). And finally, the third is standard input. Through it you can send the process some data if it expects anything from you. You do this with Process.getOutputStream().
It can be a bit confusing that the stream names feel reversed. For the process, InputStream is what you read (its output), and OutputStream is where you write (its input). But from your Java program’s perspective, it’s perfectly logical: you read what the process emits, and you write what you want to send to it.
Process stream diagram
+-------------------+ +---------------------+
| Your program | <-------> | External process |
+-------------------+ +---------------------+
| |
| getOutputStream() --------> stdin
| getInputStream() <-------- stdout
| getErrorStream() <-------- stderr
2. Reading a process’s output (stdout)
Let’s learn to read what the external process prints to the screen. It could be a command result, a program version, a success message — anything.
Example 1: Reading the output of java -version
First, start the process, then read its output.
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
public class ProcessOutputExample {
public static void main(String[] args) throws IOException {
// Create the process: the command depends on your OS!
ProcessBuilder pb = new ProcessBuilder("java", "-version");
Process process = pb.start();
// Read the error stream (java -version usually writes there!)
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("Process output: " + line);
}
}
}
}
Note! The java -version command writes its result not to normal stdout but to the stderr stream. This isn’t a bug; it’s a Java quirk. That’s why we read process.getErrorStream(). For most other commands (for example, echo, ls, dir) — use getInputStream().
Example 2: Reading the output of 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);
}
}
Why do we need BufferedReader?
The standard input stream is a byte stream (InputStream). To read it as lines, we use a combo of InputStreamReader (converts bytes to characters) + BufferedReader (convenient for line-by-line reading).
3. Writing to a process’s input stream (stdin)
Sometimes the external process expects input from us. For example, if you launch a script that asks for a username, or a calculator that awaits numbers.
Use process.getOutputStream() for that — yes, that’s right: you “write” to the process’s stream.
Example 3: Sending data to a process
Suppose you have a simple Python script that reads a line and prints it back:
echo_input.py:
# echo_input.py
line = input()
print("You said:", line)
Run it from Java and send a line:
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();
// Write to the process’s stdin
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(process.getOutputStream()))) {
writer.write("Hello from Java!\n");
writer.flush();
}
// Read the process’s stdout
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("Process response: " + line);
}
}
}
}
Don’t forget flush() — otherwise the data can “get stuck” in the buffer and never reach the process.
4. Handling errors: reading stderr
An external process can not only please us with results, but also complain — write errors. If you don’t read the error stream, you can miss important information or even get a hang (deadlock) if the error buffer fills up.
Example 4: Reading a process’s stderr
ProcessBuilder pb = new ProcessBuilder("java", "-version");
Process process = pb.start();
// Read errors
try (BufferedReader errorReader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = errorReader.readLine()) != null) {
System.out.println("Process error: " + line);
}
}
Why is it important to read both streams?
If a process writes a lot to stdout or stderr and you don’t read those streams, the OS buffer can fill up and the process will “hang,” waiting for someone to free space. Therefore, a good practice is to read both streams in parallel (or at least alternately).
5. Practice: launch a command, read output, handle errors
Let’s combine everything above into a small utility that launches an external command, reads its standard output and errors, and prints everything to the screen.
Example 5: Universal command runner
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("Specify a command to run, for example: java RunCommand ls -l");
return;
}
var process = new ProcessBuilder(args).start();
// Start separate threads to read stdout and stderr
var t1 = Thread.ofPlatform().start(() -> readStream(process.inputReader(), "[stdout]"));
var t2 = Thread.ofPlatform().start(() -> readStream(process.errorReader(), "[stderr]"));
int exitCode = process.waitFor(); // wait for the process to finish
t1.join();
t2.join();
System.out.println("Process exited with code: " + 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("Read error " + prefix + ": " + e.getMessage());
}
}
}
This approach helps you avoid deadlocks and not lose a single error!
A quick note on encodings
By default, InputStreamReader uses the system encoding. If the external process writes in a different encoding (for example, UTF-8 while you’re on Windows-1251), the output can look like garbled text. Specify the encoding explicitly:
new InputStreamReader(process.getInputStream(), "UTF-8")
7. Example: running a Python script and sending data to it
Suppose you have a Python script that expects several lines on input and then prints the result.
adder.py:
# adder.py
a = int(input())
b = int(input())
print(a + b)
From Java you can send it numbers and read the answer:
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();
// Send two numbers to the Python script’s stdin
try (var writer = process.outputWriter(StandardCharsets.UTF_8)) {
writer.println("10");
writer.println("25");
}
// Read the result from stdout using inputReader
try (BufferedReader reader = process.inputReader(StandardCharsets.UTF_8)) {
reader.lines().forEach(line -> System.out.println("Response from Python: " + line));
}
int exitCode = process.waitFor();
System.out.println("Python exited with code: " + exitCode);
}
}
8. Why deadlock happens and how to avoid it
A deadlock is a situation where the process and your program wait for each other forever. For example, if the process’s output buffer is full and you aren’t reading that stream, the process will stop. If you’re waiting for the process to finish while it’s waiting for you to write something — that’s a deadlock too.
How to avoid it:
- Always read both streams (stdout and stderr).
- For long/chatty processes, use separate threads to read.
- Don’t block reading and writing at the same time in a single thread.
9. Common mistakes when working with a process’s I/O streams
Mistake #1: Reading only one stream (stdout or stderr).
If you read only stdout while the process writes errors to stderr, the error buffer can fill up — the process will hang. Solution: read both streams (preferably in separate threads).
Mistake #2: Wrong encoding.
If you don’t specify the needed encoding, the output can be unreadable. Always check which encoding the external process uses (for example, explicitly set "UTF-8" or use StandardCharsets.UTF_8).
Mistake #3: Streams not closed.
Open streams can lead to resource leaks. Use try-with-resources or close all streams explicitly.
Mistake #4: No flush() when writing to the process’s stdin.
If you forget to call flush(), the data may never reach the process — it will wait for input forever.
Mistake #5: Platform differences.
Commands and syntax can differ between Windows and Linux/Mac. Check the OS via System.getProperty("os.name") and choose commands accordingly.
Mistake #6: Exceptions not handled.
Working with processes is often accompanied by IOException. Always handle exceptions so the program doesn’t crash.
GO TO FULL VERSION