java에서 상수를 선언하기 위해 final을 통해 변수를 선언한다.

final이 사용된 변수는 초기화 된 후에 다시 값을 바꿀 수 없다.
하지만 Reflection을 사용하면 강제적으로 final의 값을 "바꿀 수도" 있다.
import java.lang.reflect.Field;
public class ConstantValues {
final int fieldInit = 42;
final int instanceInit;
final int constructorInit;
{
instanceInit = 42;
}
public ConstantValues() {
constructorInit = 42;
}
static void set(ConstantValues p, String field) throws Exception {
Field f = ConstantValues.class.getDeclaredField(field);
f.setAccessible(true);
f.setInt(p, 9000);
}
public static void main(String... args) throws Exception {
ConstantValues p = new ConstantValues();
set(p, "fieldInit");
set(p, "instanceInit");
set(p, "constructorInit");
System.out.println(p.fieldInit + " " + p.instanceInit + " " + p.constructorInit);
}
}
ConstantValues에는 3가지 방식으로 초기화 되는 3개의 final 변수가 있다. setAccessible(true)를 통해 final 변수에 대한 접근 제한을 해제하고 필드의 값을 9000으로 설정하는 코드이다.
위 코드를 실행하면 실행 결과는 42 9000 9000으로, fieldInit의 값만 바뀌지 않은것을 알 수 있다.
컴파일한 바이트 코드를 intelliJ의 디컴파일러 또는 javap를 통해 클래스 파일의 내용을 확인해보면, 그 이유를 알 수 있다.
final int fieldInit;
descriptor: I
flags: (0x0010) ACC_FINAL
ConstantValue: int 42
final int instanceInit;
descriptor: I
flags: (0x0010) ACC_FINAL
final int constructorInit;
descriptor: I
flags: (0x0010) ACC_FINAL
중략...
(print 부분)
33: pop
34: bipush 42
36: aload_1
37: getfield #13 // Field instanceInit:I
40: aload_1
41: getfield #16 // Field constructorInit:I
fieldInit은 ConstantValue를 통해 미리 값이 들어가 있을 뿐만 아니라 사용되는 자리에 42라는 값 자체로 하드코딩되어 있는 모습을 볼 수 있다. Reflection으로 값을 바꿔도 출력이 바뀌지 않은 이유는 저렇게 42라는 값 자체가 바이트 코드에 들어있어서 반영되지 않았던것이다.

intellj의 디컴파일러를 통하면, 42가 그대로 들어가 있는걸 보고 더 쉽게 알 수 있다.
위 예시처럼 final 필드의 값을 "컴파일 단계에서 알 수 있도록" 넣어주면 변수를 참조하는 대신 그 값 자체를 대입하여 컴파일 되기 때문에, 성능면에서 약간의 이득을 볼 수 있다.
final Example example = new Example(); 처럼 객체를 필드로 가지는 경우엔 컴파일 단계에서 Example 인스턴스의 메모리 위치를 알 수 없으므로 저러한 최적화가 일어나지 않는다.