O incrível caso do uso de objeto parcialmente carregado
Eu ganho minha vida usando Java, mas isso não significa que essa linguagem e a cultura criada ao redor dela é livre de qualquer crítica. Inclusive sou muito crítico ao Javismo cultural, mas aqui é um ponto sobre algo que eu considero uma… falha… da linguagem. E o pior: eu não vejo situação em que isso possa ser corrigido no caso do Java!
O problema do final
Qual a expectativa quando se usa final? Basicamente que ele tenha apenas uma
única leitura daquela variável! Sempre! Mas existe sempre aquela situação em
que alcançamos paradoxos. E obviamente aqui estamos lidando com paradoxos de
auto-referenciamento.
Mas não é simples que você caia nos problemas de auto-referenciamento!
Por exemplo, o seguinte código é inválido:
class X {
private static final int a = b;
private static final int b = 0;
static {
IO.println(a);
IO.println(b);
}
public static void main(String... args) {
}
}
Aqui podemos garantir que os valores que deveriam ser impressos são 0 e 0. Não
há paradoxo, não há ambiguidade. b é definido por si, a é definido em
termos de b. Mas o Java não permite isso:
$ java a.java
a.java:2: error: illegal forward reference
private static final int a = b;
^
1 error
error: compilation failed
Isso porque o Java não permite que você mencione um campo à frente na
inicialização in-line de um campo. Para usar o campo b, eu só posso usar após
a declaração dele. Mas isso não vale para métodos! Eu simplesmente posso
referenciar um método qualquer!
E sabe o que eu posso fazer dentro de métodos? Referenciar qualquer campo!
Então isso aqui é um código válido:
class X {
private static final int a = b();
private static final int b = 0;
static {
IO.println(a);
IO.println(b);
}
public static int b() {
return b;
}
public static void main(String... args) {
}
}
E o resultado é que ele imprime os valores 0 e 0 sem nenhum segredo. Vamos
deixar as coisas mais interessantes? Vamos iniciar b com 3 e o valor de a
será b + 1:
class X {
private static final int a = b() + 1;
private static final int b = 3;
static {
IO.println(a);
IO.println(b);
}
public static int b() {
return b;
}
public static void main(String... args) {
}
}
E ele imprime, como esperado:
4
3
Isso significa que os valores de a e de b são conhecidos e estáveis, não é?
NÃO É!!?!?!?!
Bem, podemos testar isso… vamos botar para imprimir os valores de a e de
b no método b(), e també, na main:
class X {
private static final int a = b() + 1;
private static final int b = 3;
static {
IO.println(a);
IO.println(b);
}
public static int b() {
IO.println("valor de (a,b) aqui na função `%s`: (%d,%d)".formatted("b", a, b));
return b;
}
public static void main(String... args) {
IO.println("valor de (a,b) aqui na função `%s`: (%d,%d)".formatted("main", a, b));
}
}
E isso é impresso:
valor de (a,b) aqui na função `b`: (0,3)
4
3
valor de (a,b) aqui na função `main`: (4,3)
Hmmm, o valor de a não está pré-computado ao chamar a função b. Isso
significa que, ao ler a variável a, obtivemos um valor em um canto e outro
valor em outros cantos!
Lembra do começa da seção, qual a expectativa do final? Basicamente que toda
leitura dele retornaria o mesmo valor! Mas aqui temos 2 leituras distintas com
2 valores distintos! E é esse o problema do final!
Como o Java se comportou de fato?
Vamos fazer outro experimento? Vamos mudar o valor de b, adicionando 0
nele. Mas não qualquer 0, um zero determinado dinamicamente, com valores que
dependem de a e de b. “Ah, mas se depende de a e de b como que pode ser
zero?” Basicamente enganando o compilador fazendo uma conta complicada para
fazer x-x, que para qualquer inteiro é 0:
class X {
private static final int a = b() + 1;
private static final int b = c() + 3;
static {
IO.println(a);
IO.println(b);
}
public static int c() {
IO.println("valor de (a,b) aqui na função `%s`: (%d,%d)".formatted("c", a, b));
if (b > 0) {
return b - b;
}
return a - a;
}
public static int b() {
IO.println("valor de (a,b) aqui na função `%s`: (%d,%d)".formatted("b", a, b));
return b;
}
public static void main(String... args) {
IO.println("valor de (a,b) aqui na função `%s`: (%d,%d)".formatted("main", a, b));
}
}
O valor de c() sempre será 0. Nos dois branches de processamento, ele sempre
retornará 0. Vamos ver como que executa o processamento?
valor de (a,b) aqui na função `b`: (0,0)
valor de (a,b) aqui na função `c`: (1,0)
1
3
valor de (a,b) aqui na função `main`: (1,3)
Mas olha que interessante! Simplesmente fazer com que a variável b dependesse
da função c() fez com que o valor de b não fosse carregado na função b()!
Mas… vamos fazer um tweak na função c()? Vamos fazer com que ela não
dependa nem de a nem de b? Podemos pegar o nosso template, o tamanho dele,
e então subtrair o valor de si mesmo:
class X {
private static final int a = b() + 1;
private static final int b = c() + 3;
static {
IO.println(a);
IO.println(b);
}
public static int c() {
final var template = "valor de (a,b) aqui na função `%s`: (%d,%d)";
return template.length() - template.length();
}
public static int b() {
IO.println("valor de (a,b) aqui na função `%s`: (%d,%d)".formatted("b", a, b));
return b;
}
public static void main(String... args) {
IO.println("valor de (a,b) aqui na função `%s`: (%d,%d)".formatted("main", a, b));
}
}
E o valor que imprime:
valor de (a,b) aqui na função `b`: (0,0)
1
3
valor de (a,b) aqui na função `main`: (1,3)
Note que não coloquei para imprimir os valores na função
c()porque, se assim eu o fizesse, por mais que o valor do retorno dec()não dependesse deaou deb, a computação iria depender em algum ponto e eu queria isolar totalmente isso.
Então, o que foi que mudou? Algo na função b() foi alterado, já que o valor
de a mudou. Vamos… olhar bytecode?
Para fazer isso, podemos chamar javap -v e passar o nome da classe em
questão. No caso, eu salvei no arquivo a.java por ser algo temporário, então
eu rodo essa linha de comando para construir o .class, pegar os bytecodes e
executar a classe main:
javac a.java && javap -v X && java X
Vamos ver como fica para o case em que b = c() + 3?
class X {
private static final int a = b() + 1;
private static final int b = c() + 3;
static {
IO.println(a);
IO.println(b);
}
private static int c() {
return 0;
}
public static int b() {
IO.println("valor de (a,b) aqui na função `%s`: (%d,%d)".formatted("b", a, b));
return b;
}
public static void main(String... args) {
IO.println("valor de (a,b) aqui na função `%s`: (%d,%d)".formatted("main", a, b));
}
}
Os bytecodes são assim:
Classfile ~/computaria/blog/etc/java/X.class
Last modified Nov 3, 2025; size 858 bytes
SHA-256 checksum 1256192f860b6063ab7518b5ffddba303e7fc2eee99454f2b3fb2a6bc2f93949
Compiled from "a.java"
class X
minor version: 0
major version: 69
flags: (0x0020) ACC_SUPER
this_class: #12 // X
super_class: #2 // java/lang/Object
interfaces: 0, fields: 2, methods: 5, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = String #8 // valor de (a,b) aqui na função `%s`: (%d,%d)
#8 = Utf8 valor de (a,b) aqui na função `%s`: (%d,%d)
#9 = String #10 // b
#10 = Utf8 b
#11 = Fieldref #12.#13 // X.a:I
#12 = Class #14 // X
#13 = NameAndType #15:#16 // a:I
#14 = Utf8 X
#15 = Utf8 a
#16 = Utf8 I
#17 = Methodref #18.#19 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#18 = Class #20 // java/lang/Integer
#19 = NameAndType #21:#22 // valueOf:(I)Ljava/lang/Integer;
#20 = Utf8 java/lang/Integer
#21 = Utf8 valueOf
#22 = Utf8 (I)Ljava/lang/Integer;
#23 = Fieldref #12.#24 // X.b:I
#24 = NameAndType #10:#16 // b:I
#25 = Methodref #26.#27 // java/lang/String.formatted:([Ljava/lang/Object;)Ljava/lang/String;
#26 = Class #28 // java/lang/String
#27 = NameAndType #29:#30 // formatted:([Ljava/lang/Object;)Ljava/lang/String;
#28 = Utf8 java/lang/String
#29 = Utf8 formatted
#30 = Utf8 ([Ljava/lang/Object;)Ljava/lang/String;
#31 = Methodref #32.#33 // java/lang/IO.println:(Ljava/lang/Object;)V
#32 = Class #34 // java/lang/IO
#33 = NameAndType #35:#36 // println:(Ljava/lang/Object;)V
#34 = Utf8 java/lang/IO
#35 = Utf8 println
#36 = Utf8 (Ljava/lang/Object;)V
#37 = String #38 // main
#38 = Utf8 main
#39 = Methodref #12.#40 // X.b:()I
#40 = NameAndType #10:#41 // b:()I
#41 = Utf8 ()I
#42 = Methodref #12.#43 // X.c:()I
#43 = NameAndType #44:#41 // c:()I
#44 = Utf8 c
#45 = Utf8 Code
#46 = Utf8 LineNumberTable
#47 = Utf8 ([Ljava/lang/String;)V
#48 = Utf8 <clinit>
#49 = Utf8 SourceFile
#50 = Utf8 a.java
{
X();
descriptor: ()V
flags: (0x0000)
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static int b();
descriptor: ()I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=5, locals=0, args_size=0
0: ldc #7 // String valor de (a,b) aqui na função `%s`: (%d,%d)
2: iconst_3
3: anewarray #2 // class java/lang/Object
6: dup
7: iconst_0
8: ldc #9 // String b
10: aastore
11: dup
12: iconst_1
13: getstatic #11 // Field a:I
16: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
19: aastore
20: dup
21: iconst_2
22: getstatic #23 // Field b:I
25: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
28: aastore
29: invokevirtual #25 // Method java/lang/String.formatted:([Ljava/lang/Object;)Ljava/lang/String;
32: invokestatic #31 // Method java/lang/IO.println:(Ljava/lang/Object;)V
35: getstatic #23 // Field b:I
38: ireturn
LineNumberTable:
line 15: 0
line 16: 35
public static void main(java.lang.String...);
descriptor: ([Ljava/lang/String;)V
flags: (0x0089) ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=5, locals=1, args_size=1
0: ldc #7 // String valor de (a,b) aqui na função `%s`: (%d,%d)
2: iconst_3
3: anewarray #2 // class java/lang/Object
6: dup
7: iconst_0
8: ldc #37 // String main
10: aastore
11: dup
12: iconst_1
13: getstatic #11 // Field a:I
16: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
19: aastore
20: dup
21: iconst_2
22: getstatic #23 // Field b:I
25: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
28: aastore
29: invokevirtual #25 // Method java/lang/String.formatted:([Ljava/lang/Object;)Ljava/lang/String;
32: invokestatic #31 // Method java/lang/IO.println:(Ljava/lang/Object;)V
35: return
LineNumberTable:
line 20: 0
line 21: 35
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: invokestatic #39 // Method b:()I
3: iconst_1
4: iadd
5: putstatic #11 // Field a:I
8: invokestatic #42 // Method c:()I
11: iconst_3
12: iadd
13: putstatic #23 // Field b:I
16: getstatic #11 // Field a:I
19: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
22: invokestatic #31 // Method java/lang/IO.println:(Ljava/lang/Object;)V
25: getstatic #23 // Field b:I
28: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
31: invokestatic #31 // Method java/lang/IO.println:(Ljava/lang/Object;)V
34: return
LineNumberTable:
line 2: 0
line 3: 8
line 6: 16
line 7: 25
line 8: 34
}
SourceFile: "a.java"
Hmmm, muita coisa. Vamos focar aqui na função <clinit> (identificada por
static{}), que é o trecho de código chamado ao inicializar a classe:
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: invokestatic #39 // Method b:()I
3: iconst_1
4: iadd
5: putstatic #11 // Field a:I
8: invokestatic #42 // Method c:()I
11: iconst_3
12: iadd
13: putstatic #23 // Field b:I
16: getstatic #11 // Field a:I
19: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
22: invokestatic #31 // Method java/lang/IO.println:(Ljava/lang/Object;)V
25: getstatic #23 // Field b:I
28: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
31: invokestatic #31 // Method java/lang/IO.println:(Ljava/lang/Object;)V
34: return
LineNumberTable:
line 2: 0
line 3: 8
line 6: 16
line 7: 25
line 8: 34
Primeira coisa a se ver: descriptor: ()V. Isso quer dizer que é uma função
sem argumentos que não tem retorno. O V significa a ausência do retorno, como
se fosse um void.
Perceba que o construtor também é indicado como se não retornasse nada:
X();
descriptor: ()V
flags: (0x0000)
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
O descritor continua sendo ()V. Como que isso é possível se para criar
objetos fazemos algo como final var x = new X();? Vamos testar. Mudar aqui
para ter uma função void printValues(String place) que chama o template para
imprimir os valores naquele local específico:
class X {
private static final int a = b() + 1;
private static final int b = c() + 3;
static {
IO.println(a);
IO.println(b);
}
private static int c() {
return 0;
}
public static int b() {
new X().printValues("b");
return b;
}
public static void main(String... args) {
new X().printValues("main");
}
public void printValues(String place) {
IO.println("valor de (a,b) aqui na função `%s`: (%d,%d)".formatted(place, a, b));
}
}
Classfile ~/computaria/blog/etc/java/X.class
Last modified Nov 3, 2025; size 942 bytes
SHA-256 checksum 2b846bf7193d17d8111a25a647772fa45e13073cc409aa8eed473613bcc1a37d
Compiled from "a.java"
class X
minor version: 0
major version: 69
flags: (0x0020) ACC_SUPER
this_class: #7 // X
super_class: #2 // java/lang/Object
interfaces: 0, fields: 2, methods: 6, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // X
#8 = Utf8 X
#9 = Methodref #7.#3 // X."<init>":()V
#10 = String #11 // b
#11 = Utf8 b
#12 = Methodref #7.#13 // X.printValues:(Ljava/lang/String;)V
#13 = NameAndType #14:#15 // printValues:(Ljava/lang/String;)V
#14 = Utf8 printValues
#15 = Utf8 (Ljava/lang/String;)V
#16 = Fieldref #7.#17 // X.b:I
#17 = NameAndType #11:#18 // b:I
#18 = Utf8 I
#19 = String #20 // main
#20 = Utf8 main
#21 = String #22 // valor de (a,b) aqui na função `%s`: (%d,%d)
#22 = Utf8 valor de (a,b) aqui na função `%s`: (%d,%d)
#23 = Fieldref #7.#24 // X.a:I
#24 = NameAndType #25:#18 // a:I
#25 = Utf8 a
#26 = Methodref #27.#28 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#27 = Class #29 // java/lang/Integer
#28 = NameAndType #30:#31 // valueOf:(I)Ljava/lang/Integer;
#29 = Utf8 java/lang/Integer
#30 = Utf8 valueOf
#31 = Utf8 (I)Ljava/lang/Integer;
#32 = Methodref #33.#34 // java/lang/String.formatted:([Ljava/lang/Object;)Ljava/lang/String;
#33 = Class #35 // java/lang/String
#34 = NameAndType #36:#37 // formatted:([Ljava/lang/Object;)Ljava/lang/String;
#35 = Utf8 java/lang/String
#36 = Utf8 formatted
#37 = Utf8 ([Ljava/lang/Object;)Ljava/lang/String;
#38 = Methodref #39.#40 // java/lang/IO.println:(Ljava/lang/Object;)V
#39 = Class #41 // java/lang/IO
#40 = NameAndType #42:#43 // println:(Ljava/lang/Object;)V
#41 = Utf8 java/lang/IO
#42 = Utf8 println
#43 = Utf8 (Ljava/lang/Object;)V
#44 = Methodref #7.#45 // X.b:()I
#45 = NameAndType #11:#46 // b:()I
#46 = Utf8 ()I
#47 = Methodref #7.#48 // X.c:()I
#48 = NameAndType #49:#46 // c:()I
#49 = Utf8 c
#50 = Utf8 Code
#51 = Utf8 LineNumberTable
#52 = Utf8 ([Ljava/lang/String;)V
#53 = Utf8 <clinit>
#54 = Utf8 SourceFile
#55 = Utf8 a.java
{
X();
descriptor: ()V
flags: (0x0000)
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static int b();
descriptor: ()I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #7 // class X
3: dup
4: invokespecial #9 // Method "<init>":()V
7: ldc #10 // String b
9: invokevirtual #12 // Method printValues:(Ljava/lang/String;)V
12: getstatic #16 // Field b:I
15: ireturn
LineNumberTable:
line 15: 0
line 17: 12
public static void main(java.lang.String...);
descriptor: ([Ljava/lang/String;)V
flags: (0x0089) ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=2, locals=1, args_size=1
0: new #7 // class X
3: dup
4: invokespecial #9 // Method "<init>":()V
7: ldc #19 // String main
9: invokevirtual #12 // Method printValues:(Ljava/lang/String;)V
12: return
LineNumberTable:
line 21: 0
line 22: 12
public void printValues(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=5, locals=2, args_size=2
0: ldc #21 // String valor de (a,b) aqui na função `%s`: (%d,%d)
2: iconst_3
3: anewarray #2 // class java/lang/Object
6: dup
7: iconst_0
8: aload_1
9: aastore
10: dup
11: iconst_1
12: getstatic #23 // Field a:I
15: invokestatic #26 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
18: aastore
19: dup
20: iconst_2
21: getstatic #16 // Field b:I
24: invokestatic #26 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
27: aastore
28: invokevirtual #32 // Method java/lang/String.formatted:([Ljava/lang/Object;)Ljava/lang/String;
31: invokestatic #38 // Method java/lang/IO.println:(Ljava/lang/Object;)V
34: return
LineNumberTable:
line 25: 0
line 26: 34
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: invokestatic #44 // Method b:()I
3: iconst_1
4: iadd
5: putstatic #23 // Field a:I
8: invokestatic #47 // Method c:()I
11: iconst_3
12: iadd
13: putstatic #16 // Field b:I
16: getstatic #23 // Field a:I
19: invokestatic #26 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
22: invokestatic #38 // Method java/lang/IO.println:(Ljava/lang/Object;)V
25: getstatic #16 // Field b:I
28: invokestatic #26 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
31: invokestatic #38 // Method java/lang/IO.println:(Ljava/lang/Object;)V
34: return
LineNumberTable:
line 2: 0
line 3: 8
line 6: 16
line 7: 25
line 8: 34
}
SourceFile: "a.java"
Vamos focar aqui na função main:
public static void main(java.lang.String...);
descriptor: ([Ljava/lang/String;)V
flags: (0x0089) ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=2, locals=1, args_size=1
0: new #7 // class X
3: dup
4: invokespecial #9 // Method "<init>":()V
7: ldc #19 // String main
9: invokevirtual #12 // Method printValues:(Ljava/lang/String;)V
12: return
LineNumberTable:
line 21: 0
line 22: 12
No primeiro instante, é chamado o bytecode new passando como parâmetro o
7º elemento da constant pool. Mas que elemento é esse?
#7 = Class #8 // X
#8 = Utf8 X
O 7º elemento indica que é uma classe, que por sua vez tem o nome representado
pelo 8º elemento. O 8º elemento diz que é uma string Utf8 com o conteúdo "X".
Mas e se por acaso a classe fosse mais complexa? Bem, temos um exemplo disso:
#2 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
A classe é indicada como tendo como nome a string Utf8 "java/lang/Object".
Ok, voltando ao ponto, indicamos que chamamos new para a classe X. Isso
garante que o espaço de memória para o objeto é reservado, não que o objeto
seja inicializado. Logo depois temos dup. Então é chamado invokespecial #9,
que é o construtor <init>:()V ca classe X. Logo depois chama ldc #19
(carrega a string "main" como próxima referência na memória) e então
invokevirtual #12. O invokevirtual é o método de chamada de método
tradicional, no caso o método #12 é o método
printValues:(Ljava/lang/String;)V da classe X. Como o ldc já carregou uma
referência em memória, a JVM sabe que o método virtual vai ser do objeto
anterior a isso na stack. Como que temos um objeto da classe X na stack?
Bem, vamos seguir a trilha do que foi chamado. No começo, o new X, que
reserva o espaço de memória. Então, chama-se dup. Logo depois, uma chamada
para invokespecial de uma função que retorna V (ie, não retorna nada). Logo
depois uma carga de ldc, e então a chamada para o método printValues que
vai consumir uma referência de X e a referência de string na stack, e como é
V não vai colocar nada no lugar. O invokespecial não coloca nada no lugar,
então ele só consome a referência.
Portanto, algo entre o new e o invokespecial deixou uma referência a mais.
E advinha o que temos no caminho? O dup! O bytecode dup duplica a
referência que está na stack. E a última referência foi a retornada pelo
bytecode new. Logo, ao fazer new X().printValues("main") ele vai fazer o
seguinte:
- criar um novo objeto do tipo
X - duplicar a referência do novo objeto criado
- consumir a primeira referência chamando o construtor, para inicializar o objeto
- colocar a constante do constant pool na stack
- chamar um método virtual que do objeto
X, consumindo a segunda referência duplicada
Ok, vamos sair dessa tangente e voltar pra análise do bytecode. O próximo ponto
é flags, que nesse caso só tem o ACC_STATIC ligado (note que b() tem as
flags ACC_PUBLIC, ACC_STATIC ligadas). Após isso, começa de verdade o código
de execução. Ele tem alguns metadados (stack=2, locals=0, args_size=0) sobre
as variáveis dele e então começa algumas coisas legais:
0: invokestatic #39 // Method b:()I
3: iconst_1
4: iadd
5: putstatic #11 // Field a:I
Aqui, ele está chamando o método estático b:()I, que não tem argumentos e
retorna um inteiro, logo depois ele coloca a constante 1 na pilha. Então ele
soma esses valores (iadd) e coloca isso em uma variável estática com
putstatic. Nesse caso está colocando no campo estática a.
Logo depois temos algo semelhante que coloca o valor no campo estático b:
8: invokestatic #42 // Method c:()I
11: iconst_3
12: iadd
13: putstatic #23 // Field b:I
Na função b() temos isso:
public static int b();
descriptor: ()I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=5, locals=0, args_size=0
0: ldc #7 // String valor de (a,b) aqui na função `%s`: (%d,%d)
2: iconst_3
3: anewarray #2 // class java/lang/Object
6: dup
7: iconst_0
8: ldc #9 // String b
10: aastore
11: dup
12: iconst_1
13: getstatic #11 // Field a:I
16: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
19: aastore
20: dup
21: iconst_2
22: getstatic #23 // Field b:I
25: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
28: aastore
29: invokevirtual #25 // Method java/lang/String.formatted:([Ljava/lang/Object;)Ljava/lang/String;
32: invokestatic #31 // Method java/lang/IO.println:(Ljava/lang/Object;)V
35: getstatic #23 // Field b:I
38: ireturn
LineNumberTable:
line 15: 0
line 16: 35
Mas o que realmente interessa é apenas isso (vou cortar toda a burocracia do imprimir os valores):
public static int b();
descriptor: ()I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=5, locals=0, args_size=0
// ...
35: getstatic #23 // Field b:I
38: ireturn
LineNumberTable:
line 15: 0
line 16: 35
Basicamente ele pega o valor do campo estático b e retorna.
Certo, agora vamos ver se muda muita coisa ao inicializar o valor de b apenas
com a constante 3:
class X {
private static final int a = b() + 1;
private static final int b = 3;
static {
IO.println(a);
IO.println(b);
}
public static int b() {
IO.println("valor de (a,b) aqui na função `%s`: (%d,%d)".formatted("b", a, b));
return b;
}
public static void main(String... args) {
IO.println("valor de (a,b) aqui na função `%s`: (%d,%d)".formatted("main", a, b));
}
}
Classfile ~/computaria/blog/etc/java/X.class
Last modified Nov 3, 2025; size 803 bytes
SHA-256 checksum 1daee38dfbaf52b8404b825ad86ac7948e04da4ad15c7e27afd22063ed0a8314
Compiled from "a.java"
class X
minor version: 0
major version: 69
flags: (0x0020) ACC_SUPER
this_class: #12 // X
super_class: #2 // java/lang/Object
interfaces: 0, fields: 2, methods: 4, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = String #8 // valor de (a,b) aqui na função `%s`: (%d,%d)
#8 = Utf8 valor de (a,b) aqui na função `%s`: (%d,%d)
#9 = String #10 // b
#10 = Utf8 b
#11 = Fieldref #12.#13 // X.a:I
#12 = Class #14 // X
#13 = NameAndType #15:#16 // a:I
#14 = Utf8 X
#15 = Utf8 a
#16 = Utf8 I
#17 = Methodref #18.#19 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#18 = Class #20 // java/lang/Integer
#19 = NameAndType #21:#22 // valueOf:(I)Ljava/lang/Integer;
#20 = Utf8 java/lang/Integer
#21 = Utf8 valueOf
#22 = Utf8 (I)Ljava/lang/Integer;
#23 = Methodref #24.#25 // java/lang/String.formatted:([Ljava/lang/Object;)Ljava/lang/String;
#24 = Class #26 // java/lang/String
#25 = NameAndType #27:#28 // formatted:([Ljava/lang/Object;)Ljava/lang/String;
#26 = Utf8 java/lang/String
#27 = Utf8 formatted
#28 = Utf8 ([Ljava/lang/Object;)Ljava/lang/String;
#29 = Methodref #30.#31 // java/lang/IO.println:(Ljava/lang/Object;)V
#30 = Class #32 // java/lang/IO
#31 = NameAndType #33:#34 // println:(Ljava/lang/Object;)V
#32 = Utf8 java/lang/IO
#33 = Utf8 println
#34 = Utf8 (Ljava/lang/Object;)V
#35 = String #36 // main
#36 = Utf8 main
#37 = Methodref #12.#38 // X.b:()I
#38 = NameAndType #10:#39 // b:()I
#39 = Utf8 ()I
#40 = Utf8 ConstantValue
#41 = Integer 3
#42 = Utf8 Code
#43 = Utf8 LineNumberTable
#44 = Utf8 ([Ljava/lang/String;)V
#45 = Utf8 <clinit>
#46 = Utf8 SourceFile
#47 = Utf8 a.java
{
X();
descriptor: ()V
flags: (0x0000)
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static int b();
descriptor: ()I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=5, locals=0, args_size=0
0: ldc #7 // String valor de (a,b) aqui na função `%s`: (%d,%d)
2: iconst_3
3: anewarray #2 // class java/lang/Object
6: dup
7: iconst_0
8: ldc #9 // String b
10: aastore
11: dup
12: iconst_1
13: getstatic #11 // Field a:I
16: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
19: aastore
20: dup
21: iconst_2
22: iconst_3
23: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
26: aastore
27: invokevirtual #23 // Method java/lang/String.formatted:([Ljava/lang/Object;)Ljava/lang/String;
30: invokestatic #29 // Method java/lang/IO.println:(Ljava/lang/Object;)V
33: iconst_3
34: ireturn
LineNumberTable:
line 11: 0
line 12: 33
public static void main(java.lang.String...);
descriptor: ([Ljava/lang/String;)V
flags: (0x0089) ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=5, locals=1, args_size=1
0: ldc #7 // String valor de (a,b) aqui na função `%s`: (%d,%d)
2: iconst_3
3: anewarray #2 // class java/lang/Object
6: dup
7: iconst_0
8: ldc #35 // String main
10: aastore
11: dup
12: iconst_1
13: getstatic #11 // Field a:I
16: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
19: aastore
20: dup
21: iconst_2
22: iconst_3
23: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
26: aastore
27: invokevirtual #23 // Method java/lang/String.formatted:([Ljava/lang/Object;)Ljava/lang/String;
30: invokestatic #29 // Method java/lang/IO.println:(Ljava/lang/Object;)V
33: return
LineNumberTable:
line 16: 0
line 17: 33
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: invokestatic #37 // Method b:()I
3: iconst_1
4: iadd
5: putstatic #11 // Field a:I
8: getstatic #11 // Field a:I
11: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
14: invokestatic #29 // Method java/lang/IO.println:(Ljava/lang/Object;)V
17: iconst_3
18: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
21: invokestatic #29 // Method java/lang/IO.println:(Ljava/lang/Object;)V
24: return
LineNumberTable:
line 2: 0
line 6: 8
line 7: 17
line 8: 24
}
SourceFile: "a.java"
Focando na inicialização estática inicialmente:
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: invokestatic #37 // Method b:()I
3: iconst_1
4: iadd
5: putstatic #11 // Field a:I
8: getstatic #11 // Field a:I
11: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
14: invokestatic #29 // Method java/lang/IO.println:(Ljava/lang/Object;)V
17: iconst_3
18: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
21: invokestatic #29 // Method java/lang/IO.println:(Ljava/lang/Object;)V
24: return
LineNumberTable:
line 2: 0
line 6: 8
line 7: 17
line 8: 24
Ele começa povoando a variável estática a:
0: invokestatic #37 // Method b:()I
3: iconst_1
4: iadd
5: putstatic #11 // Field a:I
Igual ao que tinha antes (dado uma pequena mudança nos offsets do
constant pool). E depois disso ele chama para preencher os IO.println(...).
Hmmm, não povoou a variável b aqui. Inclusive, em Fieldref só há menção
para o campo a da classe X, não tem mais para o campo b da classe X.
Hmmm, intrigante. E a função b()?
public static int b();
descriptor: ()I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=5, locals=0, args_size=0
// ...
33: iconst_3
34: ireturn
LineNumberTable:
line 11: 0
line 12: 33
Tem muita bucrocia e no final tem:
- carrega o inteiro
3na pilha - retorna um inteiro
E só? O valor estático b sumiu? Pelo visto…
Ok, vamos fazer um pequeno experimento, deixar esses valores como parte visível
da API dessa classe, vamos remover o private tanto do campo a como do campo
b:
class X {
static final int a = b() + 1;
static final int b = 3;
static {
IO.println(a);
IO.println(b);
}
public static int b() {
IO.println("valor de (a,b) aqui na função `%s`: (%d,%d)".formatted("b", a, b));
return b;
}
public static void main(String... args) {
IO.println("valor de (a,b) aqui na função `%s`: (%d,%d)".formatted("main", a, b));
}
}
Classfile ~/computaria/blog/etc/java/X.class
Last modified Nov 4, 2025; size 803 bytes
SHA-256 checksum 7e138053b9ce2ad0ed79bee707080c89cacbb12b3bd71f4110865444a6399918
Compiled from "a.java"
class X
minor version: 0
major version: 69
flags: (0x0020) ACC_SUPER
this_class: #12 // X
super_class: #2 // java/lang/Object
interfaces: 0, fields: 2, methods: 4, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = String #8 // valor de (a,b) aqui na função `%s`: (%d,%d)
#8 = Utf8 valor de (a,b) aqui na função `%s`: (%d,%d)
#9 = String #10 // b
#10 = Utf8 b
#11 = Fieldref #12.#13 // X.a:I
#12 = Class #14 // X
#13 = NameAndType #15:#16 // a:I
#14 = Utf8 X
#15 = Utf8 a
#16 = Utf8 I
#17 = Methodref #18.#19 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#18 = Class #20 // java/lang/Integer
#19 = NameAndType #21:#22 // valueOf:(I)Ljava/lang/Integer;
#20 = Utf8 java/lang/Integer
#21 = Utf8 valueOf
#22 = Utf8 (I)Ljava/lang/Integer;
#23 = Methodref #24.#25 // java/lang/String.formatted:([Ljava/lang/Object;)Ljava/lang/String;
#24 = Class #26 // java/lang/String
#25 = NameAndType #27:#28 // formatted:([Ljava/lang/Object;)Ljava/lang/String;
#26 = Utf8 java/lang/String
#27 = Utf8 formatted
#28 = Utf8 ([Ljava/lang/Object;)Ljava/lang/String;
#29 = Methodref #30.#31 // java/lang/IO.println:(Ljava/lang/Object;)V
#30 = Class #32 // java/lang/IO
#31 = NameAndType #33:#34 // println:(Ljava/lang/Object;)V
#32 = Utf8 java/lang/IO
#33 = Utf8 println
#34 = Utf8 (Ljava/lang/Object;)V
#35 = String #36 // main
#36 = Utf8 main
#37 = Methodref #12.#38 // X.b:()I
#38 = NameAndType #10:#39 // b:()I
#39 = Utf8 ()I
#40 = Utf8 ConstantValue
#41 = Integer 3
#42 = Utf8 Code
#43 = Utf8 LineNumberTable
#44 = Utf8 ([Ljava/lang/String;)V
#45 = Utf8 <clinit>
#46 = Utf8 SourceFile
#47 = Utf8 a.java
{
static final int a;
descriptor: I
flags: (0x0018) ACC_STATIC, ACC_FINAL
static final int b;
descriptor: I
flags: (0x0018) ACC_STATIC, ACC_FINAL
ConstantValue: int 3
X();
descriptor: ()V
flags: (0x0000)
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static int b();
descriptor: ()I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=5, locals=0, args_size=0
0: ldc #7 // String valor de (a,b) aqui na função `%s`: (%d,%d)
2: iconst_3
3: anewarray #2 // class java/lang/Object
6: dup
7: iconst_0
8: ldc #9 // String b
10: aastore
11: dup
12: iconst_1
13: getstatic #11 // Field a:I
16: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
19: aastore
20: dup
21: iconst_2
22: iconst_3
23: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
26: aastore
27: invokevirtual #23 // Method java/lang/String.formatted:([Ljava/lang/Object;)Ljava/lang/String;
30: invokestatic #29 // Method java/lang/IO.println:(Ljava/lang/Object;)V
33: iconst_3
34: ireturn
LineNumberTable:
line 11: 0
line 12: 33
public static void main(java.lang.String...);
descriptor: ([Ljava/lang/String;)V
flags: (0x0089) ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=5, locals=1, args_size=1
0: ldc #7 // String valor de (a,b) aqui na função `%s`: (%d,%d)
2: iconst_3
3: anewarray #2 // class java/lang/Object
6: dup
7: iconst_0
8: ldc #35 // String main
10: aastore
11: dup
12: iconst_1
13: getstatic #11 // Field a:I
16: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
19: aastore
20: dup
21: iconst_2
22: iconst_3
23: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
26: aastore
27: invokevirtual #23 // Method java/lang/String.formatted:([Ljava/lang/Object;)Ljava/lang/String;
30: invokestatic #29 // Method java/lang/IO.println:(Ljava/lang/Object;)V
33: return
LineNumberTable:
line 16: 0
line 17: 33
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: invokestatic #37 // Method b:()I
3: iconst_1
4: iadd
5: putstatic #11 // Field a:I
8: getstatic #11 // Field a:I
11: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
14: invokestatic #29 // Method java/lang/IO.println:(Ljava/lang/Object;)V
17: iconst_3
18: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
21: invokestatic #29 // Method java/lang/IO.println:(Ljava/lang/Object;)V
24: return
LineNumberTable:
line 2: 0
line 6: 8
line 7: 17
line 8: 24
}
SourceFile: "a.java"
O único Fieldref que tem é a menção ao X.a. Mas após o constant pool ele
menciona uma coisa interessante:
static final int a;
descriptor: I
flags: (0x0018) ACC_STATIC, ACC_FINAL
static final int b;
descriptor: I
flags: (0x0018) ACC_STATIC, ACC_FINAL
ConstantValue: int 3
As variáveis a e b como parte da API da classe! Note que b já tem valor
determinístico definido, 3. Por isso que não é inicializada tal qual a.
Agora… e se eu mudar a inicialização de b? No lugar de ser inicialização na
declaração do campo, por no campo estático?
class X {
static final int a = b() + 1;
static final int b;
static {
b = 3;
IO.println(a);
IO.println(b);
}
// ...
}
O bytecode gerado muda um pouquinho:
Classfile ~/computaria/blog/etc/java/X.class
Last modified Nov 4, 2025; size 800 bytes
SHA-256 checksum 31c6b9274e883db117b635693265c4bcc20cf2f3e51ecd877cfe0b3fbd1fe097
Compiled from "a.java"
class X
minor version: 0
major version: 69
flags: (0x0020) ACC_SUPER
this_class: #12 // X
super_class: #2 // java/lang/Object
interfaces: 0, fields: 2, methods: 4, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = String #8 // valor de (a,b) aqui na função `%s`: (%d,%d)
#8 = Utf8 valor de (a,b) aqui na função `%s`: (%d,%d)
#9 = String #10 // b
#10 = Utf8 b
#11 = Fieldref #12.#13 // X.a:I
#12 = Class #14 // X
#13 = NameAndType #15:#16 // a:I
#14 = Utf8 X
#15 = Utf8 a
#16 = Utf8 I
#17 = Methodref #18.#19 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#18 = Class #20 // java/lang/Integer
#19 = NameAndType #21:#22 // valueOf:(I)Ljava/lang/Integer;
#20 = Utf8 java/lang/Integer
#21 = Utf8 valueOf
#22 = Utf8 (I)Ljava/lang/Integer;
#23 = Fieldref #12.#24 // X.b:I
#24 = NameAndType #10:#16 // b:I
#25 = Methodref #26.#27 // java/lang/String.formatted:([Ljava/lang/Object;)Ljava/lang/String;
#26 = Class #28 // java/lang/String
#27 = NameAndType #29:#30 // formatted:([Ljava/lang/Object;)Ljava/lang/String;
#28 = Utf8 java/lang/String
#29 = Utf8 formatted
#30 = Utf8 ([Ljava/lang/Object;)Ljava/lang/String;
#31 = Methodref #32.#33 // java/lang/IO.println:(Ljava/lang/Object;)V
#32 = Class #34 // java/lang/IO
#33 = NameAndType #35:#36 // println:(Ljava/lang/Object;)V
#34 = Utf8 java/lang/IO
#35 = Utf8 println
#36 = Utf8 (Ljava/lang/Object;)V
#37 = String #38 // main
#38 = Utf8 main
#39 = Methodref #12.#40 // X.b:()I
#40 = NameAndType #10:#41 // b:()I
#41 = Utf8 ()I
#42 = Utf8 Code
#43 = Utf8 LineNumberTable
#44 = Utf8 ([Ljava/lang/String;)V
#45 = Utf8 <clinit>
#46 = Utf8 SourceFile
#47 = Utf8 a.java
{
static final int a;
descriptor: I
flags: (0x0018) ACC_STATIC, ACC_FINAL
static final int b;
descriptor: I
flags: (0x0018) ACC_STATIC, ACC_FINAL
X();
descriptor: ()V
flags: (0x0000)
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static int b();
descriptor: ()I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=5, locals=0, args_size=0
0: ldc #7 // String valor de (a,b) aqui na função `%s`: (%d,%d)
2: iconst_3
3: anewarray #2 // class java/lang/Object
6: dup
7: iconst_0
8: ldc #9 // String b
10: aastore
11: dup
12: iconst_1
13: getstatic #11 // Field a:I
16: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
19: aastore
20: dup
21: iconst_2
22: getstatic #23 // Field b:I
25: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
28: aastore
29: invokevirtual #25 // Method java/lang/String.formatted:([Ljava/lang/Object;)Ljava/lang/String;
32: invokestatic #31 // Method java/lang/IO.println:(Ljava/lang/Object;)V
35: getstatic #23 // Field b:I
38: ireturn
LineNumberTable:
line 12: 0
line 13: 35
public static void main(java.lang.String...);
descriptor: ([Ljava/lang/String;)V
flags: (0x0089) ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=5, locals=1, args_size=1
0: ldc #7 // String valor de (a,b) aqui na função `%s`: (%d,%d)
2: iconst_3
3: anewarray #2 // class java/lang/Object
6: dup
7: iconst_0
8: ldc #37 // String main
10: aastore
11: dup
12: iconst_1
13: getstatic #11 // Field a:I
16: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
19: aastore
20: dup
21: iconst_2
22: getstatic #23 // Field b:I
25: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
28: aastore
29: invokevirtual #25 // Method java/lang/String.formatted:([Ljava/lang/Object;)Ljava/lang/String;
32: invokestatic #31 // Method java/lang/IO.println:(Ljava/lang/Object;)V
35: return
LineNumberTable:
line 17: 0
line 18: 35
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: invokestatic #39 // Method b:()I
3: iconst_1
4: iadd
5: putstatic #11 // Field a:I
8: iconst_3
9: putstatic #23 // Field b:I
12: getstatic #11 // Field a:I
15: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
18: invokestatic #31 // Method java/lang/IO.println:(Ljava/lang/Object;)V
21: getstatic #23 // Field b:I
24: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
27: invokestatic #31 // Method java/lang/IO.println:(Ljava/lang/Object;)V
30: return
LineNumberTable:
line 2: 0
line 6: 8
line 7: 12
line 8: 21
line 9: 30
}
SourceFile: "a.java"
Note que agora:
- tem o
Fieldrefparab(campo 23 do constant pool) - a variável
bnão aparece com valor na declaração dela - a função
b()usagetstatic #23no lugar deiconst_3 - existe inicialização do campo
b:iconst_3; putstatic #23
E isso obviamente alterou a saída também:
valor de (a,b) aqui na função `b`: (0,0)
1
3
valor de (a,b) aqui na função `main`: (1,3)
E como resolve esse problema?
Na hipótese “closed world”, podemos simplesmente fazer um grafo de dependência.
Não posso usar uma variável antes de ela estar inicializada, e assim eu posso
determinar que vai quebrar a compilação. Nessa situação de “closed world”,
podemos dizer que a função b() depende da variável b, e que a variável a
depende da função b(). Assim sendo, como a depende de b(), isso significa
que eu teria de remover essa linha:
IO.println("valor de (a,b) aqui na função `%s`: (%d,%d)".formatted("b", a, b));
Então, assim, para toda função teríamos e para toda variável teríamos um grafo de dependência. Nesse sentido, poderíamos fazer até inverter a ordem das declarações, visto que já há garantia de inicialização dos valores:
class X {
private final static int a = b + 1;
private final static int b = 3;
// ...
}
Isso seria possível pois saberíamos que a depende de b, mas que b não tem
nenhuma dependência para existir. Inclusive Rust trabalha assim:
static A: i32 = valor();
static B: i32 = value();
const fn value() -> i32 {
A + 1
}
const fn valor() -> i32 {
B + 1
}
fn main() {
println!("{A} / {B}");
}
Isso gera o seguinte erro:

Imagem cortesia do Shinobu, post original.
Tá, mas só em carga de classes?
Não, isso também ocorre com criação de objetos. Você pode enviar um objeto
parcialmente carregado em qualquer momento, dentro do construtor. Qualquer
método que você chamar no construtor passando this pode incorrer nesse
problema.
O exemplo com a classe e campos estáticos é apenas mais pungente, pois se espera que a classe esteja devidamente carregada para se poder usar.
Mas por quê Java está condenado a esse problema?
Porque Java NÃO É closed world. A qualquer momento é esperado que um .class
seja alterado no classpath.
Se fosse closed world, eu poderia fazer isso sem problemas, com 2 classes:
// com/jeffque/A.java
package com.jeffque;
import com.jeffque.etc.Consts;
public class A {
public static int field1() {
return FIELD_1;
}
public static int field2() {
return FIELD_2;
}
public static final int FIELD_1;
public static final int FIELD_2;
static {
FIELD_1 = Consts.firstPrime() * Consts.secondPrime();
FIELD_2 = Consts.arbitraryHash();
}
}
// com/jeffque/etc/Confs.java
package com.jeffque.etc;
public final class Confs {
public static int firstPrime() {
return 7;
}
public static int secondPrime() {
return 29;
}
public static int arbitraryHash() {
return 0xDEADBEEF;
}
}
Aqui as dependências são:
A.field1()=>A.FIELD_1A.field2()=>A.FIELD_2A.FIELD_1=>Consts.firstPrime(),Consts.secondPrime()A.FIELD_2=>Consts.arbitraryHash()
Agora, vamos alterar o hash arbitrário…
// com/jeffque/etc/Confs.java
package com.jeffque.etc;
import external.source.Values;
public final class Confs {
public static int firstPrime() {
return 7;
}
public static int secondPrime() {
return 29;
}
public static int arbitraryHash() {
return Values.basic ^ 0xDEADBEEF;
}
}
E aí, o que isso aqui imprimiria?
// com/jeffque/app/App1.java
import com.jeffque.A;
public class App1 {
public static void main(String... args) {
System.out.println(A.field2());
}
}
Isso depende agora do valor de Values.basic. Como Java assume que é “open
world”, isso significa que ele não consegue (sempre) precomputar o valor de
Confs.arbitraryHash, já que o valor depende de um código externo que não
temos garantia de como é povoado.
Portanto, em um dado momento, o retorno pode ser 0xDEADBEEF, mas se a
qualquer momento o classloader carregar external.source.Values de outro
lugar, ou de repente o arquivo dessa classe ser sobrescrito (considerando que
ou a JVM não tem essa classe carregada), o valor pode se tornar 0xDEADBE00.
Considerações finais
Isso é uma falha da linguagem, IMHO. Mas, também, é um problema que foi aceito desde o momento em que a linguagem foi concebida, já que ela foi feita assumindo como premissa “open world”, já que normalmente prender esse valor de maneira que ele sempre seja um único valor basicamente implica em assumir “closed world”.
Então, no lugar de fechar o mundo, a escolha foi feita como um compromise e permitir esse corner case. Uma escolha que eu entendo e, para manter a premissa que precisa funcionar como “open world”, foi a melhor possível.