Blog

Olá, Java 25: rode um arquivo Java sem cerimônia

O Java 25 roda um único arquivo-fonte com um comando e um void main simples, sem nenhuma classe para escrever antes. Veja o que o JDK contém, por que o Java 25 é a versão para aprender e como ler erros de compilação e exceções.

Um programa Java precisava de cinco linhas de cerimônia antes de imprimir uma palavra. O Java 25 reduziu isso para três. Você escreve um void main(), chama IO.println e roda o arquivo com um comando. A forma antiga continua funcionando, e você vai vê-la em quase todo código que já existe. Por isso este post mostra as duas e explica para que servia cada pedaço removido.

Este post explica o que é o JDK e qual versão instalar. Depois escreve um primeiro programa, olha o que java Main.java faz, lê uma linha de entrada, experimenta o jshell e lê um erro de compilação e uma exceção em tempo de execução. Todo programa abaixo rodou no Java 25, e a saída foi colada da execução. Para rodar um deles, salve como Main.java e rode java Main.java.

O JDK: um compilador, um runtime e um conjunto de ferramentas

O JDK, ou Java Development Kit, é o único download de que você precisa para escrever e rodar Java. Ele traz três tipos de coisa:

  • O compilador, javac. Ele transforma seus arquivos-fonte .java em arquivos .class cheios de bytecode, um conjunto compacto de instruções que não depende de nenhum processador específico.
  • O runtime, iniciado com java. É a Java Virtual Machine (JVM) mais a biblioteca padrão. A JVM carrega o bytecode e o executa, e compila as partes mais usadas para código de máquina enquanto o programa roda.
  • As ferramentas. jshell para testar código linha por linha, jar para empacotar, javadoc para documentação, jlink para montar um runtime enxuto, e outras.

Tutoriais mais antigos mandam instalar um JRE, o Java Runtime Environment, para rodar programas, e um JDK só para escrevê-los. Essa divisão, na maior parte, já não é um download separado. Desde o Java 11, o projeto OpenJDK não publica um JRE avulso. Alguns fornecedores e distribuições Linux ainda empacotam um, mas o JDK contém tudo o que o JRE tinha. Então instale o JDK.

Instalando e conferindo

Instale o pacote do JDK pelo gerenciador de pacotes do seu sistema operacional, como openjdk-25-jdk no Ubuntu, ou baixe o Eclipse Temurin 25 em adoptium.net, que continua publicando atualizações para as versões de suporte de longo prazo. Depois, dois comandos mostram qual runtime e qual compilador estão no seu path:

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

java -version imprime mais duas linhas com o build exato, que muda de um fornecedor para outro, então só a primeira linha aparece aqui. Os dois números devem começar com 25. Se não baterem, você tem mais de um JDK instalado e o seu path mistura os dois. Corrija isso antes, ou você vai ver erros que não fazem sentido.

Por que esta série usa o Java 25

O Java lança uma versão nova a cada seis meses, em março e setembro, e a cada dois anos uma delas é marcada como LTS, de long-term support. Uma versão LTS recebe correções de segurança e de bugs por anos. As outras recebem correções por seis meses, até a próxima versão substituí-las.

As versões LTS recentes são 17, 21 e 25, com 11 e 8 antes delas. A maioria das empresas roda uma versão LTS, então é isso que você vai encontrar no trabalho.

O Java 25 é a LTS atual, lançada em setembro de 2025. É também a primeira LTS com a forma curta de programa que este post usa. Tudo nesta série é final no Java 25, menos um recurso em preview na parte sobre virtual threads, que avisa isso. Se você está no Java 21, quase toda a série ainda vale, mas o primeiro programa abaixo não compila.

Seu primeiro programa Java

Um programa em Java 25 pode ter três linhas. Salve isto como Main.java:

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

Ele imprime:

Hello, Java 25

void main() é onde o programa começa, e void quer dizer que ele não devolve nada. IO.println imprime uma linha de texto. As chaves { } guardam o corpo do método.

Isso se chama arquivo-fonte compacto (compact source file). Arquivos-fonte compactos e a classe IO viraram recursos finais no Java 25. Peça ao compilador para tratar o mesmo arquivo como Java 24 e ele se recusa:

$ 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

A mensagem fala em “implicitly declared class”, o nome antigo da época em que o recurso estava em preview. A ideia é a mesma: você não escreveu uma classe, então o compilador escreveu uma para você.

O mesmo programa antes do Java 25

Durante quase toda a história do Java, o menor programa era assim:

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

Ele imprime:

Hello, Java 25

O Java 25 ainda roda a forma longa, e você vai lê-la em livros, em respostas na internet e na maioria dos projetos existentes. Veja o que cada pedaço fazia:

  • public class Main { ... }: todo código Java fica dentro de uma classe. A forma compacta ainda tem uma, porque o compilador embrulha o seu arquivo numa classe com o nome do arquivo.
  • public na classe e no método: deixa os dois visíveis de fora, para que o launcher possa chamar main. A partir do Java 25, o launcher não exige mais public, então a forma compacta o deixa de fora.
  • static: o método pertence à classe, não a um objeto, então o launcher pode chamá-lo sem criar um Main antes. A partir do Java 25, main pode ser um método comum, e o launcher cria o objeto para você.
  • String[] args: as palavras digitadas depois do nome do programa na linha de comando. Você ainda pode pedi-las com void main(String[] args), mas, se não for usá-las, não precisa declará-las.
  • System.out.println: System é uma classe, out é o stream de saída padrão dela, e println imprime uma linha nesse stream. IO.println faz o mesmo trabalho com menos digitação.

Um arquivo-fonte compacto também recebe o módulo java.base inteiro importado automaticamente, então List, Map e companhia estão prontos para usar, sem nenhuma linha de import:

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

Ele imprime:

[Ada, Grace]

As mesmas linhas dentro de public class Main falham com cannot find symbol até você adicionar import java.util.List;. Esta série usa a forma compacta do começo ao fim.

O que java Main.java faz de verdade

Rodar java Main.java compila o seu arquivo-fonte na memória e depois executa o resultado, tudo num passo só. Para ver isso acontecer, pegue um programa que declara um pequeno record ao lado de main. Records têm uma parte só deles. Por enquanto, leia um record como um valor com nome e um método:

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

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

Ele imprime:

Hello, Ada

Rode numa pasta vazia e liste os arquivos antes e depois. Nada novo aparece no disco:

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

A outra forma de rodar é a que o Java sempre teve: compilar com javac, que grava arquivos .class, e depois apontar java para a classe compilada:

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

-d classes diz ao javac onde colocar a saída. -cp classes define o class path, a lista de lugares onde java procura classes compiladas. Repare que o último comando nomeia uma classe, Main, e não um arquivo.

Olhe os nomes dos arquivos. O javac gerou um arquivo .class por classe, e o record virou Main$Greeting.class. O $ indica que ele é aninhado: num arquivo-fonte compacto, tudo o que você declara fica dentro da classe que o compilador escreveu para você. Tire o -cp classes e java Main procura na pasta atual, não encontra nenhum Main.class e falha com Could not find or load main class Main.

Então quando você ainda precisa do javac? Quando quer arquivos compilados para guardar. Uma aplicação de verdade compila uma vez, empacota as classes num JAR e distribui esse JAR. java Main.java compila de novo a cada execução, o que serve bem para aprender e para scripts pequenos. A parte sobre pacotes, módulos e JARs monta um projeto do jeito longo.

Explicado como se você tivesse dez anos

Imagine uma receita escrita em francês e um cozinheiro que só lê um código especial de cozinha.

O javac é um tradutor que passa a receita inteira para o código de cozinha uma vez e escreve tudo num cartão. Você guarda o cartão, e qualquer cozinheiro em qualquer cozinha pode usá-lo de novo, quantas vezes quiser.

java Main.java é um tradutor que lê a receita em francês, traduz de cabeça e passa direto para o cozinheiro. O bolo sai, mas nenhum cartão fica para trás. Na próxima vez, a tradução começa do zero.

A versão precisa

Nos dois casos, o mesmo compilador faz o mesmo trabalho. java Main.java roda o compilador dentro do launcher, mantém as classes resultantes na memória, carrega essas classes na JVM e chama main. O caminho do javac grava essas mesmas classes no disco, e um comando java depois as carrega de lá.

O “código de cozinha” é o bytecode, e o “cozinheiro” é a JVM. O bytecode não depende do seu processador nem do sistema operacional. Um arquivo .class compilado no Linux roda numa JVM de Mac ou Windows da mesma versão ou mais nova.

Onde a analogia falha: um cozinheiro segue o cartão passo a passo, para sempre. A JVM não. Ela começa interpretando o bytecode, observa quais métodos rodam mais e compila esses métodos para código de máquina de verdade enquanto o programa está rodando. Então existem dois compiladores: o javac, antes de o programa rodar, e o compilador just-in-time da JVM, durante a execução. A parte sobre a JVM trata do segundo.

Um programa em mais de um arquivo

O launcher também consegue rodar um programa espalhado por vários arquivos-fonte. Você continua nomeando só o arquivo com main, e ele compila os outros arquivos de que precisa, da mesma pasta. Rodar um único arquivo-fonte direto chegou no Java 11. Rodar vários chegou no 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 usou Greeter, então o launcher encontrou Greeter.java, compilou os dois na memória e rodou o programa. De novo, nenhum arquivo .class ficou para trás.

Lendo uma linha de entrada com IO.readln

IO.readln lê uma linha de texto que o usuário digita e a devolve como String. Se você passar uma string, ele a imprime antes, como prompt:

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

Esse programa espera uma pessoa digitar, então a saída dele não pode ser conferida automaticamente. Aqui estão execuções reais, com a entrada vinda por pipe do shell em vez de digitada:

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

O prompt e a saudação dividem a mesma linha. No teclado, o Enter que você digita aparece na tela e desce o cursor. A entrada por pipe não aparece na tela, então nada desce o cursor. A saída do próprio programa é a mesma nos dois casos.

A segunda execução não teve entrada nenhuma, e readln devolveu null, que é o valor do Java para “nenhum objeto”. Juntar null a uma string dá o texto null, então o programa imprimiu Hello, null em vez de quebrar. Um programa de verdade deve verificar isso.

Testando código no jshell

O jshell é o shell interativo do Java. Você digita uma expressão ou uma instrução, aperta Enter, e ele mostra o resultado na hora, sem arquivo e sem 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 ganha um nome. Se você não deu um nome, o jshell inventa um, como $1, e você pode usar esse nome nas linhas seguintes. Comandos que começam com barra falam com o próprio jshell: /vars lista suas variáveis, /help lista o resto, e /exit sai.

O jshell não obriga você a terminar a linha com ponto e vírgula, mas um arquivo-fonte obriga. Repare também em 10 / 4. Dividir dois números inteiros dá um número inteiro e descarta o resto. Isso surpreende quem vem de JavaScript ou Python, e é exatamente o tipo de dúvida pequena que o jshell responde rápido. A parte sobre valores e tipos explica isso.

Erros de compilação: o build para antes de qualquer coisa rodar

Um erro de compilação quer dizer que o javac não conseguiu transformar o seu código-fonte em bytecode, então nenhuma linha do programa roda. Um ponto e vírgula faltando é o caso clássico:

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

O build falha com:

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

Leia um erro do javac da esquerda para a direita. Main.java:2 é o arquivo e o número da linha. Depois de error: vem o que deu errado. A linha seguinte repete a sua linha de código, e o acento circunflexo ^ embaixo dela aponta o ponto exato onde o compilador desistiu. Aqui é logo depois do parêntese de fechamento, onde deveria haver um ponto e vírgula.

“Hello” nunca foi impresso, mesmo sendo a primeira linha. Com java Main.java, o launcher acrescenta uma última linha, error: compilation failed, e para.

Nomes com erro de digitação dão o erro mais comum de todos:

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

O build falha com:

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

O javac imprime mais duas linhas embaixo do acento circunflexo: symbol: method printn(String) e location: class IO. Juntas, elas dizem “procurei em IO um método chamado printn que recebe uma String, e não existe nenhum”. O acento circunflexo aponta para o ponto, logo antes do nome que ele não encontrou. “Symbol” é a palavra do compilador para qualquer nome: uma variável, um método ou uma classe.

Quando aparecer uma tela cheia de erros, corrija o primeiro e compile de novo. Uma chave faltando pode confundir o compilador sobre tudo o que vem depois.

Exceções em tempo de execução: o programa começa e depois para

Uma exceção em tempo de execução acontece num programa que compilou sem problema. Ele começa, roda e encontra algo que não consegue fazer. Aqui o programa tenta transformar em int um texto que não é 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));
}

Ele imprime e para:

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

Compare com os erros de compilação acima. Desta vez a primeira linha foi impressa, porque o programa estava rodando. Depois Integer.parseInt lançou uma NumberFormatException, ninguém a capturou, e o programa terminou com um código de saída diferente de zero. A última linha nunca rodou.

A linha da exceção nomeia a thread (main), a classe da exceção e uma mensagem. Embaixo dela, o Java imprime um stack trace, uma lista de linhas começando com at que mostram qual método chamou qual. A que você deve procurar é a primeira linha que menciona o seu próprio arquivo, aqui at Main.main(Main.java:4). Essa é a linha do seu código onde as coisas deram errado.

O compilador verifica o que dá para saber pelo código-fonte: sintaxe, nomes e tipos. Ele não tem como saber que "forty-two" não é um número, porque num programa de verdade esse texto chega enquanto o programa roda. O tratamento de exceções ganha uma parte só dele.

O básico da sintaxe: comentários, instruções, chaves e maiúsculas

A sintaxe do Java lembra C, C#, JavaScript e Go, então, se você conhece alguma dessas, esta seção é uma conferida rápida. Este programa usa todas as peças:

// 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);
    }
}

Ele imprime:

total = 7
inner = 14
  • Comentários. // vai até o fim da linha, e /* ... */ pode cobrir várias linhas. /** ... */ acima de uma classe ou de um método é um comentário de documentação, lido pelo javadoc.
  • Instruções e ponto e vírgula. Uma instrução é uma ordem só, e um ponto e vírgula a encerra. Quebras de linha não significam nada para o compilador, então total é uma instrução em duas linhas, e a linha de apples tem duas. O Java não insere ponto e vírgula automaticamente, ao contrário de JavaScript e Go.
  • Chaves. { } agrupam instruções num bloco: o corpo de um método, o corpo de um laço ou só um bloco sozinho, como o que guarda inner. Uma variável declarada dentro de um bloco só existe dentro dele. A indentação é para as pessoas, e o compilador a ignora.
  • Maiúsculas e minúsculas. total, Total e TOTAL são três nomes diferentes. Por convenção, variáveis e métodos começam com letra minúscula, e classes começam com maiúscula, como String e IO.

Maiúsculas também importam para main. Escreva com inicial maiúscula e o launcher não tem o que iniciar:

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

O build falha com:

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

A mensagem nomeia as duas formas que seriam aceitas. Escrever string em vez de String falha com cannot find symbol, porque não existe nenhuma classe chamada string.

Exemplos mais antigos na internet às vezes chamam um println("Hello") puro, sem IO. na frente. Isso funcionava enquanto os arquivos-fonte compactos eram um recurso em preview, no Java 23 e 24. No Java 25 final, falha com cannot find symbol, então escreva IO.println.

O que lembrar

  • O JDK traz o compilador (javac), o runtime (java e a JVM) e ferramentas como o jshell. Instale o JDK e não procure um JRE separado.
  • O Java 25 é a versão LTS atual. Confira com java -version e javac -version, e garanta que os dois digam 25.
  • Um programa em Java 25 pode ser void main() { IO.println("Hello"); } em Main.java. O compilador escreve a classe em volta, e classes de java.base como List não precisam de import.
  • java Main.java compila na memória e roda, sem deixar arquivos. javac -d classes Main.java grava arquivos .class, e java -cp classes Main os executa.
  • IO.readln lê uma linha de entrada e devolve null quando não há mais entrada.
  • Um erro de compilação para o build antes de qualquer linha rodar. Leia o arquivo, a linha, a mensagem e o acento circunflexo. Uma exceção em tempo de execução acontece num programa rodando, depois que linhas anteriores já rodaram.
  • Instruções terminam com ponto e vírgula, chaves formam blocos, e nomes diferenciam maiúsculas de minúsculas.

O Java 25 deixa você começar com três linhas, e o compilador ainda confere cada uma delas antes de qualquer coisa rodar.

Quanto este post te ajudou?

Clique em um coração para avaliar!

Média das avaliações 0 / 5. Total de votos: 0

Nenhum voto até agora. Seja o primeiro a avaliar este post.