Java Novidades do 17 ao 25

O Java ganha uma versão nova a cada seis meses, e a cada dois anos uma delas é LTS (com suporte longo): 17, 21 e 25. Esta aula é um mapa do que mudou entre o Java 17 e o Java 25, o que este guia ensina. Cada novidade tem o selo da versão em que ficou pronta, e as principais têm um exemplo rodado no JDK 25 da máquina da Ana.

Você vai aprender
  • A diferença entre um recurso final, um preview e um incubator, e como ligar um preview (--enable-preview), com as mensagens reais.
  • Uma tabela, versão por versão, das novidades de linguagem e de biblioteca do 17 ao 25.
  • Exemplos curtos das novidades que mais mudam o dia a dia: void main() e IO, import module, construtores flexíveis, getFirst()/reversed(), o _ sem nome e os scoped values.
  • O que aconteceu com os String templates (STR."…"): foram retirados.

Se você quer saber o que é LTS e qual versão instalar, veja antes Versões e LTS. Tudo nesta página foi conferido no laboratório:

$ java -version
openjdk version "25.0.4.1" 2026-08-18 LTS
OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS, mixed mode, sharing)

A ideia antes do código: final, preview e incubator

Pense numa receita nova num restaurante. Antes de entrar no cardápio, ela é servida como prato do dia, para os clientes provarem e darem opinião; o chef pode mudar a receita ou até tirá-la de vez. Só depois de aprovada ela entra no cardápio fixo.

O Java faz igual. Cada mudança é descrita num documento chamado JEP (JDK Enhancement Proposal, "proposta de melhoria do JDK"), com um número. E passa por estágios:

EstágioO que quer dizerPosso usar?
Final (permanente)Entrou no cardápio fixo. Não muda mais.Sim, normalmente.
Preview (prévia)Prato do dia: completo, mas pode mudar ou sumir na próxima versão.Só ligando a opção --enable-preview. Bom para experimentar, não para produção.
Incubator (incubadora)Uma biblioteca ainda em teste, num módulo separado jdk.incubator.….Só adicionando o módulo à mão.

Tentando usar um preview sem a opção

No JDK 25, os padrões com tipos primitivos (como case int n) ainda são preview. Veja o que o javac diz:

public class Main {
    public static void main(String[] args) {
        int nota = 8;
        String r = switch (nota) {
            case int n when n >= 7 -> "aprovado";
            case int n -> "reprovado";
        };
        System.out.println(r);
    }
}
$ javac Main.java
Main.java:5: error: primitive patterns are a preview feature and are disabled by default.
            case int n when n >= 7 -> "aprovado";
                 ^
  (use --enable-preview to enable primitive patterns)
1 error

"Padrões primitivos são um recurso em prévia e estão desligados por padrão (use --enable-preview para ligar)." Para APIs em prévia, a mensagem é parecida: error: StructuredTaskScope is a preview API and is disabled by default.

Ligando o preview

A opção precisa ir nos dois lugares: no javac (junto com --release, a versão exata) e no java:

$ javac --enable-preview Main.java
error: --enable-preview must be used with either -source or --release
$ javac --release 25 --enable-preview Main.java
Note: Main.java uses preview features of Java SE 25.
Note: Recompile with -Xlint:preview for details.
$ java Main
Erro: o método main não foi encontrado na classe Main; defina o método main como:
   public static void main(String[] args)
ou uma classe de aplicativo JavaFX deve expandir java.lang.UnsupportedClassVersionError: Preview features are not enabled for Main (class file version 69.65535). Try running with '--enable-preview'
$ java --enable-preview Main
aprovado
$ java --enable-preview Main.java      « modo arquivo-fonte: compila e roda numa tacada
aprovado

Repare na terceira tentativa: compilou com preview, mas rodou sem a opção. A mensagem do lançador sai embaralhada (começa com o aviso de main em português e termina no meio da frase), mas o final diz o que importa: "recursos em prévia não estão ligados para Main; tente rodar com --enable-preview". O número 69.65535 é a versão do arquivo .class: 69 é o Java 25, e 65535 é a marca de "usa preview".

Preview prende você a uma versão

Um .class compilado com preview do Java 25 só roda no Java 25 (nem no 26). Por isso, este guia ensina apenas recursos finais, e a tabela abaixo marca os previews do JDK 25 separadamente.


A tabela: do Java 17 ao 25

Linguagem = o que muda na forma de escrever código; API = classes e métodos novos na biblioteca; ferramentas = java, javac, javadoc… Só aparecem os recursos finais de cada versão (o número entre parênteses é o da JEP).

VersãoLinguagemAPI e ferramentas
Java 17
set/2021 · LTS
Classes seladas: sealed, permits, non-sealed (409) → aula.
Chegaram entre o 11 e o 17 e você já usa: switch com setas (14) → aula; text blocks """ (15) → aula; records (16) → aula; instanceof com padrão (16) → aula.
Novos geradores de números aleatórios, RandomGenerator (356). As partes internas do JDK ficam sempre trancadas para reflexão (403) → aula.
Java 18
mar/2022
— UTF-8 é o padrão para ler e escrever texto em qualquer sistema (400). Servidor web simples de linha de comando, jwebserver (408). Exemplos de código no Javadoc com @snippet (413).
Java 19 e Java 20
set/2022, mar/2023
Nenhum recurso de linguagem final: foram as versões de "prato do dia" das threads virtuais, dos record patterns e dos padrões no switch, que ficaram prontos no 21.
Java 21
set/2023 · LTS
Padrões no switch, com when e case null (441); record patterns (440) → aula. Threads virtuais (444) → aula. Coleções sequenciadas: getFirst(), getLast(), reversed(), addFirst() (431) → exemplo abaixo.
Java 22
mar/2024
Variáveis e padrões sem nome: _ (456) → exemplo abaixo. Rodar programas de vários arquivos direto com java Main.java (458). API de funções e memória externas (Foreign Function & Memory), para chamar código C sem JNI (454).
Java 23
set/2024
— Comentários de documentação em Markdown, com /// (467) → aula. Os String templates, que estavam em preview, foram retirados (veja abaixo).
Java 24
mar/2025
— Stream gatherers: operações intermediárias novas, como Gatherers.windowFixed (485) → streams. API para ler e gerar arquivos .class (484). Threads virtuais não ficam mais "presas" em synchronized (491). Criptografia resistente a computadores quânticos, ML-KEM e ML-DSA (496, 497).
Java 25
set/2025 · LTS
Arquivos-fonte compactos e void main() de instância, com a classe IO (512) → exemplo abaixo e Olá, Mundo. import module (511). Corpos de construtor flexíveis: código antes do super(…) (513). Scoped values (506) → exemplo abaixo. Cabeçalhos de objeto compactos, que economizam memória (519). API de derivação de chaves, KDF (510).

Em preview (ou incubadora) no JDK 25

Conferido no laboratório: estes não funcionam sem --enable-preview no JDK 25.

RecursoEstágio no 25Mensagem sem a opção
Tipos primitivos em padrões, instanceof e switch (507)3ª préviaprimitive patterns are a preview feature and are disabled by default.
Concorrência estruturada, StructuredTaskScope (505)5ª préviaStructuredTaskScope is a preview API and is disabled by default.
Valores estáveis, StableValue (502)1ª préviaStableValue is a preview API and is disabled by default.
Codificação PEM de chaves e certificados (470)1ª prévia(API em prévia)
Vector API, jdk.incubator.vector (508)incubadora (10ª)(módulo separado)

As novidades na prática

void main() e IO Java 25+

A mudança mais visível para quem começa. Desde sempre, o menor programa Java precisava de uma classe, de public static void main(String[] args) e de System.out.println. No Java 25, um arquivo pode ter só o método main, sem classe em volta (um arquivo-fonte compacto), e a classe IO ("entrada e saída") oferece IO.println, IO.print e IO.readln:

void main() {
    IO.println("Olá, Ana!");
    IO.println("2 + 3 = " + somar(2, 3));
}

int somar(int a, int b) {
    return a + b;
}
$ java Ola.java
Olá, Ana!
2 + 3 = 5
$ javac Ola.java && java Ola
Olá, Ana!
2 + 3 = 5
$ ls
Ola.class
Ola.java

Por baixo, o Java cria uma classe chamada Ola (o nome do arquivo) para você. Funciona com java Ola.java e também com javac + java. Outra vantagem: um arquivo compacto já enxerga toda a biblioteca básica (List, ArrayList, Map…) sem nenhum import. As duas formas valem no Java 25:

Clássica (todas as versões)Compacta Java 25+
public class Ola {
  public static void main(String[] args) {
    System.out.println("Olá");
  }
}
void main() {
  IO.println("Olá");
}

Em projetos de verdade, com pacotes e várias classes, a forma clássica continua sendo a regra. A compacta brilha em programas pequenos, scripts e no aprendizado.

Missão: reescreva este programa no formato compacto do Java 25: apague a classe, deixe só void main() e troque System.out.println por IO.println. A saída deve continuar Olá, Java 25!.

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

import module Java 25+

Em vez de uma linha de import para cada classe, você importa um módulo inteiro. java.base é o módulo com o básico do Java (java.util, java.time, java.io…):

import module java.base;

public class Main {
    public static void main(String[] args) {
        List<String> frutas = new ArrayList<>(List.of("maçã", "uva"));
        Map<String, Integer> estoque = new HashMap<>();
        estoque.put("maçã", 10);
        System.out.println(frutas + " " + estoque + " " + LocalDate.of(2026, 10, 16));
    }
}
$ javac Main.java && java Main
[maçã, uva] {maçã=10} 2026-10-16

List, ArrayList, Map, HashMap e LocalDate vieram de três pacotes diferentes com uma linha só.

Código antes do super(…) Java 25+

Até o Java 24, a chamada super(…) tinha de ser a primeira linha do construtor. Agora dá para validar ou preparar os argumentos antes, desde que não se use o objeto que ainda está sendo construído (this):

class Pessoa {
    String nome;
    Pessoa(String nome) { this.nome = nome; }
}

class Cliente extends Pessoa {
    Cliente(String nome) {
        if (nome == null || nome.isBlank()) {
            throw new IllegalArgumentException("nome vazio");
        }
        super(nome.strip());
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(new Cliente("  Ana  ").nome);
        new Cliente(" ");
    }
}
$ javac Main.java && java Main
Ana
Exception in thread "main" java.lang.IllegalArgumentException: nome vazio
	at Cliente.<init>(Main.java:9)
	at Main.main(Main.java:18)

O primeiro cliente teve os espaços removidos com strip() antes de chegar à classe mãe; o segundo foi recusado antes mesmo de a Pessoa ser construída (<init> no rastro é como o Java chama um construtor).

Coleções sequenciadas Java 21+

Listas, LinkedHashMap, TreeSet e outras coleções que têm uma ordem ganharam métodos com nomes óbvios:

import java.util.ArrayList;
import java.util.List;
import java.util.LinkedHashMap;

public class Main {
    public static void main(String[] args) {
        List<String> fila = new ArrayList<>(List.of("Ana", "Bruno", "Carla"));
        System.out.println(fila.getFirst());
        System.out.println(fila.getLast());
        System.out.println(fila.reversed());
        fila.addFirst("Zé");
        System.out.println(fila);
        LinkedHashMap<String, Integer> notas = new LinkedHashMap<>();
        notas.put("Ana", 9);
        notas.put("Bruno", 7);
        System.out.println(notas.firstEntry() + " " + notas.lastEntry());
    }
}
$ javac Main.java && java Main
Ana
Carla
[Carla, Bruno, Ana]
[Zé, Ana, Bruno, Carla]
Ana=9 Bruno=7
Antes do 21Java 21+
lista.get(0)lista.getFirst()
lista.get(lista.size() - 1)lista.getLast()
Copiar e chamar Collections.reverse(copia)lista.reversed() (uma "visão" ao contrário, sem copiar)
lista.add(0, x)lista.addFirst(x)

Missão: complete as três linhas usando getFirst(), getLast() e reversed().

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> fila = new ArrayList<>(List.of("Ana", "Bruno", "Carla", "Davi"));
        System.out.println("Primeiro: ");
        System.out.println("Último: ");
        System.out.println("Ao contrário: ");
    }
}
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> fila = new ArrayList<>(List.of("Ana", "Bruno", "Carla", "Davi"));
        System.out.println("Primeiro: " + fila.getFirst());
        System.out.println("Último: " + fila.getLast());
        System.out.println("Ao contrário: " + fila.reversed());
    }
}

O _ sem nome Java 22+

Quando o Java exige uma variável que você não vai usar, escreva _ (sublinhado). Fica claro para quem lê que aquilo foi ignorado de propósito:

import java.util.List;

public class Main {
    public static void main(String[] args) {
        int total = 0;
        for (String _ : List.of("a", "b", "c")) {
            total++;
        }
        IO.println("Itens: " + total);
        try {
            Integer.parseInt("dez");
        } catch (NumberFormatException _) {
            IO.println("Não é número");
        }
    }
}
$ java Main.java
Itens: 3
Não é número

Também vale em padrões: case Ponto(int x, _) (veja Pattern Matching) e em lambdas: (_, valor) -> …. Repare que o IO.println também funciona dentro de uma classe comum.

Scoped values Java 25+

Um ScopedValue ("valor com escopo") guarda uma informação, como o usuário logado, que fica visível para todos os métodos chamados dentro de um trecho, sem precisar passá-la de parâmetro em parâmetro. Fora do trecho, ela some. É a substituta moderna do ThreadLocal, pensada para conviver bem com as threads virtuais:

public class Main {
    static final ScopedValue<String> USUARIO = ScopedValue.newInstance();

    static void registrar(String acao) {
        System.out.println(USUARIO.get() + " fez: " + acao);
    }

    public static void main(String[] args) {
        ScopedValue.where(USUARIO, "ana").run(() -> registrar("venda"));
        ScopedValue.where(USUARIO, "bruno").run(() -> registrar("estoque"));
        System.out.println("Fora: " + USUARIO.isBound());
    }
}
$ javac Main.java && java Main
ana fez: venda
bruno fez: estoque
Fora: false

O método registrar não recebeu o usuário como parâmetro, mas soube quem era. Depois do run, o valor não está mais ligado (isBound() = "está ligado?" deu false). É um recurso avançado: guarde o nome para quando trabalhar com servidores.

Stream gatherers Java 24+

O método gather permite operações novas no meio de um stream, como agrupar os itens de dois em dois:

List<List<Integer>> pares = Stream.of(1, 2, 3, 4, 5).gather(Gatherers.windowFixed(2)).toList();
System.out.println(pares);
$ javac Main.java && java Main
[[1, 2], [3, 4], [5]]

O que não entrou: String templates

Você pode encontrar na internet artigos sobre String templates, uma forma de colocar variáveis dentro de textos: STR."Olá, \{nome}!". Eles foram preview no Java 21 e no 22 e, depois das opiniões recebidas, os responsáveis decidiram que o desenho não estava bom: o recurso foi retirado no Java 23 e não existe no 25, nem com --enable-preview:

Este código tem um erro de propósito.
public class Main {
    public static void main(String[] args) {
        String nome = "Ana";
        String s = STR."Olá, \{nome}!";
        System.out.println(s);
    }
}
$ javac Main.java
Main.java:4: error: illegal escape character
        String s = STR."Olá, \{nome}!";
                              ^
1 error
$ java --enable-preview Main.java
Main.java:4: error: illegal escape character
        String s = STR."Olá, \{nome}!";
                              ^
1 error
error: compilation failed

Para o Java 25, \{ é só um escape desconhecido. Para montar textos, continue usando concatenação, String.format / formatted ou StringBuilder. Essa história mostra para que serve o estágio de preview: experimentar sem prometer.


Quando dá errado

… is a preview feature and is disabled by default

Você usou um recurso que, na sua versão, ainda é preview. Conserto: confira se não há uma forma final de fazer o mesmo; se quiser só experimentar, compile com javac --release 25 --enable-preview e rode com java --enable-preview.

--enable-preview must be used with either -source or --release

O javac precisa saber a versão exata, porque o preview de uma versão não vale na outra. Conserto: acrescente --release 25.

Preview features are not enabled for Main (class file version 69.65535)

O .class foi compilado com preview, mas o java rodou sem a opção. Conserto: java --enable-preview Main.

Um recurso novo num Java velho

Se alguém rodar seu void main() num Java 21, ou getFirst() num Java 17, vai ver erros de compilação, porque aquela versão não conhece o recurso. É por isso que as aulas marcam o selo Java 21+: confira a versão com java -version e, no Maven ou Gradle, a versão configurada no projeto (veja Maven).


Referência rápida: recurso → versão final

RecursoFinal desdeAula
varJava 10var
switch com setas e yieldJava 14switch com Setas
Text blocks """Java 15Text Blocks
RecordsJava 16Records
instanceof com padrãoJava 16Pattern Matching
Classes seladasJava 17Classes Seladas
Padrões no switch, record patternsJava 21Pattern Matching
Threads virtuaisJava 21Threads Virtuais
getFirst(), getLast(), reversed()Java 21esta
_ sem nomeJava 22esta
Javadoc em Markdown ///Java 23Javadoc
Stream gatherersJava 24esta
void main(), IO, arquivo compactoJava 25esta e Olá, Mundo
import moduleJava 25esta
Código antes do super(…)Java 25esta
ScopedValueJava 25esta
Padrões primitivos, StructuredTaskScope, StableValue(preview no 25)—
String templates STR."…"(retirado no 23)—

Exercícios

1. Complete com a opção que liga os recursos em prévia:

java Main.java

2. Complete para pegar o último elemento da lista (Java 21+):

String ultimo = fila.;

3. Complete o programa compacto do Java 25:

void main() { .println("Olá"); }

4. O que aconteceu com os String templates (STR."Olá, \{nome}")?

5. Quais versões são LTS entre as estudadas aqui?

6. Por que este guia evita ensinar recursos em preview?

Missão final: num arquivo compacto do Java 25 (só void main(), sem classe e sem import), acrescente a venda 25 no fim com addLast, mostre a última venda com getLast() e a lista ao contrário com reversed(), usando IO.println.

void main() {
    var vendas = new ArrayList<>(List.of(30, 45, 12, 80));
    // acrescente 25 no fim e mostre as duas linhas
}
void main() {
    var vendas = new ArrayList<>(List.of(30, 45, 12, 80));
    vendas.addLast(25);
    IO.println("Última venda: " + vendas.getLast());
    IO.println("Mais recentes primeiro: " + vendas.reversed());
}

Saída real da solução:

$ java Main.java
Última venda: 25
Mais recentes primeiro: [25, 80, 12, 45, 30]

Resumo

  • Uma versão nova a cada 6 meses; LTS a cada 2 anos: 17, 21, 25.
  • Recursos passam por preview (precisa de --enable-preview no javac, com --release, e no java) antes de virar finais. Sem a opção: … is a preview feature and is disabled by default.
  • 17: classes seladas. 21: padrões no switch, record patterns, threads virtuais, getFirst/reversed. 22: _. 24: gatherers. 25: void main() + IO, import module, código antes do super, ScopedValue.
  • No JDK 25 ainda são preview: padrões primitivos, StructuredTaskScope, StableValue.
  • String templates foram retirados (Java 23).

Próxima aula: Threads, o começo da concorrência.