- JEP 401: Value Classes and Objects chegou ao estágio de entrar como preview real no JDK
- O objetivo central é fazer com que objetos Java “sejam programados como classes e funcionem como
int”, reduzindo os custos de cabeçalho de objeto, alocação no heap, GC e indireção por ponteiros
- A value class do JDK 28 ainda é um tipo de referência anulável; tipos non-null, genéricos especializados e codificação de 128 bits não estão incluídos, e é necessário
--enable-preview
- A JVM pode escalarizar value objects ou fazer achatamento no heap em campos e arrays, mas eles podem ser materializados como objetos no heap em tipos superiores como genéricos com erasure ou
Object
- Desenvolvedores Java precisam refletir a diferença entre identity e value no design do código, e isso afeta
==, synchronized, wrappers de primitivos, desempenho de arrays e a futura especialização de genéricos
O escopo do Valhalla que entra no JDK 28
- Em 15 de junho, a engenheira da Oracle Lois Foltan confirmou a integração de JEP 401: Value Classes and Objects no repositório principal do OpenJDK e o alvo para o JDK 28
- O pull request relacionado adiciona mais de 197 mil linhas em 1.816 arquivos
- Como a mudança é muito grande, durante a integração houve um pedido para que outros committers suspendessem temporariamente commits grandes
- O JEP 401 é um recurso preview desativado por padrão
- Para usar a sintaxe, é necessário
--enable-preview
- Brian Goetz delimitou isso como “a primeira parte do Valhalla”
- O JDK 28 está previsto para ser lançado em março de 2027, e a integração na mainline está planejada para algo em torno de julho de 2026
O custo do modelo de objetos Java que o Valhalla quer atacar
- O lema do Valhalla é “codes like a class, works like an int”
- A meta é permitir usar classes normais com métodos, validação em construtores e nomes de campos significativos, enquanto a JVM as trata com eficiência semelhante à de primitivos
- Em Java, com exceção dos 8 primitivos, quase tudo é tipo de referência
- Em
Point p = new Point(1, 2), p não é o próprio point, mas um ponteiro para um objeto no heap
- Cada vez que lê um campo, a JVM precisa seguir esse ponteiro
- Quando o número de objetos cresce, o custo aumenta rapidamente
- Cada objeto tem um cabeçalho de objeto para tipo, estado de sincronização etc.
- Objetos são alocados no heap e depois se tornam alvo do GC
- Um array com um milhão de
Point é, na prática, composto por um milhão de ponteiros e um milhão de objetos espalhados pelo heap
- Em “State of Valhalla”, Brian Goetz chama esse layout de memória de fluffy
- O que o Valhalla busca é um layout dense, em que os dados ficam lado a lado
A diferença para o hardware e os limites da escape analysis
- A razão de um layout de memória denso ser importante está na diferença de velocidade entre CPU e memória
- Em 1995, o custo de acesso à memória era parecido com o de operações da CPU
- Hoje, a CPU é dezenas de vezes mais rápida que a memória principal, e o cache preenche essa lacuna
- A CPU normalmente lê memória em unidades de cache line de 64 bytes
- Se os dados estiverem densos e em sequência, ela traz muitos valores úteis de uma vez
- Ao seguir ponteiros para objetos espalhados, pode haver cache miss, que pode ser muito mais lento que um hit
- A escape analysis da JVM pode eliminar algumas alocações de objetos
- Se ela concluir que o objeto não “escapa” para fora de um trecho local de código, pode não alocá-lo no heap e expandir seus campos em variáveis ou registradores
- Mas a escape analysis é pouco previsível e frágil
- Se o objeto entrar em um campo de outra classe, for armazenado em um array, passado para um método complexo ou cruzar um limite que o JIT não consegue analisar, a otimização pode parar
- Pequenos refactorings, atualizações do JDK ou mudanças na estrutura do código podem fazer o objeto voltar para o heap
- Se, em busca de desempenho, você abandonar objetos e codificar tudo manualmente como bytes crus, como
r, g e b, pode ganhar velocidade, mas perde segurança, legibilidade, validação e métodos
O início em 2014 e a transição de Q World para L World
- O Project Valhalla começou oficialmente em 2014
- James Gosling o descreveu na época como “six PhDs tied into a single knot”
- Os criadores do Java já queriam value types desde a era do Java 1.0, mas em 1995 o problema era difícil demais e foi deixado de lado
- O objetivo inicial era restaurar o alinhamento entre o modelo de programação e as características de desempenho do hardware moderno
- A ideia era permitir que usuários declarassem tipos flat e dense como primitivos, mas que aparentassem e se comportassem como classes normais
- O prototype inicial seguia a direção do Q World
- Ele tratava os novos value types como entidades fundamentalmente diferentes de objetos, com type descriptor, bytecode e top type separados
- Isso aumentava a complexidade porque todo o sistema de tipos da JVM precisava ter duas variações
- O L World, que surgiu por volta de 2019, tornou-se o ponto de virada
- Os value types passam a compartilhar o mesmo “L carrier” das referências normais
- A equipe esperava que essa integração fosse difícil, mas ela funcionou sem grandes concessões e resolveu vários problemas dos prototypes anteriores
- No L World surgiu uma separação importante
- O modelo da JVM e o modelo da linguagem não precisam coincidir 100%
- É possível manter o modelo L World na JVM e oferecer aos programadores um modelo de linguagem mais conveniente
- Depois disso, o trabalho passou a se dividir em duas etapas: value classes e genéricos especializados
Mudanças de nome e de modelo
- A terminologia de Valhalla mudou várias vezes, e isso não refletia apenas uma troca de nome, mas também mudanças no modelo
- O termo inicial era value types
- Na época, ainda não estava claro exatamente o que esses tipos seriam
- Por volta de 2019~2020, o modelo de inline classes se consolidou
- As classes existentes eram tratadas como identity classes, e as novas classes eram diferenciadas como inline classes sem identidade
- A inline class era final por padrão, os campos eram final, e havia a restrição de não poder ser sincronizada
- Em 2021, o “State of Valhalla” abordou primitive classes e o modelo de duas projeções
- A ideia era que um único tipo tivesse uma variante value, compacta e sem possibilidade de null, e uma variante reference que aceitasse null
- Sintaxes como
Point.val / Point.ref e, depois, Point! / Point? também foram testadas
- Esse modelo era poderoso, mas trazia uma carga cognitiva alta
- Os programadores precisavam entender no dia a dia as duas formas do mesmo tipo e os momentos de conversão entre elas
- No fim, o dualismo foi reduzido para simplificar o modelo para o usuário
- Atualmente, o JEP 401 usa value class e value object
- A declaração de uma value class é feita com o modificador
value
- As instâncias são value objects sem identidade
- A value class continua sendo um reference type
- A não nulidade foi separada em um JEP opcional à parte, Null-Restricted Value Class Types
- Não está incluído no JDK 28
- Textos antigos que explicam o modelo anterior de “primitive classes” podem diferir do padrão atual do OpenJDK
- O JEP 401 também vem acompanhado do preview JEP 402: Enhanced Primitive Boxing
- A direção é tornar mais suave a conversão entre primitive e wrapper
- Não se deve presumir que tudo entrará junto com o JEP 401 em sua forma final
O modelo de value class no JDK 28
- A value class é declarada com o modificador
value
value class USDCurrency implements Comparable<USDCurrency> {
private int cents; // implicitly final
public USDCurrency(int dollars, int cents) {
this.cents = dollars * 100 + cents;
}
public USDCurrency plus(USDCurrency that) {
return new USDCurrency(0, this.cents + that.cents);
}
// dollars(), cents(), compareTo(), toString()...
}
- Também é possível ter value record
- As principais regras são as seguintes
- Todos os instance fields são implicitamente final
- Um method não pode ser
synchronized
- A classe é final por padrão
- Hierarquias formadas por value class e abstract value class são permitidas
- Não é possível herdar de uma classe com identidade
- É possível implementar interfaces
- A característica central é a ausência de identidade
- Em um objeto comum, mesmo com o mesmo conteúdo, se você fizer
new Point(1, 2) duas vezes, serão dois objetos diferentes
- Um value object não tem identidade, assim como não existem “dois 4 diferentes” no valor
int 4
Mudanças em ==, synchronized e null
- Em value objects,
== deixa de ser comparação de identidade e passa a ser uma verificação de substitutability
- Compara recursivamente se é a mesma classe e se possui os mesmos valores de campo
- Primitive fields são comparados no nível de bits, e object fields são comparados novamente com
==
new USDCurrency(3,95) == new USDCurrency(3,95) passa a ser true
- Ainda assim, como
== observa o estado interno, para a pergunta “representa os mesmos dados?”, normalmente equals continua sendo mais apropriado
- Value objects não têm identidade para sincronizar
- Se houver tentativa de sincronização, ocorre IdentityException
- Quando for necessário verificar identidade explicitamente, podem ser usados
Objects.requireIdentity e Objects.hasIdentity
- As value classes do JDK 28 continuam sendo nuláveis
USDCurrency d = null; é válido
- Um tipo que proíba null é tema de um JEP futuro separado e não faz parte do JDK 28
- A não nulidade não é apenas uma questão de sintaxe, mas um mecanismo de desempenho que abre espaço para uma flattening mais ampla de value classes
Escalarização e achatamento no heap
- O JEP 401 dá à JVM liberdade para otimizar value objects
- Escalarização (scalarization) é uma técnica de JIT que decompõe uma referência a value object em um conjunto de campos
- Em vez de passar um ponteiro para
Color, é possível passar os bytes r, g, b e um flag indicando null
- Os custos de alocação e GC podem desaparecer
- É parecida com escape analysis, mas mais previsível e aplicável além do limite de chamadas de method não inline
- A escalarização tem limitações
- Em geral, ela não funciona quando o tipo da variável é um supertype da value class, como
Object, ou um parâmetro genérico com erasure
- Nesses casos, o objeto precisa ser materializado no heap
- Heap flattening é a abordagem de codificar os valores de campo de um value object como um vetor de bits compacto e gravá-los diretamente em um campo ou célula de array
- Não é necessário um ponteiro para outro local do heap
- Isso traz densidade de dados e locality
- Para evitar tearing em acesso concorrente, dados achatados devem poder ser lidos e escritos de forma atômica
- Em plataformas comuns, um tamanho “pequeno o suficiente” pode ficar na faixa de 64 bits, incluindo o flag de null
- Value classes pequenas podem ser bem achatadas, mas até mesmo dois campos
int ou apenas um double já podem não caber no tamanho de escrita atômica e acabar como objetos comuns no heap
- No futuro, codificações de 128 bits e tipos com restrição de null podem permitir o achatamento de value classes maiores
Efeitos em boxing, wrappers e arrays
- Com o preview ativado, as próprias primitive wrapper classes, como
Integer, Long e Double, passam a ser value class
- Como o box perde identidade, a JVM pode escalarizá-lo e achatá-lo
Integer[] caminha para uma eficiência próxima de int[], com grande redução do overhead de boxing
- O JEP 402: Enhanced Primitive Boxing amplia ainda mais as conversões entre primitive e box
- Ele abre caminho para expressões como
List<int>, mas ainda é um trabalho separado em amadurecimento
- É nos arrays que o efeito aparece com mais clareza
- Antes,
Color[] podia significar um milhão de ponteiros e um milhão de objetos espalhados pelo heap
- Com value class,
Color[] pode se tornar um bloco contíguo armazenando diretamente os valores de cor
- A CPU consegue ler vários valores em sequência por unidade de cache line
Diferença antes e depois no exemplo de Point[]
- O exemplo de class comum antes de Valhalla é o seguinte
final class Point {
final int x;
final int y;
Point(int x, int y) { this.x = x; this.y = y; }
}
Point[] points = new Point[1_000_000];
- Este array armazena um milhão de ponteiros
- Cada ponteiro aponta para um objeto
Point separado em algum lugar do heap
- Cada objeto tem, além de dois
int, um cabeçalho de objeto
- Ao percorrer, é preciso ler o ponteiro, saltar para aquele endereço e ler os campos
- Após o Valhalla, um exemplo de value class fica assim
value class Point {
final int x;
final int y;
Point(int x, int y) { this.x = x; this.y = y; }
}
Point[] points = new Point[1_000_000];
- A diferença no código é a palavra
value, mas o layout de memória muda
- A JVM pode armazenar os valores de cada point de forma compacta dentro do array
- Não há cabeçalho nem ponteiro por elemento
- Com base nos dois
int, x e y, eles podem ser dispostos de forma contígua em 8 bytes e com um possível null flag
- A manutenção também é preservada
Point continua sendo uma class com nome, construtor, validação e method
- Dá para evitar dividir em
int[] xs, int[] ys e alinhar índices manualmente
Por que os genéricos especializados ainda ficaram de fora
- Os generics de Java são implementados com type erasure
List<String> e List<Integer> são o mesmo List em runtime
- O parâmetro de tipo
T é apagado para Object
- O erasure foi uma escolha intencional para introduzir generics sem quebrar a base de código existente do Java
- Mesmo ao transformar uma class não genérica em genérica, os source files e classes compiladas existentes não quebram
- Valhalla e erasure entram em conflito em termos de desempenho
- Se você colocar um value object em
List<Point>, como T é apagado para Object, o objeto precisa ser materializado no heap
- O ganho de achatamento obtido em
Point[] pode desaparecer em ArrayList<Point>
- O plano de recuperação tem duas etapas
- Universal Generics: no nível da linguagem, permitir que variáveis de tipo também tratem value types
- Ainda usa erasure
- Como campos
T começam como null por padrão, isso pode gerar um warning do compilador de “null pollution”
- Ao resolver esses warnings, a API fica mais próxima de estar pronta para specialization
- Specialized Generics: no nível da JVM, gerar layouts de class especializados para cada argumento de tipo concreto
- Na terminologia do projeto, species e type restriction estão relacionados a isso
- Só nessa etapa
ArrayList<Point> poderá realmente usar memória flat
- O JDK 28 não tem full specialized generics
- Coleções, streams e APIs se tornarem flat e livres de alocação sobre value types é trabalho para releases futuras
O que está e o que não está no JDK 28
- O que entra no JDK 28 é o seguinte
- declarações
value class e value record
- migração para value class de classes value-based já existentes no JDK, como wrappers de primitivos
- escalarização e achatamento de classes que atendam às condições
- boxing mais barato
- O que não está no JDK 28 é o seguinte
- tipos com restrição de null
- full specialized generics
- codificação de 128 bits
- um JEP 402 totalmente maduro
- Como é um preview feature, a sintaxe e o comportamento podem mudar a cada release conforme o feedback
- O JDK 28 não é LTS
- O próximo LTS provavelmente será o JDK 29, em setembro de 2027
- Muitas empresas poderão encontrar um Valhalla estabilizado em um LTS, mas o preview do JDK 28 inicia o ciclo real de feedback em código
Mudanças que isso trará para o ecossistema e o código
- Nas áreas de Java de alto desempenho, o Valhalla passa a ser um caminho para lidar com dados densos sem abrir mão de abstrações
- Isso inclui áreas como processamento de dados, computação vetorial, ML, desenvolvimento de jogos, finanças e codecs
- Frameworks e bibliotecas podem começar a migrar classes value-based
- Código que dependia de identidade pode sofrer diferenças de comportamento
== em value objects deixa de ser comparação de endereço e passa a ser comparação de substitutability
synchronized em value objects leva a IdentityException
- Mesmo que
Integer vire uma value class, na maioria dos casos o binário continuará fazendo link
- Um novo erro de compilação aparece ao tentar sincronizar nesses tipos
== baseado na identidade de Integer ou synchronized(someInteger) podem ser afetados
- Builds early-access estão disponíveis em jdk.java.net/valhalla
Resumo das perguntas mais frequentes
- value class é diferente de record
record é a escolha de fazer com que o conteúdo seja composto por components
value é a escolha de abrir mão da identidade
- Todas as combinações são possíveis: class comum, record, value class e value record
- value objects podem ser comparados com
==
- O significado não é comparação de endereço, mas de substitutability
- Para igualdade dos dados representados, normalmente
equals é mais apropriado
- Value classes do JDK 28 podem ser null
- Tipos não anuláveis são tema de um JEP futuro
- Isso também é importante para o achatamento de value classes maiores
- Ainda não é a vez de um
ArrayList<Point> flat e rápido
- Por causa do type erasure, objetos dentro de coleções genéricas precisam ser materializados no heap
- No JDK 28, os casos representativos em que o achatamento funciona diretamente são fields e arrays de value type, como
Point[]
- Escape analysis não consegue substituir tudo
- Se o objeto escapar para um field, array ou para fora do limite da análise, a otimização pode falhar
- A escalarização de value objects é mais previsível e pode ir mais longe além dos limites de chamada de method
- O Valhalla completo será expandido ao longo de várias releases
- O JDK 28 é o primeiro preview de value class
- specialized generics, tipos com restrição de null e codificação de 128 bits são trabalhos para releases futuras
2 comentários
As threads virtuais lançadas pelo Project Loom ao longo de muito tempo são convenientes e resolvem muita coisa no nível do runtime da JVM, então o desenvolvedor acaba tendo menos com o que se preocupar.
Tomara que o Project Valhalla também chegue ao lançamento final com essa sensação de “almoço grátis”.
Comentários do Hacker News
Disseram que a diferença de memória é algo fundamental, mas fico em dúvida se o texto foi realmente revisado
Até agora há pouco não estava explicando que objetos com representação maior que 64 bits não são achatados no heap? O
Pointdo exemplo tem dois inteiros de 32 bits mais um flag de null, então dá no mínimo 65 bitsPela expressão “pode haver um flag de null” e pelas frases curtas de ênfase que vêm depois, parece que a IA foi gerar frases de destaque e acabou se desviando, e aquele bloco no meio com
"[IMAGE: the same Point[] array in two variants..."também é lamentávelTudo bem usar IA para ajudar a escrever, mas se você não coloca a própria voz, não há motivo para ler
https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing#...
Mas depois de alguns parágrafos ficou claro que era um texto feito passando por um LLM ou por algo ainda pior
Seja blog técnico ou qualquer outra coisa, por favor, não deixem a IA escrever no lugar de vocês. Ninguém quer ler esse tipo de texto
Hoje descobri que Rust tem
NonZeroU64, e que ao combiná-lo comOptionaldá para obter o comportamento necessário usando só 64 bits por itemhttps://doc.rust-lang.org/std/num/type.NonZeroU64.html
Como o próprio JEP deixa claro, isso é só a primeira entrega de uma funcionalidade enorme e, como outras funções recentes do Java, está sendo entregue em partes
Naturalmente, o objetivo é também achatar valores maiores, e esse mecanismo já existe dentro da JVM. O que falta é expor no nível da linguagem a intenção de “permitir tearing”
Reconheço o esforço do trabalho que realmente entrou em Valhalla, mas acho difícil concordar com a interpretação de que “o modelo era poderoso, mas mentalmente pesado”
Dizer que uma variável não pode ser null não é uma distinção mentalmente pesada, especialmente se tudo estiver bem anotado
A postura de “simplificar o modelo para o usuário mesmo sacrificando o teto de desempenho” pode, na verdade, ter fornecido essa simplificação ao usuário
O sistema de tipos de uma linguagem de programação existe para oferecer garantias convenientes ao desenvolvedor sobre uma CPU que, no fim, só lida com números. Não há necessidade de reduzir garantias opcionais de segurança porque seriam “complexas demais”
Ainda mais quando já se chegou ao entendimento de que “o modelo da linguagem e o modelo da JVM não precisam coincidir 100%”
A governança do Java parece fraca, especialmente em contraste com o lado do .NET, que tomou decisões em geral corretas desde o começo
Hoje em dia fico até em dúvida se Java ainda tem valor ou atenção dentro da Oracle. A empresa agora parece mais um negócio de datacenter/compute com atividades legadas e uma dívida enorme grudada nisso
Às vezes dá a impressão de que os únicos setores ainda lucrativos da Oracle são o jurídico e o de cortar grama
E o marcador de null também está a caminho: https://openjdk.org/jeps/8303099
Só que isso precisa ser lançado gradualmente, e mesmo este PR que introduz classes/objetos de valor já tem 200 mil linhas
Não me parece que estejam dizendo que é impossível, e sim que não dá para comer um elefante de uma vez só
Dito isso, já faz bastante tempo que estão roendo essa perna específica
Se isso é um conceito resolvido desde 2012, não há motivo para colocar diretamente na linguagem vários sabores de estado. Os trilhos só vão para A ou para B; o desvio depende do estado da carga do trem
Quando um conceito aparece e desaparece e vira guerra de linguagem, isso é sinal de que existe demanda e de que a linguagem não está lidando direito com ela, ou que até lida, mas criando sobrecarga mental
Coisas como
Value,Errorstates,Null,IoExceptions,WeirdOsStatesNeededToHandleUpstairshttps://fsharpforfunandprofit.com/rop/
Para falar à la Monty Python, vamos em frente logo
Integer/intEm vez de dar a cada tipo uma projeção com identidade e outra sem identidade, a equipe de Valhalla fez com que tipos de valor simplesmente não tenham identidade, e por isso
Integereintviram sinônimosO layout de memória é decidido automaticamente conforme o contexto e as decisões de otimização. Por isso, o significado de
==para wrappers primitivos comoIntegertambém mudou, e agora não depende de se estar usando uma “projeção de referência” ou uma “projeção de valor”Não foi aqui que reduziram garantias opcionais de segurança por serem “mentalmente pesadas”
Uma coisa que aparece repetidamente nos comentários do HN sobre Java/JVM é que, surpreendentemente, muita gente tem uma imagem antiga da JVM ou do Java, mas quase não conhece como ele é hoje
A JVM de 2026 é um predador muito saudável. Tem defeitos? Claro que sim, mas a base é extremamente boa
No trabalho, uso o Java 26 mais recente e recursos de preview, principalmente
StructuredConcurrency, e é excelente. Mesmo vindo de empresas anteriores onde usei Haskell e Python, não me arrependo nem um poucoPessoalmente conheço os novos recursos lançados nos últimos anos, mas no trabalho real Java está literalmente preso ao passado
Muitos comentários aqui são um tanto injustos em relação ao ótimo trabalho em andamento e aos JEPs ainda melhores que estão por vir
Se comparar Java a uma criança, nos primeiros anos ele foi criado por um pai amoroso (Sun), depois foi jogado na garagem junto com outras crianças e negligenciado por um tutor maligno (Oracle)
Como ficou negligenciado e sem carinho até o JDK 8, basicamente teve de correr atrás do prejuízo
É verdade dizer que “só agora surgiram coisas como structs ou tipos-valor”, mas isso aconteceu porque o crescimento foi dificultado por processos corporativos gigantescos, burocráticos e hostis. Agora ele está livre e sendo bem cuidado pela família OpenJDK
E continuará aproveitando a alegria de escreva uma vez e distribua em qualquer lugar
Está mais para um pai amoroso que o criou e depois, por problemas financeiros, o deixou com uma família temporária, onde ele foi negligenciado
Depois foi adotado por um novo pai amoroso, a Oracle, e Java floresceu, tornando-se um adulto saudável e estável
Foi a Oracle que concluiu a abertura do ecossistema ao tornar o OpenJDK a implementação de referência, e também abriu ferramentas antes proprietárias como JFR e Mission Control
Ela também manteve muitos membros fundadores da equipe da linguagem, algo bem raro nesse tipo de aquisição, e Java melhorou muito tanto na linguagem quanto no runtime
A Oracle fez Java avançar em um ritmo sem precedentes, mantendo ao mesmo tempo a maior parte da compatibilidade retroativa
Dizem que o .NET “fez certo desde o começo”, mas se isso significa a divisão e reescrita entre .NET Framework/.NET Core/.NET, então isso não faz sentido nem dentro desta discussão. O .NET pôde aprender com Java, e mesmo assim errou em algumas coisas
O mesmo vale para o MySQL. Neste site diziam que estava “morto”, mas para quem realmente entende, ele reviveu sob a Oracle
A última versão de Java sob a Sun saiu em 2006, a Oracle comprou a Sun em 2010, o JDK 7 saiu em 2011 e o JDK 8 em 2014
A equipe em grande parte permaneceu a mesma, e a maior diferença foi que a Oracle encerrou a negligência e colocou mais dinheiro. Por isso o ritmo de Java acelerou após a aquisição
Falam em “correr atrás”, mas nem está claro atrás de quem. As únicas linguagens tão ou mais populares que Java são JS/TS e Python
Quem diz que Java ficou para trás normalmente o compara com linguagens que estão em situação muito pior. Às vezes, pessoas que gostam de um recurso específico deixam passar que a linguagem que tem esse recurso vai mal não por causa disso, mas apesar disso
Gestores gostam de lançamentos rápidos, enquanto a liderança técnica — desde a era Sun — costuma defender fazer as coisas com cautela, devagar e direito
Entendo a sensação de que Java não é tão popular quanto era em 2003, mas aquele período foi excepcionalmente unificado não só para Java, como para todo o ecossistema de software, e nunca mais houve algo tão unificado nem antes nem depois
Agora a tecnologia que realmente é escreva uma vez e distribua em qualquer lugar é o WebAssembly. A JVM teve sua chance e perdeu
Ainda assim, eu não chamaria isso de “crescimento prejudicado”. Foram feitas escolhas; algumas eram razoáveis, outras não, e esse tipo de escolha é muito difícil de corrigir depois
Basta olhar para C++: pessoalmente vejo a semicompatibilidade com C como um albatroz de 150 pés impossível de remover, e muitas versões desde o C++11 foram um trabalho de tornar esse albatroz um pouco mais suportável
Considero tratar todas as classes de valor na JVM como um único tipo-L, como os tipos primitivos, uma solução bem elegante para um problema difícil
No fim, tudo isso vem da decisão tomada no Java 2 de implementar generics com apagamento de tipos por causa da compatibilidade retroativa, e o C3 viu esse resultado e rejeitou esse caminho
Só a evolução dos tipos-valor em Java já daria um ótimo thriller técnico
Li a mailing list e vi todos os vídeos relacionados, e o processo de integrar o design em algo que sempre parecesse tipicamente Java foi realmente impressionante
Ao mesmo tempo, houve um aprofundamento muito mais detalhado no que tipos-valor significam e em que pontos quais otimizações podem ser feitas
valueEm classes de valor,
==acaba funcionando praticamente comomemcmp()Isso é meio decepcionante, porque quebra a encapsulação e expõe detalhes de implementação
O código cliente pode passar a fazer ramificações dependendo de como um dado valor é representado internamente. Em certo sentido, isso é até pior do que comparação de identidade, porque a comparação de identidade pelo menos não expõe o estado interno
Não é fazer orientação a objetos clássica de um jeito novo, e sim uma linguagem nascida da ideologia orientada a objetos dando mais um passo rumo a um mundo pós-orientação a objetos
Imagino que o lado do Java também tenha considerado bem a possibilidade de excluir padding da comparação ou forçar bytes de padding a 0
Isso também precisaria funcionar para strings. Strings claramente continuarão sendo alocadas no heap, e fazer
memcmpde ponteiros dentro de uma nova “struct” é exatamente comparação de identidadeJava separa verificação de identidade de objeto e verificação de igualdade.
==basicamente olha se dois ponteiros são o mesmo, enquanto igualdade é um conceito subjetivo baseado em interfaces comoequals/hashCodePor isso,
new Integer(1000) == new Integer(1000)antes erafalse, mas agora passa a sertrue;new Integer(1000).equals(new Integer(1000))étrue; enew Integer(10) == new Long(10)antes erafalse, mas agora vira erro de compilaçãoNo Java antigo, inteiros abaixo de certo valor às vezes eram substituídos por um tipo canonicalizado, e acho que o limite era por volta de 128. Daí vinha a diferença entre 10 e 1000
Agora parece que essas comparações fazem unboxing implícito. O fato de a comparação
Integer/Longantes serfalsee agora ser erro de compilação deixa claro que há unboxing envolvidoTalvez ainda seja possível obter o comportamento antigo usando variáveis
De todo modo, quando classes de valor perdem identidade,
==deixa de ser igualdade de ponteiro e vira igualdade bit a bit. Espero que resolvam esses vários casos de canto, mas tecnicamente é uma mudança que quebra compatibilidadeEntendo a ideia das classes de valor, mas a implementação é falha
O que o código a seguir imprime?
Point a = new Point(10, 10); Point b = a; a.x = 100; System.out.println(b.x);Até agora a resposta era clara, mas com a adição de classes de valor a resposta passa a depender de
Pointser uma classe de valor ou uma classe de referência. Então esse design prejudica a legibilidadeIsso viola o princípio da uniformidade. Em The Psychology of Computer Programming, de Weinberg, uniformidade é descrita como o princípio psicológico segundo o qual o usuário espera que coisas parecidas se comportem de forma parecida, e coisas diferentes se comportem de forma diferente
Quando uma linguagem de programação permite comportamentos semanticamente diferentes para duas construções que parecem quase idênticas no ponto de uso, ela aumenta a carga cognitiva do leitor. Para saber se atribuição, igualdade, identidade e mutação funcionam como em objetos de referência comuns ou como em valores, é preciso verificar a declaração do tipo ou depender de ferramentas
Isso poderia ser corrigido se a palavra-chave
valuefosse exigida não só na declaração, mas também no ponto de uso. Por exemplo, algo comovalue Point a = new Point(10, 10);https://openjdk.org/jeps/401
finalClaro, olhando só para essas quatro linhas não há como saber que isso aconteceria, mas esse problema já existe hoje. A mesma coisa aconteceria se
Pointfosse um recorda.x = 100;não seria válida. Tiposrecordsão imutáveisPortanto, a situação temida não deveria ser possível
Se fosse algo como
value Point a = new Point(10, 10); value Point b copy= a; a.x uniq= 100; System.out.println(b.x);, ficaria bem mais claro que ocorre clonagem/cópia e que mudar um campo não afeta o campo de outro objetoSei que no mundo Java é quase falta de educação reconhecer a existência do .NET, mas fiquei curioso sobre como isso difere de uma struct do .NET
Olhando por alto para tipos de valor, especialização de genéricos e boxing, parece que fizeram escolhas parecidas
Se C# basicamente copiou C sob uma ótica de baixo nível, o lado do Java abordou isso em nível mais alto e analisou em detalhe que restrições trazem quais vantagens
Enquanto em outras linguagens a distinção struct/classe é binária, Java permite um controle mais fino que reflete a semântica do domínio subjacente
E ficou claro que structs, especialmente em contextos paralelos, têm várias armas apontadas para o próprio pé
Pessoalmente, vejo structs de C/C# como mutáveis e passadas por cópia, enquanto classes de valor são imutáveis e passadas por valor
Acho que alocação na stack não é possível em Java
A falsa dicotomia do tipo “structs de C# têm identidade e mutação, então precisam definir com precisão a semântica de cópia em atribuição ou passagem, dando ao programador um modelo mais pesado e ao runtime menos liberdade” não combina muito com o que está sendo explicado
Pode não haver identidade no sentido da semântica de referência de classe do Java, mas no sentido de uma estrutura de memória única em um endereço específico, obviamente ainda há identidade. Isso chega perto de discutir semântica no detalhe da terminologia do Java
A nota de rodapé 6, “como isso difere de
structem C#”, é imprecisaVendo o tanto de imagens geradas por IA no texto, dá até a impressão de que houve bastante alucinação na escrita, ou pelo menos no processo de pesquisa
O texto é meio confuso e dramático, mas felizmente os documentos originais são bem fáceis de ler
Página principal: https://openjdk.org/projects/jdk/28/spec/
Status dos JEPs: https://bugs.openjdk.org/secure/Dashboard.jspa?selectPageId=...
Seria bom se alguém acompanhasse os desenvolvimentos relacionados em C#, Swift, Java e Rust. Todos vêm competindo para alcançar o hardware e, na minha visão, também vêm se influenciando mutuamente
Pessoalmente, minha preocupação é com o impacto que essas mudanças terão no compartilhamento de memória via FFI