5. Crear procesos en Java¶
Hasta ahora has lanzado programas desde la terminal o con doble clic. En este apartado aprenderás a hacer que tu programa Java lance otros procesos: comandos del sistema, otras clases Java o el navegador.
5.1 Punto de partida: cada java es un proceso¶
public class HolaMundo {
public static void main(String[] args) {
System.out.println("¡Hola, mundo!");
}
}
javac HolaMundo.java # crea HolaMundo.class: el programa, en disco
java HolaMundo # el SO crea un proceso: una JVM con su PID que ejecuta el main
javacgeneraHolaMundo.class. Es el programa: está en disco y no hace nada.java HolaMundopide al sistema operativo que cree un proceso: arranca una máquina virtual de Java, carga la clase y ejecutamain.- La terminal es el padre y el proceso
javaes el hijo. Cuandomaintermina, el proceso termina y devuelve un código de salida: 0 si todo ha ido bien.
Lo que hace la terminal, crear un proceso hijo y esperar a que termine, lo puede hacer también nuestro programa.
5.2 Las dos clases: ProcessBuilder y Process¶
| Clase | Papel | Métodos principales |
|---|---|---|
ProcessBuilder |
Prepara y lanza el proceso: qué comando, con qué argumentos, en qué carpeta, con qué variables de entorno y adónde van su entrada y su salida. | command(), directory(), environment(), inheritIO(), redirect…(), start() |
Process |
Controla el proceso ya lanzado. Es como un mando a distancia del hijo. | pid(), waitFor(), exitValue(), isAlive(), destroy(), flujos de entrada y salida |
start() es un método de ProcessBuilder y devuelve un objeto Process.
No uses Runtime.exec(String)
En código antiguo verás Runtime.getRuntime().exec("ping -c 3 8.8.8.8"). Esa versión está obsoleta desde Java 18: trocea el texto por los espacios y falla con rutas que los contienen. Usa siempre ProcessBuilder.
5.3 Lanzar un proceso y esperar a que termine¶
import java.io.File;
import java.io.IOException;
public class EjemploProcessBuilder {
public static void main(String[] args) {
try {
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "3", "8.8.8.8"); // Windows: "-n"
pb.directory(new File(System.getProperty("user.home"))); // carpeta de trabajo
pb.environment().put("MODO", "prueba"); // variable de entorno
pb.inheritIO(); // el hijo usa nuestra consola
Process p = pb.start(); // crea el proceso
System.out.println("PID del hijo: " + p.pid());
int codigo = p.waitFor(); // espera a que termine
System.out.println("Terminó con código " + codigo); // 0 = todo bien
} catch (IOException e) {
System.err.println("Error al lanzar el proceso: " + e.getMessage());
} catch (InterruptedException e) {
System.err.println("El proceso fue interrumpido: " + e.getMessage());
Thread.currentThread().interrupt();
}
}
}
Línea a línea:
- El constructor recibe el comando troceado: primero el programa y luego cada argumento como una cadena distinta. Si escribieras
new ProcessBuilder("ping -c 3 8.8.8.8"), Java buscaría un programa llamado literalmente «ping -c 3 8.8.8.8» y fallaría. directory()fija la carpeta de trabajo del hijo. Si no se indica, hereda la del padre.environment()devuelve unMapcon las variables de entorno, heredadas del padre; podemos añadir o quitar.inheritIO()hace que el hijo escriba en nuestra misma consola. Sin esto, su salida va a una tubería que habría que leer (apartado 6).start()pide al sistema operativo que cree el proceso y devuelve el objetoProcess.waitFor()bloquea al padre hasta que el hijo termina y devuelve su código de salida. Además, recoge al hijo: así no queda zombi.
Las dos excepciones¶
| Excepción | Cuándo ocurre | Qué hacer |
|---|---|---|
IOException |
start() no puede crear el proceso: el programa no existe, no está en el PATH o no tiene permisos de ejecución. |
Avisar al usuario con un mensaje claro. |
InterruptedException |
Otro hilo interrumpe al padre mientras espera en waitFor(). |
Volver a marcar la interrupción con Thread.currentThread().interrupt(), para que quien llame a nuestro código sepa que ocurrió. |
En ejemplos cortos verás a veces throws IOException, InterruptedException en el main, para ahorrar espacio. En un programa real, captúralas.
5.4 Comandos diferentes según el sistema operativo¶
Muchos comandos cambian entre sistemas. Para que el programa sea portable, se consulta el sistema con System.getProperty("os.name"):
String so = System.getProperty("os.name").toLowerCase();
boolean windows = so.contains("win");
ProcessBuilder pb = windows
? new ProcessBuilder("cmd", "/c", "dir") // dir es una orden interna de cmd
: new ProcessBuilder("ls", "-l");
pb.inheritIO().start().waitFor();
| Tarea | Windows | Linux y macOS |
|---|---|---|
| Listar ficheros | cmd /c dir |
ls -l |
| Ping de 3 paquetes | ping -n 3 host |
ping -c 3 host |
| Ver la configuración de red | ipconfig |
ip a (Linux), ifconfig (macOS) |
| Abrir una URL o un fichero | cmd /c start "" url |
xdg-open url (Linux), open url (macOS) |
¿Por qué cmd /c?
dir, start, cd o echo no son programas .exe, sino órdenes internas del intérprete cmd. Para ejecutarlas hay que lanzar cmd y pasarle la orden con /c («ejecuta esto y termina»). En Linux ocurre algo parecido con bash -c "orden".
5.5 El código de salida¶
Por convenio, un proceso termina con:
- 0 si todo ha ido bien.
- Cualquier otro valor si ha habido un error. Cada programa decide el significado de cada número.
En Java, un programa elige su código de salida con System.exit(n). Si el main termina normalmente, el código es 0. Si termina por una excepción no capturada, es 1.
El padre lo recibe como resultado de waitFor(), o después con exitValue().
Warning
El código de salida solo sirve para números pequeños: de 0 a 255 en Linux y macOS. Para devolver datos de verdad (un texto, un número grande, una lista), el hijo los escribe en su salida y el padre los lee (apartado 6).
5.6 Ejecutar otra clase Java como proceso: el cocinero y el pinche¶
Para ejecutar código nuestro en un proceso independiente, lanzamos otra máquina virtual de Java con esa clase como programa principal.
El ejemplo: un cocinero (proceso padre) encarga a un pinche (proceso hijo) que mezcle dos ingredientes y le diga si la mezcla cabe en la olla, que admite 1000 g. El pinche recibe las cantidades por argumentos y responde con su código de salida.
Cocinero (padre) Pinche (hijo)
│ │
│── start(): java -cp … Pinche 300 400 ────────►│
│ │ suma 300 + 400 = 700 g
│ waitFor(): el cocinero espera │
│◄──────────────────── System.exit(0) ───────────│
│ ✕ el pinche termina
│ interpreta el código: 0 = cabe, 1 = se pasa
El hijo: Pinche¶
public class Pinche {
static final int CAPACIDAD_OLLA = 1000; // gramos
public static void main(String[] args) {
int ing1 = Integer.parseInt(args[0]); // los argumentos llegan como texto
int ing2 = Integer.parseInt(args[1]);
int total = ing1 + ing2;
System.out.println("Pinche (PID " + ProcessHandle.current().pid()
+ "): mezclo " + ing1 + "g + " + ing2 + "g = " + total + "g");
// Devolvemos el resultado al padre con el código de salida
System.exit(total <= CAPACIDAD_OLLA ? 0 : 1);
}
}
El padre: Cocinero¶
import java.io.IOException;
public class Cocinero {
public static void main(String[] args) {
mandarTareaAlPinche(300, 400); // 700 g: cabe
mandarTareaAlPinche(700, 500); // 1200 g: se pasa
}
public static void mandarTareaAlPinche(int ing1, int ing2) {
try {
// El classpath dice a la nueva JVM dónde están nuestras clases compiladas
String cp = System.getProperty("java.class.path");
ProcessBuilder pb = new ProcessBuilder(
"java", "-cp", cp, "Pinche",
String.valueOf(ing1), String.valueOf(ing2));
pb.inheritIO(); // para ver lo que dice el pinche
System.out.println("Cocinero: Le pido al pinche que mezcle " + ing1 + "g y " + ing2 + "g...");
Process procesoPinche = pb.start();
int codigoSalida = procesoPinche.waitFor(); // el cocinero espera al pinche
if (codigoSalida == 0) {
System.out.println("Cocinero: Perfectas noticias, la mezcla CABE en la olla.\n");
} else if (codigoSalida == 1) {
System.out.println("Cocinero: ¡Atención! La mezcla SE PASA de la capacidad de la olla.\n");
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
Salida real:
Cocinero: Le pido al pinche que mezcle 300g y 400g...
Pinche (PID 708): mezclo 300g + 400g = 700g
Cocinero: Perfectas noticias, la mezcla CABE en la olla.
Cocinero: Le pido al pinche que mezcle 700g y 500g...
Pinche (PID 727): mezclo 700g + 500g = 1200g
Cocinero: ¡Atención! La mezcla SE PASA de la capacidad de la olla.
Fíjate en que:
- Cada pinche tiene un PID distinto: son dos procesos de verdad, dos JVM independientes.
- Los datos bajan del padre al hijo como argumentos del
main, siempre comoString. Por eso se usanString.valueOfen el padre eInteger.parseInten el hijo. - La respuesta sube del hijo al padre como código de salida, con
System.exit. Unreturnen elmaindevolvería siempre 0. - Hay que compilar las dos clases (
javac *.java). Si faltaPinche.class, el hijo falla con «Could not find or load main class Pinche».
Usar la misma JVM que el padre
En lugar de "java" a secas, se puede usar la ruta de la JVM que está ejecutando el padre. Así funciona aunque java no esté en el PATH o haya varias versiones instaladas:
¿Procesos o hilos?
Arrancar una JVM entera cuesta decenas de megas de memoria y unas décimas de segundo. Si solo queremos hacer algo en paralelo dentro de nuestro programa, es mucho más barato un hilo (Tema 2). Se usan procesos cuando queremos aislamiento: si el pinche falla, el cocinero sigue funcionando.
5.7 Abrir una página web desde Java¶
Para abrir una web no programamos un navegador: le pedimos al sistema operativo que abra la URL con el navegador predeterminado. Y eso se hace lanzando un proceso.
En macOS, el comando es open:
import java.io.IOException;
public class AbrirWeb {
public static void main(String[] args) {
String url = "https://www.sevillafc.es/";
try {
ProcessBuilder pb = new ProcessBuilder("open", url); // macOS
pb.inheritIO();
Process p = pb.start();
System.out.println("Abriendo la web " + url + " ... (PID: " + p.pid() + ")");
int codigo = p.waitFor();
System.out.println("Proceso finalizado con código: " + codigo);
} catch (IOException | InterruptedException e) {
System.err.println("Ocurrió un error al intentar abrir el navegador:");
e.printStackTrace();
}
}
}
¿Qué proceso se ha lanzado?¶
- Java lanza
open: ese es nuestro proceso hijo, y su PID es el que se imprime. openconsulta cuál es el navegador predeterminado y le pide que abra la URL. Si el navegador ya estaba abierto, solo añade una pestaña.opentermina enseguida con código 0. El navegador sigue abierto, pero no es hijo nuestro: si cerramos el programa Java, la web sigue abierta.
Versión para cualquier sistema¶
static void abrir(String url) throws IOException, InterruptedException {
String so = System.getProperty("os.name").toLowerCase();
List<String> cmd;
if (so.contains("win")) cmd = List.of("cmd", "/c", "start", "", url);
else if (so.contains("mac")) cmd = List.of("open", url);
else cmd = List.of("xdg-open", url); // Linux
Process p = new ProcessBuilder(cmd).inheritIO().start();
System.out.println("Lanzado " + cmd.get(0) + " con PID " + p.pid());
System.out.println("Código de salida: " + p.waitFor());
}
El "" de Windows
start interpreta el primer argumento entre comillas como el título de la ventana. Sin la cadena vacía "", tomaría la URL como título y no abriría nada.
Alternativa sin procesos
java.awt.Desktop.getDesktop().browse(new URI(url)) también abre una web y elige el comando por nosotros. Pero no crea un proceso que podamos controlar, no da PID ni código de salida y no funciona en equipos sin entorno gráfico. En este tema usamos ProcessBuilder porque lo que estudiamos son los procesos.
5.8 Errores frecuentes¶
| Error | Qué pasa | Solución |
|---|---|---|
| Pasar el comando en una sola cadena | IOException: no encuentra el programa |
Trocear: "ping", "-c", "3", "8.8.8.8" |
Usar ping sin límite en Linux |
El hijo no termina nunca y waitFor() espera para siempre |
-c 3 en Linux y macOS, -n 3 en Windows |
| No compilar la clase hija | «Could not find or load main class» | javac *.java |
Devolver el resultado con return |
El código de salida siempre es 0 | System.exit(n) |
| Lanzar procesos dentro de un bucle sin control | El equipo se llena de procesos y se queda sin recursos | Comprobar bien la condición del bucle y esperar a los hijos |
Concatenar lo que escribe el usuario en una orden de cmd o bash |
Inyección de órdenes: el usuario puede ejecutar lo que quiera | Pasar los datos como argumentos separados y validarlos |
Preguntas de repaso¶
- ¿Qué diferencia hay entre
ProcessBuilderyProcess? ¿De cuál es el métodostart()? - ¿Por qué hay que trocear el comando? ¿Qué pasa si no se hace?
- ¿Para qué sirve
inheritIO()? ¿Qué pasa con la salida del hijo si no se llama? - En el ejemplo del cocinero, ¿por dónde viajan los datos del padre al hijo? ¿Y la respuesta del hijo al padre?
- Al abrir una web, ¿por qué el proceso termina enseguida aunque el navegador siga abierto?
- ¿Qué excepción lanza
start()si el programa no existe? ¿YwaitFor()si interrumpen al padre?