O Java 26 trouxe o JEP 526: Lazy Constants. Uma forma nativa e thread-safe de inicializar um valor uma única vez, só quando ele for usado de verdade. Aquele synchronized com double-checked locking que você escreveu há anos? Agora tem alternativa de primeira classe.
O que é o JEP 526 do Java 26?
O JEP 526 traz a classe java.lang.LazyConstant<T>: um container que guarda um valor imutável, inicializado no máximo uma vez. No Java 25, a feature se chamava "Stable Values" (JEP 502). No Java 26, foi renomeada, ganhou uma API mais simples e entrou em second preview.
O problema que ela resolve é antigo: campos final garantem imutabilidade, mas exigem inicialização imediata. Campos não-final permitem inicialização tardia, mas perdem as garantias de concorrência. LazyConstant junta os dois mundos: você inicializa quando quiser, mas só uma vez, com a mesma segurança de um final.
Por que o double-checked locking existe e qual é o problema?
Todo dev Java já escreveu algo assim:
public class MetricRegistry {
private static volatile MetricRegistry instance;
public static MetricRegistry getInstance() {
if (instance == null) {
synchronized (MetricRegistry.class) {
if (instance == null) {
instance = new MetricRegistry();
}
}
}
return instance;
}
}
Funciona. Mas exige volatile, dois null checks e um synchronized. Quem lê pela primeira vez não entende rápido. A alternativa com classe interna estática é mais segura, mas continua sendo um workaround.
Nenhuma dessas opções é uma abstração de primeira classe pro problema real: "inicialize isso uma vez, quando precisar, de forma segura".
Como o LazyConstant funciona na prática?
Imagine um serviço de pagamento com um cliente HTTP caro de criar, que só faz sentido existir se a rota for chamada:
public class PaymentService {
private final LazyConstant<HttpClient> httpClient =
LazyConstant.of(() -> HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build());
public String charge(String orderId, BigDecimal amount) {
HttpClient client = httpClient.get();
// usa o client para chamar o gateway
}
}
O HttpClient só é criado na primeira chamada a httpClient.get(). Depois disso, a JVM retorna o mesmo valor e pode até aplicar constant-folding. Thread-safe por construção, sem synchronized, sem volatile.
O que mudou do Java 25 para o Java 26 no JEP 526?
No Java 25, a API coloca métodos de baixo nível como setOrThrow, trySet e orElseSet, o que transformava a coisa num primitivo de sincronização. Ia contra o objetivo da feature.
No Java 26, ficou só o LazyConstant.of(supplier). Você fornece a função de inicialização e a JVM cuida do resto.
As factory methods para listas e mapas lazy também mudaram de lugar, saindo de StableValue para List e Map diretamente:
// Lista lazy com 10 slots, cada um inicializado de forma independente
List<OrderProcessor> processors = List.ofLazy(10, _ -> new OrderProcessor());
// Mapa lazy com chaves definidas, valores inicializados sob demanda
Map<String, RateLimiter> limiters = Map.ofLazy(
Set.of("payments", "refunds", "reports"),
key -> RateLimiter.create(requestsPerSecond(key))
);
Cada slot da lista e cada entry do mapa tem seu próprio ciclo de inicialização. O slot payments pode existir sem que refunds tenha sido criado.
Qual é a diferença entre LazyConstant e um campo final normal?
Um campo final exige que o valor exista na construção do objeto. Nem sempre isso é possível: a dependência pode não estar pronta, a inicialização pode ser cara demais pro startup, ou o valor depende de algo que só acontece depois.
// final obriga inicialização imediata
private final ExpensiveClient client = new ExpensiveClient(); // roda no construtor
// LazyConstant inicializa na primeira chamada a .get()
private final LazyConstant<ExpensiveClient> client =
LazyConstant.of(ExpensiveClient::new);
Depois de inicializado, a JVM pode tratar o LazyConstant como constante e aplicar as mesmas otimizações de um final estático.
O que preciso saber antes de usar Lazy Constants no Java 26?
Ainda é preview. Para compilar e rodar, você precisa de --enable-preview:
javac --enable-preview --release 26 PaymentService.java
java --enable-preview PaymentService
LazyConstant não é Serializable. Se você precisa serializar o objeto, não vai funcionar.
O valor do container é imutável, mas o objeto dentro pode não ser. O LazyConstant garante que a referência é atribuída uma vez. O que você faz com o objeto depois é problema seu.
Quando usar e quando não usar LazyConstant?
Use quando o objeto é caro de criar, nem sempre necessário e precisa ser thread-safe sem que você gerencie sincronização na mão. Conexões de banco, clients HTTP, caches de configuração, registros de métricas.
Não use quando um final comum resolve, quando precisa de serialização, ou quando quer controle exato de quando a inicialização acontece. LazyConstant garante que ela ocorre antes do primeiro .get(), mas não no momento específico que você escolher.
Por que isso importa agora, sendo ainda preview?
O double-checked locking está em produção em milhares de sistemas Java. Não porque alguém goste da complexidade, mas porque não tinha alternativa melhor. O JEP 526 é a resposta do OpenJDK pra isso.
Pode mudar antes de ser finalizado. A segunda preview serve pra isso. Mas a direção é clara: a plataforma quer tratar esse padrão como abstração de primeira classe, com suporte do compilador e da JVM, não como receita de código que cada dev implementa do seu jeito.
Experimentar agora com --enable-preview é a forma mais direta de entender o impacto no seu código antes que a feature chegue como estável.
Perguntas frequentes sobre LazyConstant e JEP 526 no Java 26
LazyConstant é thread-safe?
Sim. A inicialização única vale em ambientes multithread sem precisar de synchronized ou volatile.
Posso usar LazyConstant com Serializable?
Não. Se o objeto precisa de serialização, use final ou outro mecanismo.
LazyConstant substitui o padrão Singleton com classe interna estática?
Pra maioria dos casos, sim. O resultado é o mesmo, mas o código fica mais direto e a intenção fica explícita no tipo.
Preciso de Java 26 para usar essa feature?
Sim, com --enable-preview. O JEP 526 é second preview e ainda não é estável.
O que acontece se a função de inicialização lançar uma exceção?
A exceção é propagada pra quem chamou .get() e o LazyConstant não fica marcado como inicializado. A próxima chamada tenta de novo.
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:
Top comments (0)