DEV Community

Luis Fabrício De Llamas
Luis Fabrício De Llamas

Posted on

Java 26: o que muda quando você altera um campo final com reflection (JEP 500)

Outro dia eu estava preparando material para o DevConverge Buenos Aires, que acontece dia 11 de abril, e resolvi montar um exemplo rápido sobre uma das mudanças do Java 26 que eu acho que vai pegar muita gente de surpresa.

Não é HTTP/3. Não é o G1 mais rápido. É algo que parece simples, mas mexe com a fundação de como a gente escreve Java há anos: o JEP 500, chamado "Prepare to Make Final Mean Final", que começa a emitir warnings quando alguém muta um campo final usando reflection.

E sim, eu sei. Você provavelmente já fez isso. Eu também já fiz.

O que é o JEP 500 do Java 26?

O JEP 500 é uma mudança no Java 26 que emite um aviso em tempo de execução sempre que um campo declarado como final é alterado via deep reflection. Essa alteração faz parte da iniciativa "Integrity by Default" do OpenJDK, que pretende garantir que o modificador final seja respeitado de verdade pela JVM.

No Java 26, o comportamento padrão é emitir um warning. Em uma versão futura do JDK, esse warning será substituído por uma IllegalAccessException, bloqueando a mutação de campos final por padrão.

Por que era possível alterar campos final com reflection em Java?

Quando você declara um campo como final em Java, a ideia é clara: esse valor não muda depois que o objeto é construído. É um contrato. O compilador confia nisso. O JIT confia nisso. Você deveria confiar nisso.

Mas desde o JDK 5, a API de reflection permite quebrar essa promessa. Chamando setAccessible(true) e depois Field.set(...), o campo final era alterado sem nenhum aviso. A JVM aceitava calada.

Isso nasceu para suportar serialização. A JVM precisava reconstruir objetos a partir de streams, inclusive com campos final, e não tinha outro jeito. Com o tempo, frameworks de injeção de dependência, ORMs como Hibernate, libs de mocking como Mockito e ferramentas de teste adotaram o mesmo truque. A brecha virou padrão de mercado.

O resultado? O modificador final nunca significou imutabilidade real na JVM. Até o Java 26.

Como o warning do JEP 500 aparece na prática?

Quando um campo final é mutado via reflection no Java 26, o runtime emite uma mensagem como esta:

WARNING: final field apiKey in class PaymentGatewayConfig has been mutated
reflectively by class ConfigOverrideTest in module ALL-UNNAMED.
WARNING: Use --enable-final-field-mutation=ALL-UNNAMED to avoid a warning
WARNING: Mutating final fields will be blocked in a future release unless
final field mutation is enabled
Enter fullscreen mode Exit fullscreen mode

O código continua rodando. Mas o recado é claro: essa prática tem data de validade.

Exemplo prático: configuração de gateway de pagamento

Para não ficar abstrato, pensa num cenário que todo dev backend já viu. Uma classe de configuração de gateway de pagamento com campos final:

public class PaymentGatewayConfig {

    private final String apiKey;
    private final String merchantId;
    private final String environment;

    public PaymentGatewayConfig(String apiKey, String merchantId, String environment) {
        this.apiKey = apiKey;
        this.merchantId = merchantId;
        this.environment = environment;
    }

    public String apiKey() { return apiKey; }
    public String merchantId() { return merchantId; }
    public String environment() { return environment; }
}
Enter fullscreen mode Exit fullscreen mode

Esses campos são final por uma razão. Depois que o gateway é configurado, ninguém deveria conseguir trocar a apiKey em runtime. Não é só boa prática, é integridade de negócio.

Agora imagina um teste, ou pior, uma lib, que faz isso nos bastidores:

PaymentGatewayConfig config = new PaymentGatewayConfig(
    "sk_live_abc123", "merchant_456", "production"
);

Field apiKeyField = PaymentGatewayConfig.class.getDeclaredField("apiKey");
apiKeyField.setAccessible(true);
apiKeyField.set(config, "sk_test_override");

System.out.println(config.apiKey()); // sk_test_override
Enter fullscreen mode Exit fullscreen mode

Até o Java 25, isso passava sem nenhum sinal. No Java 26, o warning aparece. E ele está te dizendo: arruma isso enquanto dá tempo.

Quais são os modos de controle do JEP 500?

O JEP 500 introduz a flag de linha de comando --illegal-final-field-mutation com três comportamentos possíveis:

O modo warn é o padrão no Java 26. O campo final é mutado normalmente, mas o runtime emite o aviso na primeira ocorrência.

O modo debug funciona igual ao warn, mas inclui a stack trace completa. Se você tem um projeto grande com dezenas de dependências, esse é o modo que vai te mostrar de onde vem cada mutação.

O modo deny lança IllegalAccessException direto. Esse será o comportamento padrão em uma versão futura do JDK.

Se você quer se antecipar de verdade, roda seus testes assim:

java --illegal-final-field-mutation=deny -jar minha-app.jar
Enter fullscreen mode Exit fullscreen mode

Se quebrar, você acaba de encontrar exatamente o que precisa corrigir.

Como permitir mutação de campos final em módulos específicos?

Nem toda dependência pode ser atualizada amanhã. O JEP 500 prevê isso e oferece um escape cirúrgico por módulo com a flag --enable-final-field-mutation:

java --enable-final-field-mutation=com.minha.lib.legada -jar minha-app.jar
Enter fullscreen mode Exit fullscreen mode

Isso permite rodar com deny no geral e abrir exceção só para os módulos que realmente precisam. Para código em classpath, o famoso ALL-UNNAMED:

java --enable-final-field-mutation=ALL-UNNAMED -jar minha-app.jar
Enter fullscreen mode Exit fullscreen mode

Não é um "libera tudo". É um "eu sei o que estou fazendo aqui, e documentei o motivo".

Como auditar mutações de campos final com JDK Flight Recorder?

Se você quer uma visão mais ampla do que está acontecendo em produção ou staging, o JEP 500 integra com o JDK Flight Recorder (JFR) através do evento jdk.FinalFieldMutation:

java -XX:StartFlightRecording:filename=recording.jfr -jar minha-app.jar
Enter fullscreen mode Exit fullscreen mode

Depois:

jfr print --events jdk.FinalFieldMutation recording.jfr
Enter fullscreen mode Exit fullscreen mode

Isso mostra cada campo final que foi mutado, por qual classe, em qual módulo. Em um sistema com muitas dependências, essa é a forma mais honesta de fazer o levantamento sem ficar adivinhando.

Quais frameworks e bibliotecas Java são afetados pelo JEP 500?

A mutação de campos final via reflection não é exclusiva de testes. Diversos tipos de bibliotecas e frameworks Java usam essa técnica: frameworks de ORM como Hibernate, bibliotecas de serialização como Jackson em certos cenários, ferramentas de mocking como Mockito e frameworks de injeção de dependência.

O escopo costuma ser maior do que a gente imagina. A recomendação é rodar os testes com --illegal-final-field-mutation=debug para descobrir todas as ocorrências com stack traces completas.

Qual é a melhor estratégia para se preparar para o JEP 500?

Ignorar os warnings porque "ainda funciona" não é estratégia. O Java segue sempre o mesmo caminho: warning primeiro, exception depois. Foi assim com --add-opens, foi assim com acesso a internals. A janela de preparação é agora.

Mas também não faz sentido ligar deny em produção amanhã cedo. Começa pelo CI. Roda a suite de testes com --illegal-final-field-mutation=debug, olha as stack traces, classifica o que é código seu e o que é dependência. Atualiza o que dá pra atualizar. Documenta o que precisa do escape. Depois avança para staging, e só então para produção.

Grava uma sessão com JFR em staging para pegar mutações que só aparecem em fluxos reais de aplicação, não cobertos por testes unitários. Depois sobe o CI com deny ativado para garantir que nenhuma regressão passe despercebida.

Por que isso importa agora

Esse é o tipo de mudança que não aparece no radar de quem olha só release notes por cima. Mas pra quem mantém sistemas em produção, é uma das mais relevantes do Java 26.

Inclusive, é exatamente esse tipo de conversa que queremos trazer no [DevConverge Buenos Aires], dia 11 de abril. A proposta é conectar devs de toda a América Latina pra falar sobre o que realmente muda no dia a dia: Java, IA, DevOps, automação. Se você constrói software e está por Buenos Aires, chega junto.

Voltando ao JEP 500: o valor real não está no warning. Está em usar essa janela de preparação para auditar o que roda no seu projeto antes que o comportamento padrão mude.

Em um sistema como gateway de pagamento, plataforma de e-commerce, serviço financeiro ou qualquer backend onde integridade de configuração importa, essa mudança é relevante. Não quando o deny virar padrão. Agora.

Perguntas frequentes sobre o JEP 500 e campos final no Java 26

O JEP 500 já bloqueia a mutação de campos final no Java 26?
Não. No Java 26, o comportamento padrão é apenas emitir um warning. O bloqueio com IllegalAccessException será o padrão em uma versão futura do JDK.

Qual a diferença entre --illegal-final-field-mutation e --enable-final-field-mutation?
A flag --illegal-final-field-mutation controla o comportamento global (warn, debug ou deny). A flag --enable-final-field-mutation permite habilitar a mutação seletivamente para módulos específicos, funcionando como um escape controlado.

Records são afetados pelo JEP 500?
Não. Os campos implicitamente declarados em records já não permitiam mutação via deep reflection antes do Java 26. O JEP 500 alinha o comportamento de classes normais com o que records já faziam.

Como saber se minhas dependências mutam campos final?
Rode sua aplicação ou testes com --illegal-final-field-mutation=debug para ver warnings com stack trace completa. Para produção, use o JDK Flight Recorder com o evento jdk.FinalFieldMutation.

Preciso atualizar meu código agora?
Sim, é recomendável começar a auditoria agora. A transição de warning para exception seguirá o mesmo padrão de outras mudanças de integridade do JDK. Quanto antes você identificar as ocorrências, mais tranquila será a migração.

Eu sou o Luis De Llamas, Developer Advocate na act digital, Oracle ACE e IBM Champion. Se quiser acompanhar mais conteúdo sobre Quarkus, Java e o que rola no ecossistema, me encontra aqui:

Nos vemos!

Top comments (0)