Todos os artigos

// Knowledge.log — 技術記事

Construtor flexível no Java 25: validar antes do super() sem helper estático

JEP 513 no Java 25: valide e normalize argumentos da subclasse antes do super(), sem helper estático. Compilação real, erro do javac e testes JUnit 5.

Até o Java 24, o construtor de uma subclasse tinha uma regra rígida: super(...) (ou this(...)) precisava ser a primeira instrução. Se você queria validar ou normalizar o argumento antes de repassar para o pai, não tinha onde colocar essa lógica — ela ia para um método estático chamado dentro da própria chamada a super(...), ou para depois do super(...), quando o objeto já tinha sido construído com um valor que talvez fosse inválido.

O Java 25 muda essa regra. Este artigo mostra o que passou a ser legal antes de super(), um exemplo que valida e normaliza argumentos de uma subclasse sem helper estático, o que continua proibido (com o erro exato do javac) e os testes JUnit que comprovam o comportamento. Tudo compilado e executado neste host com Temurin 25.0.4 e JUnit Jupiter 5.14.4 — sem Spring, sem preview flag, sem números de desempenho.

O que o JEP 513 tornou legal

O recurso se chama Flexible Constructor Bodies e virou final — não preview — no JDK 25, depois de três rodadas de preview (JEP 447 no JDK 22, JEP 482 no JDK 23, JEP 492 no JDK 24, repetido sem mudanças). O resumo oficial do JEP diz o essencial:

> "In the body of a constructor, allow statements to appear before an explicit constructor invocation, i.e., super(...) or this(...). Such statements cannot reference the object under construction, but they can initialize its fields and perform other safe computations."

Leia com atenção a segunda frase: as instruções antes de super(...) não podem referenciar o objeto em construção, mas podem inicializar campos e fazer outros cálculos seguros. É essa distinção que separa "posso validar meus parâmetros" de "posso mexer no this antes de ele existir de verdade" — e é exatamente a linha que o javac fiscaliza.

Fonte primária: JEP 513: Flexible Constructor Bodies.

Prólogo e epílogo

A JLS chama de prólogo o trecho de instruções antes da chamada explícita a super(...)/this(...), e de epílogo o que vem depois. Se não houver chamada explícita, o prólogo fica vazio e o compilador insere um super(); implícito logo no início — nesse caso, tudo que você escreveu é epílogo, como sempre foi.

Dentro do prólogo, o objeto ainda não existe como instância válida da subclasse: o pai ainda não rodou. Por isso o JLS chama esse trecho de "early construction context" e restringe o que pode acontecer nele — ver JLS SE 25, §8.8.7 Constructor Body. Também vale a referência prática da Oracle: Flexible Constructor Bodies — Java SE 25.

Nada disso precisa de --enable-preview. No Java 25, javac --release 25 compila prólogo normalmente — é feature finalizada, não prévia.

Validando e normalizando antes do super(), sem helper estático

O exemplo é o clássico dinheiro-em-centavos: uma classe Money que guarda um long cents, e uma subclasse PositiveMoney que só aceita valores positivos — e que também sabe interpretar uma string tipo " $1_000 " como 1000 centavos. Antes do JEP 513, a validação e a normalização de string tinham que morar num método estático para poder ser chamadas dentro da expressão de super(...). Agora elas moram no próprio construtor, como código sequencial de verdade.

Money, sem novidade nenhuma:

package money;

public class Money {
    private final long cents;

    public Money(long cents) {
        this.cents = cents;
    }

    public long cents() {
        return cents;
    }
}

PositiveMoney, com dois construtores que validam antes de chamar super(cents) — sem nenhum método estático auxiliar:

package money;

import java.util.Objects;

public class PositiveMoney extends Money {
    public PositiveMoney(Long cents) {
        Objects.requireNonNull(cents, "cents");
        if (cents <= 0) {
            throw new IllegalArgumentException("cents must be > 0, got " + cents);
        }
        super(cents);
    }

    public PositiveMoney(String raw) {
        Objects.requireNonNull(raw, "raw");
        String normalized = raw.strip().replace("_", "");
        if (normalized.startsWith("$")) {
            normalized = normalized.substring(1);
        }
        long cents = Long.parseLong(normalized);
        if (cents <= 0) {
            throw new IllegalArgumentException("cents must be > 0, got " + cents);
        }
        super(cents);
    }
}

Repare no que está acontecendo em cada construtor: Objects.requireNonNull é uma chamada estática, permitida no prólogo porque não toca no objeto sendo construído. A checagem de faixa (cents <= 0) só lê o parâmetro local, não a instância. A normalização de string (strip, replace, substring, parseLong) também opera só em variáveis locais. Nenhuma dessas linhas referencia this, nenhuma chama método de instância, nenhuma lê campo do objeto — por isso o compilador aceita tudo isso antes do super(cents).

Compilando com o compilador real, sem preview:

$ javac --release 25 -d ok/out ok/Money.java ok/PositiveMoney.java

Saída: compilação limpa, código de saída 0. Sem flag de preview, sem warning.

O guia da Oracle usa um exemplo irmão deste — uma PositiveBigInteger que testa value <= 0 e chama super(Long.toString(value)) — no mesmo padrão de "valida, normaliza, só então super". Vale a leitura complementar: Flexible Constructor Bodies — Java SE 25.

Se você já viu times espalhando verify* estáticos só para poder chamá-los dentro dos parênteses de super(...), esse é o padrão que o JEP 513 aposenta. O helper estático continua compilando — ele não vira erro, só vira redundante.

O que continua proibido — e o erro exato do javac

O prólogo ganhou liberdade para validar e computar, não para agir como se o objeto já existisse. Continuam proibidos, no trecho antes de super(...)/this(...):

  • Ler this (exceto na forma restrita de atribuir um campo sem inicializador).
  • Ler ou escrever campos de instância que já têm um inicializador na declaração.
  • Chamar método de instância (mesmo implicitamente, sem this. na frente).
  • Acessar super.campo ou chamar super.metodo().
  • Usar return, ou envolver a própria chamada super(...)/this(...) num try.

Um construtor que tropeça em quase todas essas regras de uma vez:

class Super {
    int j;
    void m() {}
}

class IllegalThis extends Super {
    int i;
    String s = "hello";

    IllegalThis() {
        this.i++;          // 1
        i++;               // 2
        this.hashCode();   // 3
        hashCode();        // 4
        System.out.print(this); // 5
        super.j++;         // 6
        super.m();         // 7
        s = "goodbye";     // 8 atribuição a campo já inicializado
        super();
    }
}

Compilando com javac --release 25 fail/IllegalThis.java, o host devolve 9 erros, entre eles:

cannot reference this before supertype constructor has been called
cannot reference i before supertype constructor has been called
cannot reference hashCode() before supertype constructor has been called
cannot reference super before supertype constructor has been called
cannot assign initialized field 's' before supertype constructor has been called

Cada linha do exemplo mapeia para um desses diagnósticos: this.i++ e i++ disparam "cannot reference this / i"; this.hashCode() e hashCode() disparam a variante de método; super.j++ e super.m() disparam "cannot reference super"; e s = "goodbye" — atribuição a um campo que já tem inicializador (String s = "hello") — dispara o erro de atribuição, que é diferente do erro de leitura. É um detalhe fácil de esquecer: campo sem inicializador pode ser atribuído no prólogo; campo com inicializador, não.

Correção: mova toda leitura de this, chamada de método de instância e acesso a super.* para depois de super(...) — ou seja, para o epílogo, onde sempre foi permitido. Observação em produção: esse gate não precisa de teste em runtime, porque é um erro de compilação — javac --release 25 na esteira de CI já barra o commit antes de qualquer deploy. É o mesmo tipo de garantia que o artigo sobre sealed interfaces e o fim do boolean flag soup usa para estados inválidos — só que ali quem barra é o switch exaustivo sobre uma interface sealed, aqui é o prólogo do construtor. São dois mecanismos de erro-em-tempo-de-compilação, não o mesmo.

Testes JUnit 5 para o caminho feliz e a rejeição

Compilar sem erro garante que a sintaxe está correta; não garante que a validação faz o que você acha que faz. Para isso, cinco testes JUnit Jupiter, todos rodando puro Java SE — sem Spring, sem Testcontainers:

import org.junit.jupiter.api.Test;
import money.PositiveMoney;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

class PositiveMoneyTest {

    @Test
    void happyPathStoresCents() {
        assertEquals(250, new PositiveMoney(250L).cents());
    }

    @Test
    void normalizesCurrencyAndSeparatorsBeforeSuper() {
        assertEquals(1000, new PositiveMoney(" $1_000 ").cents());
    }

    @Test
    void rejectsNullCents() {
        assertThrows(NullPointerException.class, () -> new PositiveMoney((Long) null));
    }

    @Test
    void rejectsNonPositiveCents() {
        assertThrows(IllegalArgumentException.class, () -> new PositiveMoney(0L));
        assertThrows(IllegalArgumentException.class, () -> new PositiveMoney(-1L));
    }

    @Test
    void rejectsNullRaw() {
        assertThrows(NullPointerException.class, () -> new PositiveMoney((String) null));
    }
}

Rodando sem Maven, direto com o console launcher do JUnit Platform:

javac --release 25 -cp junit-platform-console-standalone-1.14.4.jar:ok/out \
  -d tests/out tests/src/PositiveMoneyTest.java

java --class-path junit-platform-console-standalone-1.14.4.jar:ok/out:tests/out \
  org.junit.platform.console.ConsoleLauncher --select-class PositiveMoneyTest

Resultado neste host, em 2026-09-20: 5 testes iniciados, 5 bem-sucedidos, 0 falhos — happyPathStoresCents, normalizesCurrencyAndSeparatorsBeforeSuper, rejectsNullCents, rejectsNonPositiveCents, rejectsNullRaw. Concretamente: new PositiveMoney(250L).cents() devolve 250; new PositiveMoney(" $1_000 ").cents() devolve 1000 — a normalização de cifrão, espaço e underscore acontece toda no prólogo, antes de super(cents) ver o valor final. Passar Long nulo lança NullPointerException; passar 0L ou -1L lança IllegalArgumentException; passar String nula também lança NullPointerException.

Se você prefere Maven a javac/java na mão, o pom.xml fica pequeno — é Java SE puro, sem plugin de framework:

<properties>
  <maven.compiler.release>25</maven.compiler.release>
  <junit.version>5.14.4</junit.version>
</properties>

<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>5.14.4</version>
  <scope>test</scope>
</dependency>

Documentação da versão usada: JUnit 5 User Guide (5.14.4). Fique em JUnit Jupiter 5.14.4 aqui — não porque a 6.x quebre no Java 25, mas porque este exemplo usa a linha 5 como base, e misturar as duas só adiciona ruído que o artigo não precisa.

Correção quando um teste de rejeição falhar: confira se a exceção declarada no assertThrows bate com o que o construtor realmente lança — NullPointerException para argumento nulo, IllegalArgumentException para faixa inválida; são tipos diferentes e o teste erra o alvo se você trocar um pelo outro. Observação em produção: os testes de rejeição são a rede de segurança para regra de negócio (valor <= 0, string nula); o javac cobre a sintaxe do prólogo, os testes cobrem a semântica da validação. Um sem o outro deixa buraco.

Recomendação

Dentro deste stack — Java 25 Temurin, JUnit Jupiter 5.14.4, sem framework por cima — trocar helper estático por validação direta no prólogo do construtor vale a pena sempre que a subclasse precisa recusar ou normalizar argumento antes de repassar ao pai. É menos indireção: a invariante fica lida de cima para baixo no próprio construtor, em vez de espalhada entre a assinatura de super(...) e um método estático que só existe para satisfazer o compilador antigo.

Vale recuar dessa técnica em dois casos: se o projeto ainda precisa compilar em JDK anterior ao 25 (o prólogo simplesmente não existe lá, e não há flag que resolva), ou se a lógica de validação é complexa o bastante para merecer um objeto próprio — nesse ponto, o ganho de mover para dentro do construtor perde para o ganho de nomear a regra como algo testável isoladamente. Para o caso comum — requireNonNull, checagem de faixa, normalização simples de string — o construtor já dá conta, e o javac --release 25 continua sendo o gate que te avisa se alguém tentar ler a instância cedo demais.

javajdk-25

// Continue.training — 次のステップ

Conhecimento só conta quando vira prática.

Volte ao artigo, execute os exemplos e compartilhe o que aprendeu.

Explorar mais artigos