Blog

Hola, Java 25: ejecuta un archivo Java sin ceremonia

Java 25 ejecuta un solo archivo fuente con un comando y un void main a secas, sin una clase que escribir antes. Aprende qué trae el JDK, por qué Java 25 es la versión que conviene aprender y cómo leer errores de compilación y excepciones.

Antes, un programa Java necesitaba cinco líneas de ceremonia para poder imprimir una palabra. Java 25 lo redujo a tres. Escribes un void main(), llamas a IO.println y ejecutas el archivo con un solo comando. La forma antigua sigue funcionando, y la vas a ver en casi cualquier código existente, así que este post muestra las dos y explica para qué servía cada pieza que se quitó.

Este post explica qué es el JDK y qué versión conseguir. Después escribe un primer programa, mira qué hace java Main.java, lee una línea de entrada, prueba jshell y lee un error de compilación y una excepción en tiempo de ejecución. Cada programa de abajo se ejecutó en Java 25, y su salida está copiada de esa ejecución. Para ejecutar uno tú mismo, guárdalo como Main.java y ejecuta java Main.java.

El JDK: un compilador, un runtime y un conjunto de herramientas

El JDK, o Java Development Kit, es la única descarga que necesitas para escribir y ejecutar Java. Trae tres tipos de cosas:

  • El compilador, javac. Convierte tus archivos fuente .java en archivos .class llenos de bytecode, un conjunto compacto de instrucciones que no depende de ningún procesador en particular.
  • El runtime, que se inicia con java. Es la Java Virtual Machine (JVM) más la biblioteca estándar. La JVM carga el bytecode y lo ejecuta, y compila a código máquina las partes más usadas mientras el programa corre.
  • Las herramientas. jshell para probar código línea por línea, jar para empaquetar, javadoc para la documentación, jlink para armar un runtime recortado, y más.

Los tutoriales viejos te dicen que instales un JRE, el Java Runtime Environment, para ejecutar programas, y un JDK solo para escribirlos. Esa división ya casi no existe como descarga aparte. Desde Java 11, el proyecto OpenJDK no publica un JRE independiente. Algunos proveedores y distribuciones de Linux todavía empaquetan uno, pero el JDK contiene todo lo que tenía el JRE, así que instala el JDK.

Instalar y comprobar

Instala el paquete del JDK desde el gestor de paquetes de tu sistema operativo, como openjdk-25-jdk en Ubuntu, o descarga Eclipse Temurin 25 desde adoptium.net, que sigue publicando actualizaciones para las versiones de soporte a largo plazo. Luego, dos comandos te dicen qué runtime y qué compilador están en tu path:

$ java -version
openjdk version "25.0.4" 2026-07-21
$ javac -version
javac 25.0.4

java -version imprime dos líneas más con el build exacto, que cambia según el proveedor, así que solo se muestra la primera. Los dos números deberían empezar con 25. Si no coinciden, tienes más de un JDK instalado y tu path los mezcla. Arregla eso primero, o vas a ver errores que no tienen sentido.

Por qué esta serie usa Java 25

Java saca una versión nueva cada seis meses, en marzo y septiembre, y cada dos años una de ellas se marca como LTS, de long-term support (soporte a largo plazo). Una versión LTS recibe correcciones de seguridad y de bugs durante años. Las demás reciben correcciones durante seis meses, hasta que la siguiente versión las reemplaza.

Las versiones LTS recientes son 17, 21 y 25, y antes de ellas 11 y 8. La mayoría de las empresas usa una versión LTS, así que eso es lo que vas a encontrar en el trabajo.

Java 25 es la LTS actual, publicada en septiembre de 2025. También es la primera LTS con la forma corta de programa que usa este post. Todo en esta serie es final en Java 25, excepto una funcionalidad en preview en la parte sobre hilos virtuales, que lo aclara. Si estás en Java 21, casi toda la serie sigue sirviendo, pero el primer programa de abajo no va a compilar.

Tu primer programa Java

Un programa Java 25 puede tener tres líneas. Guarda esto como Main.java:

void main() {
    IO.println("Hello, Java 25");
}

Imprime:

Hello, Java 25

void main() es donde empieza el programa, y void significa que no devuelve nada. IO.println imprime una línea de texto. Las llaves { } encierran el cuerpo del método.

Esto se llama archivo fuente compacto. Los archivos fuente compactos y la clase IO pasaron a ser funcionalidades finales en Java 25. Pídele al compilador que trate el mismo archivo como Java 24 y se niega:

$ javac --release 24 Main.java
Main.java:1: error: implicitly declared classes are not supported in -source 24
void main() {
^
  (use -source 25 or higher to enable implicitly declared classes)
1 error

El mensaje lo llama “implicitly declared class” (clase declarada implícitamente), que es el nombre viejo de cuando la funcionalidad estaba en preview. La idea es la misma: tú no escribiste una clase, así que el compilador escribió una por ti.

El mismo programa antes de Java 25

Durante casi toda la historia de Java, el programa más pequeño se veía así:

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, Java 25");
    }
}

Imprime:

Hello, Java 25

Java 25 todavía ejecuta la forma larga, y la vas a leer en libros, en respuestas en internet y en la mayoría de los proyectos existentes. Esto es lo que hacía cada pieza:

  • public class Main { ... }: todo el código Java vive dentro de una clase. La forma compacta también tiene una, porque el compilador envuelve tu archivo en una clase con el nombre del archivo.
  • public en la clase y en el método: los hace visibles desde afuera, así que el launcher puede llamar a main. Desde Java 25, el launcher ya no exige public, así que la forma compacta lo omite.
  • static: el método pertenece a la clase, no a un objeto, así que el launcher puede llamarlo sin crear antes un Main. Desde Java 25, main puede ser un método común y el launcher crea el objeto por ti.
  • String[] args: las palabras que escribes después del nombre del programa en la línea de comandos. Todavía puedes pedirlas con void main(String[] args), pero si no las usas, no tienes que declararlas.
  • System.out.println: System es una clase, out es su stream de salida estándar y println imprime una línea en ese stream. IO.println hace el mismo trabajo escribiendo menos.

Un archivo fuente compacto también recibe todo el módulo java.base importado automáticamente, así que List, Map y compañía están listos para usar sin ninguna línea import:

void main() {
    List<String> names = List.of("Ada", "Grace");
    IO.println(names);
}

Imprime:

[Ada, Grace]

Las mismas líneas dentro de public class Main fallan con cannot find symbol hasta que agregas import java.util.List;. Esta serie usa la forma compacta en todo momento.

Qué hace realmente java Main.java

Ejecutar java Main.java compila tu archivo fuente en memoria y luego ejecuta el resultado, todo en un paso. Para verlo, toma un programa que declara un record pequeño junto a main. Los records tienen su propia parte. Por ahora, léelo como un valor con nombre que tiene un método:

record Greeting(String name) {
    String text() {
        return "Hello, " + name;
    }
}

void main() {
    var g = new Greeting("Ada");
    IO.println(g.text());
}

Imprime:

Hello, Ada

Ejecútalo en una carpeta vacía y lista los archivos antes y después. No aparece nada nuevo en el disco:

$ ls
Main.java
$ java Main.java
Hello, Ada
$ ls
Main.java

La otra forma de ejecutarlo es la que Java tuvo siempre: compilar con javac, que escribe archivos .class, y luego apuntar java a la clase compilada:

$ javac -d classes Main.java
$ ls classes
Main$Greeting.class  Main.class
$ java -cp classes Main
Hello, Ada

-d classes le dice a javac dónde poner lo que genera. -cp classes define el class path, la lista de lugares donde java busca clases compiladas. Fíjate en que el último comando nombra una clase, Main, no un archivo.

Mira los nombres de los archivos. javac generó un archivo .class por cada clase, y el record se convirtió en Main$Greeting.class. El $ significa que está anidado: en un archivo fuente compacto, todo lo que declaras vive dentro de la clase que el compilador escribió por ti. Si quitas -cp classes, java Main busca en la carpeta actual, no encuentra ningún Main.class y falla con Could not find or load main class Main.

Entonces, ¿cuándo sigues necesitando javac? Cuando quieres archivos compilados que se queden. Una aplicación real compila una vez, empaqueta sus clases en un JAR y distribuye eso. java Main.java vuelve a compilar en cada ejecución, lo que sirve para aprender y para scripts pequeños. La parte sobre paquetes, módulos y JARs arma un proyecto por el camino largo.

Explicado como si tuvieras diez años

Imagina una receta escrita en francés, y un cocinero que solo lee un código especial de cocina.

javac es un traductor que convierte toda la receta a código de cocina una sola vez y la escribe en una tarjeta. Tú guardas la tarjeta, y cualquier cocinero en cualquier cocina puede usarla una y otra vez.

java Main.java es un traductor que lee la receta en francés, la traduce en su cabeza y se la pasa directo al cocinero. El pastel se hace, pero no queda ninguna tarjeta. La próxima vez, la traducción empieza otra vez desde cero.

La versión precisa

En los dos casos, el mismo compilador hace el mismo trabajo. java Main.java ejecuta el compilador dentro del launcher, guarda las clases resultantes en memoria, las carga en la JVM y llama a main. El camino con javac escribe esas mismas clases en el disco, y un comando java posterior las carga desde ahí.

El “código de cocina” es el bytecode, y el “cocinero” es la JVM. El bytecode no depende de tu procesador ni de tu sistema operativo. Un archivo .class compilado en Linux se ejecuta en una JVM de Mac o de Windows de la misma versión o una más nueva.

Dónde falla la analogía: un cocinero sigue la tarjeta paso a paso, para siempre. La JVM no. Empieza interpretando el bytecode, observa qué métodos se ejecutan más y los compila a código máquina real mientras el programa corre. Así que hay dos compiladores: javac antes de que el programa se ejecute, y el compilador just-in-time (JIT) de la JVM durante la ejecución. La parte sobre la JVM trata el segundo.

Un programa en más de un archivo

El launcher también puede ejecutar un programa repartido en varios archivos fuente. Sigues nombrando solo el archivo que tiene main, y compila los demás archivos que necesita desde la misma carpeta. Ejecutar un solo archivo fuente directamente llegó en Java 11. Ejecutar varios llegó en Java 22.

$ cat Main.java
void main() {
    IO.println(Greeter.greet("Ada"));
}
$ cat Greeter.java
class Greeter {
    static String greet(String name) {
        return "Hello, " + name + ", from another file";
    }
}
$ java Main.java
Hello, Ada, from another file
$ ls
Greeter.java  Main.java

Main.java usaba Greeter, así que el launcher encontró Greeter.java, compiló los dos en memoria y ejecutó el programa. Otra vez, no quedó ningún archivo .class.

Leer una línea de entrada con IO.readln

IO.readln lee una línea de texto que escribe el usuario y la devuelve como String. Si le pasas una cadena, primero la imprime como prompt:

void main() {
    String name = IO.readln("What's your name? ");
    IO.println("Hello, " + name);
}

Ese programa espera a que una persona escriba, así que su salida no se puede comprobar automáticamente. Aquí hay ejecuciones reales, con la entrada enviada por un pipe desde la shell en vez de escrita:

$ echo Ada | java Main.java
What's your name? Hello, Ada
$ java Main.java < /dev/null
What's your name? Hello, null

El prompt y el saludo comparten línea. En un teclado, el Enter que escribes se muestra en pantalla y baja el cursor. La entrada por pipe no se muestra, así que nada lo baja. La salida del propio programa es la misma en los dos casos.

La segunda ejecución no tuvo ninguna entrada, y readln devolvió null, que es el valor de Java para “ningún objeto”. Unir null a una cadena da el texto null, así que el programa imprimió Hello, null en vez de fallar. Un programa real debería comprobarlo.

Probar código en jshell

jshell es la shell interactiva de Java. Escribes una expresión o una sentencia, presionas Enter y te muestra el resultado al instante, sin archivo y sin main:

$ jshell
|  Welcome to JShell -- Version 25.0.4
|  For an introduction type: /help intro

jshell> 2 + 3
$1 ==> 5

jshell> 10 / 4
$2 ==> 2

jshell> 10 / 4.0
$3 ==> 2.5

jshell> var name = "Ada"
name ==> "Ada"

jshell> "Hello, " + name
$5 ==> "Hello, Ada"

jshell> name.toUpperCase()
$6 ==> "ADA"

jshell> /exit
|  Goodbye

Cada resultado recibe un nombre. Si no le pusiste uno, jshell inventa uno, como $1, y puedes usar ese nombre en líneas posteriores. Los comandos que empiezan con barra le hablan a jshell mismo: /vars lista tus variables, /help lista el resto y /exit sale.

jshell no te obliga a terminar una línea con punto y coma, pero un archivo fuente sí. Fíjate también en 10 / 4. Dividir dos números enteros da un número entero y descarta el resto. Eso sorprende a quien viene de JavaScript o Python, y es justo el tipo de pregunta pequeña que jshell responde rápido. La parte sobre valores y tipos lo explica.

Errores de compilación: el build se detiene antes de que algo se ejecute

Un error de compilación significa que javac no pudo convertir tu código fuente en bytecode, así que no se ejecuta ni una línea de tu programa. Un punto y coma que falta es el clásico:

void main() {
    IO.println("Hello")
    IO.println("Goodbye");
}

La compilación falla con:

Main.java:2: error: ';' expected
    IO.println("Hello")
                       ^

Lee un error de javac de izquierda a derecha. Main.java:2 es el archivo y el número de línea. Después de error: viene lo que salió mal. La línea siguiente repite tu línea de código, y el caret ^ debajo apunta al lugar exacto donde el compilador se rindió. Aquí está justo después del paréntesis de cierre, donde iba un punto y coma.

“Hello” nunca se imprimió, aunque esa línea venía primero. Con java Main.java, el launcher agrega una última línea, error: compilation failed, y se detiene.

Los nombres mal escritos dan el error más común de todos:

void main() {
    String name = "Ada";
    IO.printn("Hello, " + name);
}

La compilación falla con:

Main.java:3: error: cannot find symbol
    IO.printn("Hello, " + name);
      ^

javac imprime dos líneas más debajo del caret: symbol: method printn(String) y location: class IO. Juntas dicen “busqué en IO un método llamado printn que recibe un String, y no hay ninguno”. El caret apunta al punto, justo antes del nombre que no pudo encontrar. “Symbol” (símbolo) es la palabra del compilador para cualquier nombre: una variable, un método o una clase.

Cuando tienes una pantalla llena de errores, arregla el primero y vuelve a compilar. Una llave que falta puede confundir al compilador con todo lo que viene después.

Excepciones en tiempo de ejecución: el programa arranca y luego se detiene

Una excepción en tiempo de ejecución ocurre en un programa que compiló bien. Arranca, se ejecuta y se topa con algo que no puede hacer. Aquí el programa intenta convertir en int un texto que no es un número:

void main() {
    String input = "forty-two";
    IO.println("Parsing " + input);
    int age = Integer.parseInt(input);
    IO.println("Next year you'll be " + (age + 1));
}

Imprime y se detiene:

Parsing forty-two
Exception in thread "main" java.lang.NumberFormatException: For input string: "forty-two"

Compáralo con los errores de compilación de arriba. Esta vez la primera línea sí se imprimió, porque el programa estaba corriendo. Luego Integer.parseInt lanzó una NumberFormatException, nada la atrapó y el programa terminó con un código de salida distinto de cero. La última línea nunca se ejecutó.

La línea de la excepción nombra el hilo (main), la clase de la excepción y un mensaje. Debajo, Java imprime un stack trace, una lista de líneas que empiezan con at y muestran qué método llamó a cuál. La que tienes que buscar es la primera línea que menciona tu propio archivo, aquí at Main.main(Main.java:4). Esa es la línea de tu código donde algo salió mal.

El compilador comprueba lo que puede saber a partir del código fuente: sintaxis, nombres y tipos. No puede saber que "forty-two" no es un número, porque en un programa real ese texto llega mientras el programa se ejecuta. El manejo de excepciones tiene su propia parte.

Sintaxis básica: comentarios, sentencias, llaves y mayúsculas

La sintaxis de Java se parece a la de C, C#, JavaScript y Go, así que si conoces alguno de ellos, esta sección es un repaso rápido. Este programa usa todas las piezas:

// A line comment runs to the end of the line.

/*
 * A block comment can span lines.
 */
void main() {
    int apples = 3; int pears = 4;   // two statements, one line: legal, but hard to read
    int total = apples
            + pears;                 // one statement, two lines: the semicolon ends it
    IO.println("total = " + total);
    {
        int inner = total * 2;
        IO.println("inner = " + inner);
    }
}

Imprime:

total = 7
inner = 14
  • Comentarios. // llega hasta el final de la línea, y /* ... */ puede abarcar muchas líneas. /** ... */ encima de una clase o un método es un comentario de documentación, que lee javadoc.
  • Sentencias y punto y coma. Una sentencia es una instrucción, y un punto y coma la termina. Los saltos de línea no significan nada para el compilador, así que total es una sentencia en dos líneas, y la línea de apples tiene dos. Java no inserta punto y coma automáticamente, a diferencia de JavaScript y Go.
  • Llaves. { } agrupan sentencias en un bloque: el cuerpo de un método, el cuerpo de un bucle, o simplemente un bloque suelto, como el que contiene inner. Una variable declarada dentro de un bloque solo existe dentro de él. La indentación es para las personas, y el compilador la ignora.
  • Mayúsculas y minúsculas. total, Total y TOTAL son tres nombres distintos. Por convención, las variables y los métodos empiezan con minúscula, y las clases con mayúscula, como String e IO.

Las mayúsculas también importan en main. Escríbelo con mayúscula y el launcher no tiene nada que arrancar:

void Main() {
    IO.println("Hello");
}

La compilación falla con:

Main.java:1: error: compact source file does not have main method in the form of void main() or void main(String[] args)

El mensaje nombra las dos formas que habría aceptado. Escribir string en vez de String falla con cannot find symbol, porque no existe ninguna clase llamada string.

Algunos ejemplos viejos en internet llaman a un println("Hello") suelto, sin IO. delante. Eso funcionaba mientras los archivos fuente compactos eran una funcionalidad en preview, en Java 23 y 24. En la versión final de Java 25 falla con cannot find symbol, así que escribe IO.println.

Qué recordar

  • El JDK trae el compilador (javac), el runtime (java y la JVM) y herramientas como jshell. Instala el JDK, y no busques un JRE aparte.
  • Java 25 es la versión LTS actual. Compruébalo con java -version y javac -version, y asegúrate de que los dos digan 25.
  • Un programa Java 25 puede ser void main() { IO.println("Hello"); } en Main.java. El compilador escribe la clase alrededor, y las clases de java.base como List no necesitan import.
  • java Main.java compila en memoria y ejecuta, sin dejar archivos. javac -d classes Main.java escribe archivos .class, y java -cp classes Main los ejecuta.
  • IO.readln lee una línea de entrada, y devuelve null cuando no hay más entrada.
  • Un error de compilación detiene el build antes de que se ejecute cualquier línea. Lee el archivo, la línea, el mensaje y el caret. Una excepción en tiempo de ejecución ocurre en un programa que está corriendo, después de que las líneas anteriores ya se ejecutaron.
  • Las sentencias terminan con punto y coma, las llaves forman bloques y los nombres distinguen entre mayúsculas y minúsculas.

Java 25 te deja empezar con tres líneas, y el compilador igual revisa cada una de ellas antes de que algo se ejecute.

¿Qué tan útil te resultó este post?

¡Haz clic en un corazón para calificar!

Calificación promedio 0 / 5. Total de votos: 0

Todavía no hay votos. Sé el primero en calificar este post.