# Java에서 final이 붙으면 불변일까?

### 💡정말 final 변수**는 불변할까?**

> Once a final variable has been assigned, it always contains the same value.

[JLS11](https://docs.oracle.com/javase/specs/jls/se11/html/jls-4.html#jls-4.12.4)을 참고해 보면 final 변수는 값이 한번 할당되면, 항상 같은 값을 유지한다고 합니다. 그런데 제가 알기로 리플렉션을 이용하면 final을 사용하더라도 "재할당"이 가능하다고 알고 있었는데, 틀렸던 걸까요? 다음 코드를 한번 살펴봅시다.

```java
public class Main {

    private static final Integer value = 10;

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        System.out.println(value); // [1]
        setFinalStatic(Main.class.getDeclaredField("value"), 20);
        System.out.println(value); // [2]
    }

    private static void setFinalStatic(Field field, Object newValue) throws NoSuchFieldException, IllegalAccessException {
        field.setAccessible(true);

        // Field 객체의 지역 변수 modifiers를 획득합니다. 
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        // field.getModifiers()의 Modifier.FINAL 값을 반전시킵니다.
        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

        field.set(null, newValue);
    }
}
```

인라인으로 final 키워드를 붙여준 Integer형 `value`는 `10`으로 초기화 되어있습니다. final 키워드를 붙여서 값이 항상 불변하다고 한다면, 리플렉션을 사용하더라도 변하지 않아야겠죠.

그렇다면 `main()` 메서드의 \[1\], \[2\]에서 출력하는 `value`는 각각 무엇일까요?

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690252549027/c93c56fc-8f93-459a-800d-73879af6b653.png align="center")

Java11에서는 금지된 리플렉션 접근이라는 경고를 띄우긴 하지만, 런타임에 `10, 20`을 출력해주네요. 값이 바뀐 것을 확인할 수 있습니다. 내부에서는 다음과 같이 동작합니다.

1. 원시 타입 `10`, `20`은 참조 타입 `new Integer(10)`, `new Integer(20)`으로 오토박싱 됩니다.
    
2. \[1\]에서 `value`는 `new Integer(10)`을 참조하고 있으므로 `10`을 출력합니다.
    
3. 리플렉션이 Integer 타입인 `value`가 `new Integer(20)`을 참조하도록 변경합니다.
    
4. 결과적으로 \[2\]에서 `value`는 `new Integer(20)`을 참조하므로 `20`을 출력합니다.
    

실제로 주소를 비교를 해보면, 참조 값이 변경된 것을 알 수 있습니다.

```java
Object o = value;
System.out.println(System.identityHashCode(o)); // 985922955
System.out.println(System.identityHashCode(value)); // 985922955
System.out.println(o == value); // true
setFinalStatic(Main.class.getDeclaredField("value"), 20);
System.out.println(System.identityHashCode(o)); // 985922955
System.out.println(System.identityHashCode(value)); // 873415566
System.out.println(o == value); // false
```

fyi. Java12부터는 아래 예외가 발생하기 때문에, [조금 다른 방식](https://bugs.openjdk.org/browse/JDK-8210522)으로 작성해야 합니다. (이 문서는 `JLS11` 기준으로 작성되었습니다.)

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690252542819/f1794da1-13ef-4232-a00e-8d09ac537a25.png align="center")

### 💡**그건 참조 타입이니까 그렇고, 원시 타입은 수정 안 돼.**

> If a final variable holds a reference to an object, then the state of the object may be changed by operations on the object, but the variable will always refer to the same object.

JLS 문서에 의거하여 "참조형은 상태는 수정이 가능하다. 하지만 final 변수가 참조하는 객체는 변경할 수 없다"고 알고 있나요? 그런데 우리는 분명 `참조하는 객체를 변경`했습니다.

다시 생각해보면, 우리가 값을 변경했던 `value`는 `Integer` 타입의 참조 자료형이었습니다. 런타임에 final 키워드를 제거해버렸으니 새로운 레퍼런스를 가리키게 하는 것은 당연할 지도 모릅니다.

그렇다면 과연 처음부터 원시 타입으로 초기화한 경우는 어떨까요?

### **💡놀랍게도 원시 타입 또한 중간에 변경이 됩니다.**

> Even then, there are a number of complications. If a final field is initialized to a constant expression in the field declaration, changes to the final field may not be observed, since uses of that final field are replaced at compile time with the value of the constant expression.

중간에 수정은 되지만, 다만 다시 상수 값으로 대체되어 그래서 변경 사항을 관찰할 수 없다고 합니다. 코드로 살펴봅시다.

```java
public class Main {

    // Integer가 int로 바뀜
    private static final int value = 10;

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        System.out.println(value);
        setFinalStatic(Main.class.getDeclaredField("value"), 20);
        System.out.println(value);
    }

    private static void setFinalStatic(Field field, Object newValue) throws NoSuchFieldException, IllegalAccessException {
        ...
    }
}
```

처음 봤던 코드에서 레퍼런스 타입을 원시 타입으로 바꾼 코드입니다. 이 경우의 출력값은 어떨까요?

정답은 `10, 10`입니다. 안 바뀐 거 맞지 않냐구요? 최종적으로는 값이 같지만, 중간에 사실 20으로 바뀌었습니다. `javap -c` 커맨드로 클래스 파일을 역어셈블하여 한번 확인해 봅시다. 바이트 코드에 대해 궁금하시다면 [D2 블로그](https://d2.naver.com/helloworld/1230)를 참고해 주세요.

```java
Compiled from "Main.java"
public class Main {
  public Main();
    Code:
       0: aload_0
       1: invokespecial #1     // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.NoSuchFieldException, java.lang.IllegalAccessException;
    Code:
       0: getstatic     #7     // Field java/lang/System.out:Ljava/io/PrintStream;
       3: bipush        10
       5: invokevirtual #15    // Method java/io/PrintStream.println:(I)V
       8: ldc           #13    // class Main
      10: ldc           #21    // String value
      12: invokevirtual #23    // Method java/lang/Class.getDeclaredField:(Ljava/lang/String;)Ljava/lang/reflect/Field;
      15: bipush        20
      17: invokestatic  #29    // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      20: invokestatic  #35    // Method setFinalStatic:(Ljava/lang/reflect/Field;Ljava/lang/Object;)V
      23: getstatic     #7     // Field java/lang/System.out:Ljava/io/PrintStream;
      26: bipush        10
      28: invokevirtual #15    // Method java/io/PrintStream.println:(I)V
      31: return
}
```

`bipush`는 OpCode는 1byte 크기의 정수를 스택에 로드한다는 의미입니다. 그래서 `3: bipush 10`는 1byte 크기의 `10`이라는 피연산자를 스택에 넣는 명령이 됩니다. `invokevirtual`는 메서드를 호출하는 명령어의 OpCode인데, 2byte의 피연산자를 필요로 합니다. `12: invokevirtual #23`는 `getDeclaredField()` 메서드를 호출하는 명령이 됩니다.

그런데 다음 줄을 보면 `15: bipush 20` 명령이 있네요. 스택에 있던 값을 다시 20으로 바꾸어버렸습니다! 리플렉션으로 분명히 중간에 스택의 값이 변경되었지만, `value`는 컴파일 타임의 상수이기 때문에 다시 20으로 대체됩니다.

결론적으로 JVM에 로드된 런타임 시점에서는 변경이 됐었다는 것을 알 수가 없습니다.

---

### 🎯**정리**

* final 키워드를 붙이더라도 Field의 modifiers를 가져와서 final 선언을 무효화시킬 수 있습니다.
    
* 참조 타입은 실제로 위의 방식으로 변경이 가능합니다!
    
* 원시 타입은 최종적으로는 변경이 되지 않습니다. 불변하다고 표현할 수 있겠네요.
    

---

### 🔖참고

* [https://docs.oracle.com/javase/specs/jls/se11/html/jls-4.html#jls-4.12.4](https://docs.oracle.com/javase/specs/jls/se11/html/jls-4.html#jls-4.12.4)
    
* [https://docs.oracle.com/javase/specs/jls/se11/html/jls-16.html](https://docs.oracle.com/javase/specs/jls/se11/html/jls-16.html)
    
* [https://docs.oracle.com/javase/specs/jls/se11/html/jls-17.html#jls-17.5.3](https://docs.oracle.com/javase/specs/jls/se11/html/jls-17.html#jls-17.5.3)
    
* [https://stackoverflow.com/questions/3301635/change-private-static-final-field-using-java-reflection](https://stackoverflow.com/questions/3301635/change-private-static-final-field-using-java-reflection)
    
* [https://stackoverflow.com/questions/17506329/java-final-field-compile-time-constant-expression](https://stackoverflow.com/questions/17506329/java-final-field-compile-time-constant-expression)
    
* [https://stackoverflow.com/questions/74723932/java-17-reflection-issue](https://stackoverflow.com/questions/74723932/java-17-reflection-issue)
    
* [https://d2.naver.com/helloworld/1230](https://d2.naver.com/helloworld/1230)
