네이버 카페에 다음과 같은 질문이 올라왔다.
코드는 아래와 같다. 아래 처럼 밖의 클래스(Outer)안의 메서드에 정의된 내부 클래스를 지역 내부 클래스(Local inner class)라고 부른다.
public class Outer {
private int a = 1;
private static int b = 2;
public void method1() {
int c = 3;
class Inner {
public void method2() {
System.out.println("sum: " + (a + b + c));
}
}
Inner i = new Inner();
i.method2();
}
public static void main(String[] args) {
Outer O = new Outer();
O.method1();
}
}
우선 고백을 하자면, 이너클래스는 가상 머신의 명세를 완성할 무렵 역사적인 실수로 인해 자바 언어 스펙에 "내부 클래스"라는 항목이 추가되었다고 한다. 이 이야기의 출처는 자바 챔피언 Cay S. Horstmann의 말이다.
어떻게 하면 Inner class의 메소드에서 각각 a,b,c에 접근 가능한 이유를 원리적으로 설명할 수 있을까?
바이트 코드 관점
우선 위의 내부 클래스 예를 컴파일하면 하나가 아닌 두 개의 클래스가 생겨난다.
Outer 클래스와 Inner 클래스에 대한 것 두 개이다.
다만 Inner 클래스는 Outer에 종속되기에 Inner.class 가 아닌 Outer$1Inner.class 라는 이름으로 생긴다.
JDK를 설치하면 java, javac 외에도 javap 라는 명령어 도구가 생겨나는데, decompile을 할 수 있다.
-private 혹은 -p 옵션을 주면 모든 클래스와 멤버들을 볼 수 있다.
$ javap -p com/example/springboot/sandbox/naver/goharrm/Outer\$1Inner
위의 뼈대를 일반 Java 코드로 옮겨보면 다음과 같다.
public class Inner {
final int c;
final Outer outer;
public Inner() {
}
public void method2() {
}
}
하지만 위의 코드는 컴파일이 되지 않을 것이다. 왜냐하면 final로 선언된 멤버의 경우 생성자에서 초기화가 되어야 하기 때문이다.
javap 명령에서 -c 옵션을 주면 디스어셈블된 바이트 코드를 볼 수 있다.
javap -c com/example/springboot/sandbox/naver/goharrm/Outer\$1Inner
위쪽의 com.example.springboot.sandbox.naver.goharrm.Outer$1Inner(); 라는 라벨 아래의 코드가 바로 생성자 부분이다.
따라서 생성자의 코드는 아래와 같다고 볼 수 있다.
public class Inner {
final int c;
final Outer outer;
public Inner(Outer outer, int c) {
this.outer = outer;
this.c = c;
}
public void method2() {
}
}
결국 Inner 클래스의 경우는 아래와 같은 형태라고 볼 수 있다.
public class Inner {
final int c;
final Outer outer;
public Inner(Outer outer, int c) {
this.outer = outer;
this.c = c;
}
public void method2() {
System.out.println("sum: " + (outer.a + Outer.b + c));
}
}
Outer와 Inner 클래스를 동시에 나타낸다면 아래와 같은 코드가 될 것이다.
public class Outer {
private int a = 1;
private static int b = 2;
public void method1() {
int c = 3;
Inner i = new Inner(this, c);
i.method2();
}
class Inner {
final int c;
final Outer outer;
public Inner(Outer outer, int c) {
this.outer = outer;
this.c = c;
}
public void method2() {
System.out.println("sum: " + (outer.a + Outer.b + c));
}
}
public static void main(String[] args) {
Outer O = new Outer();
O.method1();
}
}
결국 Outer 클래스 내부의 인스턴스 변수 a는 외부 클래스를 지칭하는 this$0 에 의해(위의 코드에서는 이해하기 쉽도록 Outer로 되어 있다.) 참조가 가능하고, Outer 클래스의 정적 멤버인 b는 Outer.b 로 바로 참조가 가능하다. 메서드 변수 c는 생성자에 의해 내부 final 변수 val$c에 저장되어 참조가 되는 것이다.
실제 업무에서 이렇게 복잡한 코드를 만들어 본 적이 없다. 하지만 궁금증을 가지고 질문하는 것은 좋은 태도라고 생각하여 기록해둔다.
'Programing > JVM(Java, Kotlin)' 카테고리의 다른 글
[Java] ArithmeticException 는 누가 던지는 것인가? (0) | 2019.05.09 |
---|---|
[Java] Inner class 에도 main 함수(진입점)이 가능하나? (0) | 2019.05.03 |
[Java] 통화 표준 및 통화코드 그리고 Currency 클래스 (0) | 2019.05.03 |
[Java] Class 생성 실험 (0) | 2019.04.01 |
[Java] switch와 String 그리고 바이트코드 (0) | 2019.03.22 |