Durante años, el primer programa en Java fue también el primer obstáculo: una clase pública, un método public static void main(String[] args) y una llamada a System.out.println, cuatro conceptos que no tienen nada que ver con lo que el principiante quiere hacer, que es mostrar un texto en pantalla. Con Java 25, publicado en septiembre de 2025 como versión de soporte a largo plazo, eso ha cambiado de forma definitiva: la JEP 512 (ficheros fuente compactos y métodos main de instancia) dejó de ser una característica en preview y pasó a formar parte del lenguaje, sin --enable-preview y sin banderas especiales.
Este artículo recorre lo que ofrece la versión final, qué reglas impone el lanzador, cómo funciona la nueva clase java.lang.IO y cómo se ve todo junto en un ejemplo completo, un juego de adivinar el número, escrito en dos versiones: la moderna y la clásica con Scanner. Si buscas un repaso más amplio de las novedades recientes del lenguaje, tienes Java moderno en 2026: records, sealed classes, pattern matching y text blocks.
De cuatro conceptos a cero: el hola mundo antes y después
El programa clásico necesita declarar una clase, hacer público su método principal, marcarlo como estático, recibir un array de cadenas que casi nunca se usa y acceder a la salida estándar a través de System.out:
public class Hola {
public static void main(String[] args) {
System.out.println("Hola, mundo");
}
}
En Java 25 el mismo programa cabe en un fichero Hola.java con esto:
void main() {
IO.println("Hola, mundo");
}
Sin clase explícita, sin modificadores, sin parámetros y sin import. Se ejecuta directamente con el lanzador, sin compilar antes:
java Hola.java
Qué dice exactamente la JEP 512
La propuesta tiene un recorrido largo. Se presentó como preview en la JEP 445 (JDK 21), se refinó en la JEP 463 (JDK 22) y en la JEP 477 (JDK 23), pasó por una cuarta preview en la JEP 495 (JDK 24), donde se llamaba «ficheros fuente simples», y quedó finalizada en la JEP 512 (JDK 25), que además cambió el nombre a «ficheros fuente compactos». Entre las modificaciones respecto a las previews hay tres que conviene conocer:
IOpasó al paquetejava.lang, de modo que cualquier fichero la ve sin importar nada.- Los métodos estáticos de
IOno se importan automáticamente, así que hay que escribirIO.println(...)salvo que se añada unimport static java.lang.IO.*;explícito. IOse apoya enSystem.outySystem.in, no enjava.io.Consolecomo en las previews.
El objetivo declarado es doble: que quien empieza pueda escribir programas pequeños sin aprender las características de los grandes, y que esos programas puedan crecer hasta convertirse en clases normales sin pasar por un dialecto distinto del lenguaje.
Reglas de un fichero fuente compacto
Un fichero compacto es aquel que contiene métodos y campos fuera de cualquier declaración de clase. El compilador los envuelve en una clase implícita que cumple estas condiciones:
- Es una clase de nivel superior,
finaly situada en el paquete sin nombre. - Extiende
java.lang.Objecty no implementa ninguna interfaz. - Tiene un único constructor por defecto, sin parámetros, y no se pueden declarar otros.
- Sus miembros son los campos y métodos escritos en el fichero.
- Su nombre depende de la implementación y no se puede usar en el código, así que no se puede instanciar con
new, aunquethissí está disponible. - Debe contener un método
mainque el lanzador pueda invocar; si no, es un error de compilación.
Hay una segunda ventaja menos comentada: todo fichero compacto se trata como si empezara con import module java.base;, la declaración de importación de módulos de la JEP 511, también final en JDK 25. Eso hace visibles sin import las clases e interfaces públicas de los paquetes que exporta java.base: List, Map, Random, Scanner, Path, LocalDate y el resto.
Cómo elige el lanzador el método main
El método main ya no tiene que ser público ni estático ni recibir String[]. El lanzador aplica este orden:
- Si la clase declara o hereda un
maincon parámetroString[], usa ese. - Si no, y declara o hereda un
mainsin parámetros, usa ese. - Si no hay ninguno, informa de un error y termina.
Si el método elegido es estático, se invoca directamente. Si es de instancia, la clase necesita un constructor no privado sin argumentos: el lanzador crea un objeto y llama a main sobre él. Sin ese constructor, el lanzador falla.
Esto último no es exclusivo de los ficheros compactos. Funciona también en una clase normal, que además puede guardar estado en campos:
class Contador {
int cuenta = 0;
void main() {
cuenta++;
IO.println("Cuenta: " + cuenta);
}
}
Guardada como Contador.java, se ejecuta con java Contador.java y escribe Cuenta: 1. La clase no tiene que ser public, y IO sigue estando disponible porque vive en java.lang.
La clase java.lang.IO
La clase IO existe desde Java 25 y ofrece acceso cómodo, orientado a líneas, a la entrada y la salida estándar. Es final y solo contiene métodos estáticos:
Método | Qué hace |
| Escribe la representación textual del objeto, sin salto de línea. |
| Igual que el anterior y añade el separador de línea. |
| Escribe solo el separador de línea. |
| Lee una línea de la entrada estándar, sin el terminador. |
| Escribe el mensaje como |
Hay tres detalles de la documentación oficial que evitan sorpresas:
readlndevuelvenullsi se llega al final del flujo sin haber leído ningún carácter, por ejemplo cuando se redirige la entrada desde un fichero que se acaba o se pulsa Ctrl+D en Linux y macOS. Un programa que no lo contemple acabará con unNullPointerExceptionen cuanto llame a un método sobre el resultado.- Los errores de entrada o salida se notifican con
java.io.IOError, que es unErrory por tanto no obliga a capturar nada. - La decodificación de la entrada usa el valor de la propiedad
stdin.encodingy, si no existe o no se puede cargar, UTF-8. Los bytes mal formados se sustituyen por el carácter de reemplazo en lugar de lanzar un error.
La documentación añade además una advertencia importante: tras la primera llamada a readln, cualquier uso directo de System.in produce un comportamiento no especificado. Dicho de otro modo, o se lee todo con IO.readln o se lee todo con Scanner o BufferedReader, pero no se mezclan en el mismo programa.
Ejecutar sin compilar: el modo de fichero fuente
El comando java Fichero.java no es nuevo. La JEP 330 introdujo en JDK 11 la ejecución de un único fichero fuente: el lanzador lo compila en memoria y lo ejecuta, sin generar ningún .class. La JEP 458, final en JDK 22, amplió el mecanismo a programas con varios ficheros: el lanzador busca las clases referenciadas en el árbol de fuentes, deduce la raíz a partir del paquete del fichero inicial y compila en memoria solo lo que se necesita, de modo que los errores de compilación pueden aparecer cuando el programa ya está en marcha.
Si prefieres generar los .class, javac Hola.java también acepta ficheros compactos. Y en las versiones anteriores a Java 25 el mismo código necesitaba --enable-preview, y en modo fichero fuente además --source N con la versión correspondiente. Con Java 25 o posterior basta con java Hola.java. Para comprobar qué JDK tienes activo, java -version debe mostrar 25 o una versión superior.
Ejemplo completo: adivina el número
Un juego de adivinar un número aprovecha todo lo anterior: estado, bucle, lectura de teclado y validación. Este es el programa en un único fichero Adivina.java:
static final int MAXIMO = 100;
void main() {
var secreto = new Random().nextInt(1, MAXIMO + 1);
var intentos = 0;
IO.println("He elegido un número entre 1 y " + MAXIMO + ". ¿Cuál es?");
while (true) {
var entrada = IO.readln("Tu número: ");
if (entrada == null) {
IO.println("nEntrada cerrada, lo dejamos aquí.");
return;
}
int numero;
try {
numero = Integer.parseInt(entrada.strip());
} catch (NumberFormatException e) {
IO.println("Eso no es un entero válido.");
continue;
}
intentos++;
if (numero < secreto) {
IO.println("Más alto.");
} else if (numero > secreto) {
IO.println("Más bajo.");
} else {
IO.println("¡Acertado! Te ha costado " + intentos + " intentos.");
return;
}
}
}
Se ejecuta con java Adivina.java y no hay nada que importar. Random viene de java.base gracias a la importación implícita del módulo, y nextInt(1, MAXIMO + 1) usa la sobrecarga con origen y límite superior exclusivo de RandomGenerator, así que el resultado va de 1 a 100 ambos incluidos. Algunos puntos del código:
varinfiere el tipo de las variables locales (intensecretoeintentos,Stringenentrada). Es una característica de Java 10 y no tiene relación con la JEP, pero encaja bien en programas cortos.- El campo
MAXIMOes un miembro de la clase implícita, y por eso se declara fuera del método, igual que se haría en una clase normal. - La comprobación de
nullcubre el fin de flujo, que es la única forma en quereadlndevuelve ese valor. - La variable
numerose declara sin inicializar y se asigna dentro deltry. Como elcatchtermina concontinue, el compilador sabe que después del bloque siempre está asignada.
La versión clásica con Scanner
Para comparar, este es el mismo juego en el estilo tradicional, válido en cualquier versión desde Java 17. Necesita su clase pública, sus importaciones, el main estático y un Scanner sobre System.in:
import java.util.Random;
import java.util.Scanner;
public class AdivinaClasico {
static final int MAXIMO = 100;
public static void main(String[] args) {
var secreto = new Random().nextInt(1, MAXIMO + 1);
var intentos = 0;
try (var teclado = new Scanner(System.in)) {
System.out.println("He elegido un número entre 1 y " + MAXIMO + ". ¿Cuál es?");
while (true) {
System.out.print("Tu número: ");
if (!teclado.hasNextLine()) {
return;
}
int numero;
try {
numero = Integer.parseInt(teclado.nextLine().strip());
} catch (NumberFormatException e) {
System.out.println("Eso no es un entero válido.");
continue;
}
intentos++;
if (numero < secreto) {
System.out.println("Más alto.");
} else if (numero > secreto) {
System.out.println("Más bajo.");
} else {
System.out.println("¡Acertado! Te ha costado " + intentos + " intentos.");
return;
}
}
}
}
}
La lógica es idéntica. Cambian el ruido de la ceremonia (declaración de clase, modificadores, importaciones) y la forma de detectar el fin de entrada, que con Scanner se hace con hasNextLine() en lugar de comparar con null. Ambas versiones funcionan en JDK 25, pero la primera deja ver la lógica del juego de un vistazo, que es justo el propósito de la JEP.
Cuándo conviene salir del fichero compacto
Los ficheros compactos están pensados para programas pequeños, scripts, prácticas y pruebas rápidas. Su clase vive en el paquete sin nombre, no se puede instanciar ni referenciar por nombre desde otros ficheros y tiene un solo constructor. En cuanto el programa necesite varios tipos, paquetes, pruebas con JUnit o una herramienta de construcción como Maven o Gradle, lo natural es envolver el contenido en una clase con nombre. El paso es mecánico: se añade la declaración class Adivina { ... } alrededor de los miembros, y el main de instancia puede mantenerse tal cual, como en el ejemplo de Contador. No hace falta reescribir nada más.
Qué sigue siendo preview
Conviene distinguir lo que ya es estable de lo que no. Los ficheros compactos, los main de instancia, java.lang.IO, la importación de módulos (JEP 511) y la ejecución de programas multifichero (JEP 458) son características finales: no requieren ninguna bandera. En cambio, según la página oficial de JDK 26, publicado el 17 de marzo de 2026, siguen en preview o incubación la codificación PEM de objetos criptográficos (JEP 524, segunda preview), la concurrencia estructurada (JEP 525, sexta preview), las constantes perezosas (JEP 526, segunda preview), los tipos primitivos en patrones, instanceof y switch (JEP 530, cuarta preview) y la API Vector (JEP 529, undécima incubación). Sobre la concurrencia estructurada hay un artículo específico: Concurrencia estructurada en JDK 27: StructuredTaskScope con ejemplos prácticos.
Resumen práctico
- Con JDK 25 o posterior, un programa Java puede ser un fichero con un método
void main()y nada más. - Las salidas y entradas básicas se hacen con
IO.println,IO.printyIO.readln, sin importar nada, aunque las llamadas llevan el prefijoIO.salvo que se useimport static. readlndevuelvenullal final del flujo, y no debe combinarse con otras lecturas deSystem.inen el mismo programa.java Fichero.javacompila en memoria y ejecuta, y desde JDK 22 también admite programas de varios ficheros.- Con una clase normal, el
mainde instancia también funciona siempre que haya un constructor sin argumentos que no sea privado. - El salto de un programa compacto a una clase con nombre es mínimo, y esa continuidad es lo que busca la JEP 512.
Imagen: Pexels / Lukas Blazek
