1. Потоки вводу/виводу процесу
Коли ви запускаєте зовнішній процес із Java, між ним і вашою програмою відкривається справжній мінідіалог — три канали зв’язку.
Перший канал — це стандартний вивід. Усе, що зовнішній процес хоче показати «на екран», ви можете перехопити через Process.getInputStream(). Другий канал — потік помилок. Якщо процес вирішить поскаржитися або видати повідомлення про помилку, ви дізнаєтеся про це через Process.getErrorStream(). І нарешті, третій — стандартне введення. Через нього ви можете надіслати процесу певні дані, якщо він чогось від вас чекає. Робиться це за допомогою Process.getOutputStream().
Трохи збиває з пантелику, що назви потоків ніби переплутані. Для процесу InputStream — це те, що ви читаєте (його вивід), а OutputStream — те, куди ви пишете (його ввід). Але якщо дивитися очима вашої Java‑програми, усе стає логічним: ви читаєте те, що процес видає, і пишете те, що хочете йому передати.
Схема потоків процесу
+-------------------+ +---------------------+
| Ваша программа | <-------> | Внешний процесс |
+-------------------+ +---------------------+
| |
| 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 {
// Створюємо процес: команда залежить від вашої ОС!
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: Передавання даних процесу
Припустімо, у вас є простий Python‑скрипт, який читає рядок і друкує його назад:
echo_input.py:
# echo_input.py
line = input()
print("You said:", line)
Запустимо його з Java та надішлемо рядок:
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, а ви не читаєте ці потоки, буфер ОС може переповнитися, і процес «зависне», очікуючи, поки хтось звільнить місце. Тому гарна практика — читати обидва потоки паралельно (або принаймні почергово).
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());
}
}
}
Такий підхід дозволяє уникнути deadlock’у і не втратити жодної помилки!
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();
// Надсилаємо два числа в stdin Python-скрипта
try (var writer = process.outputWriter(StandardCharsets.UTF_8)) {
writer.println("10");
writer.println("25");
}
// Читаємо результат із stdout за допомогою inputReader
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? Як його уникнути
Deadlock — це ситуація, коли процес і ваша програма чекають одне одного до нескінченності. Наприклад, якщо буфер виводу процесу переповнений, а ви не читаєте цей потік, процес зупиниться. Якщо ви чекаєте завершення процесу, а він чекає, поки ви йому щось запишете, — теж deadlock.
Як уникнути:
- Завжди читайте обидва потоки (stdout і stderr).
- Для довгих/багатослівних процесів використовуйте окремі потоки для читання.
- Не блокуйте запис і читання одночасно в одному потоці.
9. Типові помилки під час роботи з потоками вводу/виводу процесу
Помилка № 1: Читання лише одного потоку (stdout або stderr).
Якщо читати тільки stdout, а процес пише помилку в stderr, буфер помилок може переповнитися — процес зависне. Рішення: читати обидва потоки (краще в окремих потоках).
Помилка № 2: Неправильне кодування.
Якщо не вказати потрібне кодування, вивід може бути некоректним. Завжди перевіряйте, у якому кодуванні пише зовнішній процес (наприклад, явно задавайте "UTF-8" або використовуйте StandardCharsets.UTF_8).
Помилка № 3: Не закрили потоки.
Відкриті потоки можуть призвести до витоків ресурсів. Використовуйте try-with-resources або явно закривайте всі потоки.
Помилка № 4: Не викликали flush() під час запису в stdin процесу.
Якщо забути викликати flush(), дані можуть так і не потрапити в процес — він чекатиме вводу вічно.
Помилка № 5: Платформні відмінності.
Команди та синтаксис можуть відрізнятися між Windows і Linux/Mac. Перевіряйте ОС через System.getProperty("os.name") і добирайте команди відповідно.
Помилка № 6: Не обробили винятки.
Робота з процесами часто супроводжується IOException. Завжди обробляйте винятки, щоб програма не завершилася аварійно.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ