
내부 클래스를 공부하던중 관련 내용이 살짝 헷갈릴 수 있어 여기에 정리하였다. 해당 그림과 예전 글을 참고하면 본문 내용을 확실히 이해할 수 있다. 기본적으로 static 내부 클래스와 인스턴스 내부 클래스는 결국 각각 static 멤버와 인스턴스 멤버라는 걸 이해하면 된다.
✍️ 작성
public class InnerEx2 {
class InstanceInner1 {}
static class StaticInner1 {}
// 인스턴스 멤버간에는 서로 직접 접근이 가능하다.
InstanceInner1 iv = new InstanceInner1();
// static 멤버 간에는 서로 직접 접근이 가능하다.
static StaticInner1 cv = new StaticInner1();
static void staticMethod() { // static 멤버는 인스턴스 멤버에 직접 접근할 수 없다.
// InstanceInner1 obj1 = new InstanceInner1();
StaticInner1 obj2 = new StaticInner1();
// 굳이 접근할려면 아래와 같이 객체를 생성해야 한다.
// 인스턴스 클래스는 외부 클래스를 먼저 생성해야만 생성할 수 있다.
InnerEx2 outer = new InnerEx2();
InstanceInner1 obj1 = outer.new InstanceInner1();
}
void instanceMethod() {
// 인스턴스 메서드에서는 인스턴스 멤버와 static 멤버 모두 접근 가능하다.
InstanceInner1 obj1 = new InstanceInner1();
StaticInner1 obj2 = new StaticInner1();
// 메서드 내에 지역적으로 선언된 내부 클래스는 외부에서 접근할 수 없다.
// LocalInner Iv = new LocalInner();
}
void myMethod() {
class LocalInner1 {}
LocalInner1 lv = new LocalInner();
}
public static void main(String[] args) {
// TODO Auto-generated method stub
}
}
본 코드 내용과 위 그림에서 알 수 있듯이 인스턴스 내부 클래스는 결국 인스턴스 멤버이다. 특정 클래스의 어떠한 인스턴스 내부 클래스를 생성하려면 우선 그 특정 클래스의 인스턴스를 생성해야 한다. 이 점은 기존 인스턴스 멤버를 다룰 때와 동일하다. 따라서 static 멤버는 인스턴스 내부 클래스에 직접 접근할 수 없다.
동일한 원리로 인스턴스 메서드에서는 인스턴스 내부 클래스와 static 내부 클래스 모두 접근 가능하다. 이는 static 내부 클래스 역시 static 멤버이기에 특정 인스턴스가 아닌 해당 외부 클래스 자체에 종속되기 때문이다.
마지막으로 지역 내부 클래스는 특정 메서드 내에서 선언되고 정의되며 그러므로 그 클래스의 유효 범위는 해당 메서드 내부로 한정된다. 또한 지역 변수와 마찬가지로, 지역 내부 클래스도 메서드가 실행되는 동안에만 유효하기 때문에 메서드가 종료되면 지역 클래스와 그 인스턴스는 메모리에서 사라진다. 따라서 메서드 외부에서는 이 클래스를 참조할 수 없고, 외부에서 접근할 방법도 없다. 즉 메서드 내에 지역적으로 선언된 내부 클래스는 외부에서 접근할 수 없다.
✍️ 작성
public class InnerEx3 {
private int outerIv = 0;
static int outerCv = 0;
class InstanceInner3 {
int iiv = outerIv; // 외부 클래스의 private 멤버도 접근 가능하다.
int iiv2 = outerCv;
}
static class StaticInner3 {
// static 클래스는 외부 클래스의 인스턴스 멤버에 접근할 수 없다.
static int scv = outerCv;
}
void myMethod() {
int lv = 0;
final int LV = 0; // JDK 1.8부터 final 생략 가능
class LocalInner3 {
int liv = outerIv;
int liv2 = outerCv;
// 외부 클래스의 지역 변수는 final이 붙은 변수 (상수)만 접근가능하다.
// int liv4 = lv; // 에러!!! (JDK 1.8부터 아님)
int liv4 = LV; // OK
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
}
}
이 코드에서 유심히 봐야할 부분은 지역 클래스 내부에서 메서드에 정의된 지역 변수에 접근할 때 오직 final이 붙은 지역변수만 접근이 가능하다는 점이다. 그 이유는 다음과 같다.
메서드가 종료된 후에도 지역 내부 클래스의 객체는 존재 할 수 있음
지역 클래스의 인스턴스는 힙(Heap)영역에서 생성되지만 지역 변수는 스택(Stack) 영역에서 생성된다. 그렇기 때문에 지역 내부 클래스의 객체가 메서드 밖으로 전달되거나 스레드에서 사용되면, 그 객체는 메서드가 끝난 이후에도 여전히 살아 있고 동작할 수 있다.
반면, 메서드 내의 지역 변수는 메서드가 끝나면 소멸된다. 즉 지역 변수는 메서드가 종료될 때 스택에서 제거된다. 이 때문에 지역 내부 클래스가 메서드 내의 변수를 자유롭게 수정할 수 있으면 변수가 존재하지 않는 상태에서 값을 참조하려는 문제가 생길 수도 있는 것이다. 이를 방지하기 위해 지역 변수를 final로 제한하여, 그 값이 변경되지 않도록 만드는 것이다.
변수의 일관성 유지
메서드가 종료된 후에도 지역 내부 클래스가 그 변수를 참조할 수 있다면, 그 변수는 어디서나 일관된 값을 유지해야 한다. 만약 지역 내부 클래스가 메서드의 지역 변수를 자유롭게 수정할 수 있다면, 메서드가 종료된 이후에도 그 변수가 바뀔 수 있어서, 프로그램의 동작이 예측하기 어렵고 복잡진다.
따라서, 자바는 지역 내부 클래스에서 참조할 수 있는 지역 변수는 final 또는 effectively final 로 한정해서 변수 값이 일관되게 유지되도록 하고 있다. 이렇게 하면 지역 내부 클래스에서 사용되는 변수는 항상 일정한 값을 유지할 수 있고 메모리 관리나 값 참조에 있어서 안정성을 보장할 수 있다.
※ effectively final이란 자바 8부터 도입된 개념으로 명시적으로 final 키워드로 선언되지 않았지만 그 값이 한 번 초기화된 후 변경되지 않는 변수를 의미한다.
자바의 정석 3판 (남궁성)