좋은 질문이에요!
"지역 변수의 final"과 "effectively final"은 겉보기는 비슷하지만 엄밀히 따지면 개념적으로 완전히 다르다는 말, 핵심을 짚어서 아주 차근차근 설명해드릴게요.
final 키워드final이 붙으면 변수에 값을 한 번만 대입할 수 있고, 그 이후에는 재할당(=값 변경)이 불가능해집니다.final 키워드를 붙입니다.void example() {
final int x = 10; // final로 선언된 지역 변수
// x = 20; // 오류! final 변수이므로 재할당 불가
}
final 키워드가 붙으면, 컴파일러가 해당 변수에 재할당하는 모든 시도를 바로 에러로 잡아냅니다.final 키워드를 명시적으로 붙이지 않았지만, '사실상' 한 번 초기화 후 재할당되지 않아 변경 불가 상태가 되는 경우를 말합니다.final 키워드는 없지만 컴파일러가 보기에 재할당이 없으므로 사실상 final과 동일한 취급을 받는 상태입니다.final 선언 없이도, 변수 값이 바뀌지 않는 한 final 변수처럼 안전하게 사용할 수 있도록 도입된 개념입니다.void example() {
int y = 10; // final 키워드 없음
// y = 20; // 만약 이렇게 재할당하면 effectively final이 아님
Runnable r = () -> {
System.out.println(y); // y는 effectively final이기 때문에 람다에서 사용 가능
};
}
y가 이후에 재할당되면 컴파일 에러 발생합니다.| 구분 | 지역 변수의 final | 지역 변수의 effectively final |
|---|---|---|
| 키워드 | final 명시적 사용 | 키워드 없음 (컴파일러가 분석해 판단) |
| 컴파일 타임 강제 여부 | 재할당 시 컴파일 에러 | 재할당이 있으면 컴파일 에러, 없으면 허용 |
| 코드 작성자 의도 명시 여부 | 명시적으로 "변경 금지" 의도 표현 | 의도는 없지만 사실상 값 변경이 없는 경우 |
| 주로 쓰이는 맥락 | 어디서나 쓰임 (일반 지역 변수) | 람다, 익명 클래스에서 지역 변수 사용 시 |
| 코드 유지보수성 | 코드 읽을 때 '이 변수 변경 없다' 확실히 알 수 있음 | 키워드 없어서 바로 보이진 않지만 변경 없음 |
final 붙여야 했어요.final 선언 강제는 불편해서, 컴파일러가 대신 분석해 "실제로 값이 안 바뀌면 final 처럼 쓰도록 허용"하는 effectively final 개념을 도입했습니다.final은 '명시적 선언', effectively final은 '컴파일러가 추론한 상태'**라고 이해하면 쉬워요.final 변수 = "여기 붙어 있으니 절대 움직이지 마라!"라고 큰 글씨로 경고문 붙여놓은 상태void test() {
final int a = 5; // 명시적 final
int b = 10; // effectively final (초기값 이후 변경 없음)
Runnable r = () -> {
System.out.println(a); // OK
System.out.println(b); // OK (effectively final)
};
// a = 6; // 오류! final 변수 재할당 불가
// b = 15; // 오류! effectively final 아니게 됨 -> 람다에서 사용 불가
}
final 키워드 붙인 변수는 반드시 재할당 금지, 코드에서 명확히 알 수 있음effectively final은 키워드 없지만, 컴파일러가 "재할당 없는 변수"로 판단하여 final처럼 취급함final이거나 effectively final이어야만 한다는 점에서 중요하게 구분됩니다.좋아요! 문제 풀이를 아주 상세하게, 한 줄 한 줄 설명하듯 해볼게요.
public class FinalTest {
public void testMethod() {
final int x = 10;
int y = 20;
Runnable r1 = () -> {
System.out.println("x = " + x);
System.out.println("y = " + y);
};
// y = 30; // (A) 이 줄의 주석을 해제하면 어떻게 될까요?
System.out.println("Before run");
r1.run();
System.out.println("After run");
}
public static void main(String[] args) {
new FinalTest().testMethod();
}
}
(A) 줄의 주석이 해제되지 않은 상태에서는 컴파일 오류가 발생할까요? 발생한다면 이유는 무엇인가요?
x 변수는 final int x = 10;으로 명시적으로 final 선언되어 있습니다.
→ 그래서 x는 절대 재할당이 불가능한 변수입니다.
→ 람다 안에서 사용할 때 문제없음.
y 변수는 int y = 20;으로 final 키워드 없이 선언됐지만, 이후에 값이 변경되지 않음 → effectively final 상태입니다.
→ 즉, 값이 최초 할당(20) 이후에 변경되지 않아, 컴파일러가 final처럼 인식합니다.
람다 표현식에서 지역 변수 참조 시, 참조하는 변수는 반드시 final이거나 effectively final이어야 함 (Java 문법 규칙).
(A) 주석이 해제되어 있지 않으면 y는 변경되지 않고 그대로 20을 유지하기 때문에,
람다 내에서 y를 사용하는 데 컴파일 오류가 발생하지 않습니다.
(A)주석 해제 안 한 상태에서는x는 final,y는 effectively final이므로 둘 다 람다에서 문제 없이 참조 가능하다.
→ 따라서 컴파일 오류는 발생하지 않는다.
(A) 줄의 주석을 해제한 상태에서는 컴파일 오류가 발생할까요? 발생한다면 이유는 무엇인가요?
(A) 줄을 해제해서 y = 30;로 하면, y가 값이 변경됩니다.
이 경우, y는 더 이상 effectively final이 아닙니다.
람다 내 지역 변수 참조 규칙은,
람다 내에서 사용하는 지역 변수는 final이거나 effectively final이어야 한다는 점입니다.
하지만 y는 final 키워드가 없고, 값이 변경되었으므로 effectively final 조건도 만족하지 않습니다.
따라서 컴파일러는 람다 내 y 참조 부분에서 컴파일 오류를 발생시킵니다.
구체적으로,
y가 변경될 가능성이 있기 때문에,
(A)주석 해제하면y가 재할당되어 effectively final 조건을 깨트리므로, 람다에서y를 참조하는 부분에서 컴파일 오류 발생한다.
x 변수는 final로 선언되어 있고, y 변수는 final 키워드가 없는데도 람다 내에서 정상적으로 사용됩니다.
왜 y도 람다에서 사용 가능할까요?
x는 명시적 final 변수이므로 당연히 람다에서 사용할 수 있습니다.
y는 final 키워드가 없지만, 선언 이후 재할당 없이 값이 변경되지 않았기 때문에
→ 컴파일러가 이를 effectively final로 판단합니다.
Java 8부터, 람다 내부에서 사용하는 지역 변수는 반드시 final이거나 effectively final이어야만 한다고 규칙이 바뀌었고,
그래서 final 키워드를 꼭 붙이지 않아도 변수가 변경되지 않으면 람다 내에서 사용할 수 있도록 허용되었습니다.
effectively final은 컴파일러가 분석해 결정하는 특성이기 때문에, 코드에는 키워드가 없지만 final과 동일하게 취급되는 것입니다.
y는final키워드는 없지만, 재할당하지 않아 effectively final로 인정되어 람다에서 사용 가능하다.
final 또는 effectively final이어야 한다는 제한을 둔 것입니다.| 상황 | 컴파일 오류 발생 여부 | 이유 |
|---|---|---|
(A) 주석 해제 안 함 | 오류 없음 | y가 effectively final 상태임 |
(A) 주석 해제 함 | 오류 발생 | y가 재할당되어 effectively final 아님 |
x 사용 | 항상 오류 없음 | 명시적 final 변수이기 때문 |