Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
208: Running External Processes with ProcessBuilder
I've seen this exact scenario play out a dozen times with junior devs: you need Java to do something that a command-line tool already does perfectly, so you reach for ProcessBuilder. It seems straightforward—you tell Java what to run, you call waitFor(), and you move on. But then, suddenly, your application just... stops. No exception, no crash, just a permanent hang.
Take a look at this snippet. I'm trying to run a recursive directory listing to get a feel for a project structure.
ProcessBuilder pb = new ProcessBuilder("ls", "-R", "/usr/local/bin");
Process process = pb.start();
// We just want to wait until the command finishes
int exitCode = process.waitFor();
System.out.println("Finished with code: " + exitCode);
The Mysterious Process Hang
If you run this on a directory with a few files, it works fine. But if you run it on a massive directory, the program will hang indefinitely. You'll be staring at your IDE, wondering why the process hasn't returned, and you might even think you've hit a deadlock in your Java code.
Here is what's actually happening: Every process has a limited buffer for its standard output (stdout) and standard error (stderr). When the external process (in this case, ls) prints a lot of text, it fills up that buffer. Once the buffer is full, the external process blocks—it literally stops executing—because it can't write any more data until some other process (your Java app) reads that data out of the pipe.
But your Java app is calling process.waitFor(). You've told Java to sit still and do nothing until the process finishes. You've created a classic stalemate: the process is waiting for you to read, and you are waiting for the process to finish.
Solving the Buffer Blockage
The quickest way to fix this, especially if you just want to see the output in your own console for debugging, is to use inheritIO(). This tells the child process to use the same standard I/O streams as the parent Java process. Essentially, it bypasses the Java-managed buffers and sends the output directly to your terminal.
ProcessBuilder pb = new ProcessBuilder("ls", "-R", "/usr/local/bin");
// This is the magic line
pb.inheritIO();
Process process = pb.start();
int exitCode = process.waitFor();
System.out.println("Finished with code: " + exitCode);
Now, the output flows straight to your console, the buffer never fills up, and waitFor() can finally return. If you actually need to capture that output to use it in a variable, you can't use inheritIO(). Instead, you'd need to read the process.getInputStream() in a separate thread or use redirectOutput(File file) to dump the results into a temporary file.
Handling Arguments Correctly
Another thing I want to mention is how you pass commands. A common instinct is to try and pass the whole command as one string: new ProcessBuilder("ls -R /bin"). This will fail with an IOException because Java looks for an executable file literally named "ls -R /bin", which obviously doesn't exist.
Always pass the command and its arguments as separate strings in a list or as varargs. This avoids shell injection vulnerabilities and ensures the operating system handles the argument parsing correctly. If you absolutely must use a shell feature like pipes (|) or redirects (>), you have to wrap the execution in a shell call, like this:
// On Linux/macOS: running a piped command
ProcessBuilder pb = new ProcessBuilder("sh", "-c", "ls -R /usr/local/bin | grep 'my-app'");
pb.inheritIO();
pb.start().waitFor();
I generally recommend avoiding sh -c unless it's strictly necessary. It adds overhead and makes your code platform-dependent. Stick to the list-of-arguments approach whenever possible.
📋 Practical Task
Exercise: Building a Git Branch Auditor
Your task is to create a small utility that audits a Git repository to see how many local branches exist. This will require you to execute an external process and actually process the output in Java rather than just printing it to the console.
Requirements:
- Use
ProcessBuilderto run the commandgit branch. - Do not use
inheritIO(). Instead, useprocess.getInputStream()and aBufferedReaderto read the output. - Count the number of lines returned by the command (each line represents a branch).
- Print the final count to the console in the format:
"Local branch count: X". - Ensure you handle the
IOExceptionandInterruptedExceptionthat these methods throw.
Hint: You'll need to wrap the input stream in an InputStreamReader before passing it to the BufferedReader.
There are no comments for now.