[자바] 중첩 클래스
클래스 내부에 클래스를 정의한 것
중첩 클래스 종류
- 정적 중첩 클래스 → 바깥 인스턴스와 전혀 다른 인스턴스, 바깥에 소속되어 있지 않음 (static)
- 내부 클래스 → 바깥 인스턴스 소속 (non-static)
- 내부 클래스
- 지역 클래스
- 익명 클래스
사용하는 이유
- 특정 클래스가 다른 하나의 클래스 안에서만 사용되거나, 둘이 아주 긴밀하게 연결되어 있는 특별한 경우에만 사용
- 즉 외부의 여러 클래스가 특정 중첩 클래스를 사용하면 안됨.
- 논리적 그룹화 : 다른 곳에서 사용될 필요가 없는 중첩 클래스를 외부에 노출시키지 않게 하기 위함
- 캡슐화 : 중첩 클래스는 바깥의 private 멤버에 접근이 가능. 따라서 둘을 긴밀하게 연결하고 불필요한 public 메서드 제거 가능
정적 중첩 클래스
- 자신의 멤버에 접근 가능
- 외부 클래스 인스턴스 변수나 메서드에는 접근 불가.
- static 멤버 변수, 메서드는 메서드 영역에 저장되며, 클래스의 모든 인스턴스가 공유. 정적 내부 클래스의 인스턴스는 힙 영역에 생성되므로, 외부 클래스의 정적 멤버에 접근 가능
- new 바깥클래스.정적중첩클래스() 로 접근 가능 → 바깥 인스턴스와 아무 상관 없으므로 외부 클래스의 인스턴스 생성 없이 독립적으로 존재가 가능
- 중첩 클래스는 바깥 클래스의 private 접근 제어자에 접근 가능 → 바깥 클래스 안에 있으므로
- 정적 중첩 클래스의 클래스 정보 역시 메서드 영역에 저장.
- private static class 역시 외부 클래스의 멤버로 외부 클래스 내에서만 접근할 수 있음
public class Outer {
private static int outerStaticValue = 1;
private int outerValue = 2;
private static class NestedClass {
private int nestedValue = 3;
public void nestedCall() {
System.out.println(outerStaticValue);
// System.out.println(outerValue); 접근 불가
System.out.println(nestedValue);
}
}
public void outerCall() {
NestedClass nestedClass = new NestedClass(); //접근 가능
nestedClass.nestedCall();
System.out.println(outerStaticValue);
System.out.println(outerValue);
// System.out.println(nestedValue); 접근 불가
}
}
public class Test {
public static void main(String[] args) {
Outer outer = new Outer();
// outer.NestedClass(); 불가능, 외부 클래스와연관 X
// 정적 중첩클래스가 private 일 때는 외부에서 생성 불가. public 일 때는 아래처럼 생성 가능
// Outer.NestedClass nestedClass = new Outer.NestedClass();
nestedClass.nestedCall();
}
}
nestedCall() 메서드의 스택 프레임에는 로컬 변수나 매개변수가 저장, 정적 변수는 포함되지않음.
메서드 내에서 정적 변수에 접근할 때는 해당 변수가 메서드 영역에 저장된 위치 참조.
중첩 클래스(내부 클래스 포함)는 자신이 소속된 바깥 클래스 안에서 사용하기 위한 용도이다.
그런데 정적 중첩 클래스는 외부에 속해있지 않는데, 굳이 정적 중첩 클래스로 만드는 이유는 무엇일까?
정적 중첩 클래스는 외부와 연관 없는 게 맞지만, 논리적으로 외부 클래스에 종속된 개념일 때 사용한다. 만약 외부에서 생성하고 사용하고 있다면 중첩 클래스를 톱레벨 클래스로 만들어야 한다.
내부 클래스
- 정적 중첩 클래스와는 다르게 외부 클래스의 인스턴스에 소속
- static이 붙지 않음 → 인스턴스의 멤버
- 자신의 멤버에는 접근 가능
- 바깥 클래스의 인스턴스 멤버 접근 가능
- 바깥 클래스의 클래스 멤버 역시 접근 가능
- 내부 클래스 역시 바깥 클래스의 private 접근 제어자에 접근 가능
- 바깥 클래스의 인스턴스를 먼저 생성해야만 내부 클래스의 인스턴스 생성 가능
public class Outer2 {
private static int outerStaticValue = 1;
private int outerValue = 2;
class InnerClass {
private int innerValue = 3;
public void innerCall() {
System.out.println("InnerClass.innerCall");
System.out.println(outerStaticValue);
System.out.println(outerValue);
System.out.println(innerValue);
outerCall();
}
}
public void outerCall() {
System.out.println("Outer2.outerCall");
System.out.println(outerStaticValue);
System.out.println(outerValue);
}
}
public class Test {
public static void main(String[] args) {
Outer2 outer2 = new Outer2();
Outer2.InnerClass innerClass1 = outer2.new InnerClass();
Outer2.InnerClass innerClass2 = new Outer2().new InnerClass();
innerClass1.innerCall();
}
}
정적 중첩 클래스와는 다르게 외부 클래스의 메서드에 접근할 수 있는 것을 확인할 수 있다. 이는 메모리 구조를 확인하면 내부 클래스 인스턴스에는 외부 클래스에 대한 참조를 가지고 있기 때문이다.
(외부 클래스 인스턴스 생성 시 내부 클래스 인스턴스가 자동으로 생성되는 것이 아님. → 따로 존재)
(즉 외부 클래스 인스턴스만 생성하고 내부 클래스 인스턴스를 생성하지 않았으면 외부 인스턴스 참조 공간에 null 값이 저장되는 것이 아니라 내부 클래스 인스턴스가 자체가 생성되지 않음)
실제로 getDeclaredFields() 메서드 (클래스의 선언된 모든 필드 반환 - 접근제어자 상관X)를 통해 확인해보면 외부 클래스의 참조를 확인할 수 있다.
public class Test {
public static void main(String[] args) {
Outer.NestedClass nestedClass = new Outer.NestedClass();
for (Field field : nestedClass.getClass().getDeclaredFields()) {
System.out.println(field);
}
System.out.println("-----------------------");
Outer2 outer2 = new Outer2();
Outer2.InnerClass innerClass1 = outer2.new InnerClass();
Outer2.InnerClass innerClass2 = new Outer2().new InnerClass();
System.out.println(innerClass1.getClass());
for (Field field : innerClass1.getClass().getDeclaredFields()) {
System.out.println(field);
}
}
}
출력 결과
private int nested.hello.Outer$NestedClass.nestedValue
————————————————
class nested.hello.Outer2$InnerClass
private int nested.hello.Outer2$InnerClass.nestedValue
final nested.hello.Outer2 nested.hello.Outer2$InnerClass.this$0
마지막 출력 결과에서 nested.hello.Outer2 는 Outer2 클래스의 인스턴스를 가리키는 것이고 this$0라는 Java에서 자동으로 생성한 필드로, 내부 클래스가 외부 클래스의 인스턴스에 접근할 수 있도록 하는 역할을 한다.
참고 : 만약 위에서 변수들의 이름이 모두 같은 경우 내부 인스턴스 변수에는 this.변수명, 외부 클래스의 인스턴스 변수에는 외부클래스.this.변수명으로 접근 가능하지만 명확성을 위해 처음부터 변수명을 다르게 하도록 함.
지역 클래스
내부 클래스 중 하나로 마치 지역 변수처럼 메서드 내에서 작성하는 클래스를 의미한다.
- 자신의 인스턴스 변수에 접근 가능
- 같은 메서드 내의 지역 변수, 매개 변수에 접근 가능
- 내부 클래스의 한 종류로 마찬가지인 외부 클래스의 인스턴스 멤버에도 접근 가능
- 인터페이스 구현이나 상속 역시 가능
public class Outer3 {
private int outerValue = 1;
public static void main(String[] args) {
Outer3 outer = new Outer3();
Parent parent = outer.outerCall(3);
parent.localCall();
}
public Parent outerCall(int param) {
int value = 2;
class LocalClass implements Parent {
int localValue = 4;
@Override
public void localCall() {
System.out.println("외부 클래스 인스턴스 변수 : " + outerValue);
System.out.println("같은 메서드 내의 지역 변수 : " + value);
System.out.println("같은 메서드 내의 매개 변수 : " + param);
System.out.println("자신의 인스턴스 변수 : " + localValue);
}
}
LocalClass localClass = new LocalClass();
return localClass;
}
}
출력 결과
외부 클래스 인스턴스 변수 : 1
같은 메서드 내의 지역 변수 : 2
같은 메서드 내의 매개 변수 : 3
자신의 인스턴스 변수 : 4
여기서 지역 변수의 스코프는 메서드가 끝나면 사라지는 가장 짧은 생명 주기를 가지고 있다. 근데 위 코드에서 outerCall() 메서드는 Parent 타입을 반환하면서 스택 프레임에서 사라지게 된다. 즉 스택 프레임이 가지고 있는 지역 변수(value), 매개 변수(param)는 스택 프레임에서 사라진 상태인데 출력 결과를 보면 모두 출력이 되는 것을 확인할 수 있다.
지역 변수 캡처
위 상황이 가능한 이유는 자바는 지역 클래스 인스턴스를 생성하는 시점에 필요한 변수들을 인스턴스 내부(힙 영역)에 복사하도록 한다. 이를 지역 변수 캡처라고 한다.
(모든 지역 변수를 가지는 것이 아닌 접근이 필요한 변수만 캡처)
public static void main(String[] args) {
Outer3 outer = new Outer3();
Parent parent = outer.outerCall(3);
//parent.localCall();
for (Field field : parent.getClass().getDeclaredFields()) {
System.out.println(field);
}
}
출력 결과
int nested.hello.Outer3$1LocalClass.localValue
final int nested.hello.Outer3$1LocalClass.val$value
final int nested.hello.Outer3$1LocalClass.val$param
final nested.hello.Outer3 nested.hello.Outer3$1LocalClass.this$0
위 상황에서 클래스의 필드를 출력해보면 인스턴스 내부에 value, param을 가지고 있는 것을 확인할 수 있다.
이 때 결과를 보면 앞에 final이 붙었는데 이는 자바에서 지역 클래스의 지역 변수는 중간에 값이 변하지 않도록 막아놓았기 때문이다.
→ effectively final
값이 변하게 되면 스택 영역의 존재하는 값과 캡처한 인스턴스 변수 값이 서로 달라지는 동기화 문제가 발생할 수 있으며 이는 디버깅을 힘들게 하고 복잡한 상황을 유발할 수 있어 자바에서는 이 값들을 못바꾸도록 final로 하였다.
(실제로 지역 클래스 내부에서 값 변경 시에 컴파일 오류 발생)
public class Outer3 {
private int outerValue = 1; // 값 변경 가능
public static void main(String[] args) {
Outer3 outer = new Outer3();
Parent parent = outer.outerCall(3);
parent.localCall();
for (Field field : parent.getClass().getDeclaredFields()) {
System.out.println(field);
}
}
public Parent outerCall(int param) {
int value = 2;
value = 3; // 값 변경 불가능
param = 2; // 값 변경 불가능
class LocalClass implements Parent {
int localValue = 4; // 값 변경 가능
@Override
public void localCall() {
localValue = 1; // 가능 -> 해당 인스턴스 내부에 값이 있으므로
outerValue = 4; // 가능 -> 외부 클래스를 참조하고 있으므로
System.out.println("외부 클래스 인스턴스 변수 : " + outerValue);
System.out.println("같은 메서드 내의 지역 변수 : " + value); // 컴파일 에러
System.out.println("같은 메서드 내의 매개 변수 : " + param); // 컴파일 에러
System.out.println("자신의 인스턴스 변수 : " + localValue);
}
}
LocalClass localClass = new LocalClass();
return localClass;
}
}
Variable 'value' is accessed from within inner class, needs to be final or effectively final
Variable 'param' is accessed from within inner class, needs to be final or effectively final
익명 클래스
익명 클래스는 지역 클래스인데 이름이 없는 클래스를 말한다.
- new 상속 또는 구현할 클래스명() {본문} - 클래스 이름 생략 + 선언과 생성을 한 번에 처리함.
- 즉 익명 클래스를 사용 시에는 상속받을 상위 클래스나 인터페이스가 필요.
- 이름이 없으므로 기본 생성자만 가질 수 있음
- Outer4$1 처럼 자바 내부에서 바깥 클래스 이름 + $ + 숫자로 구분, 여러 개일 때는 숫자가 2, 3, 4 이렇게 증가
- 지역 클래스를 일회성으로 사용할 때 사용
public class Outer4 {
private int outerValue = 1;
public static void main(String[] args) {
Outer4 outer = new Outer4();
Parent parent = outer.outerCall(3);
parent.localCall();
}
public Parent outerCall(int param) {
int value = 2;
return new Parent() {
int localValue = 4;
@Override
public void localCall() {
System.out.println("외부 클래스 인스턴스 변수 : " + outerValue);
System.out.println("같은 메서드 내의 지역 변수 : " + value);
System.out.println("같은 메서드 내의 매개 변수 : " + param);
System.out.println("자신의 인스턴스 변수 : " + localValue);
}
};
}
}
출력 결과
외부 클래스 인스턴스 변수 : 1
같은 메서드 내의 지역 변수 : 2
같은 메서드 내의 매개 변수 : 3
자신의 인스턴스 변수 : 4
class nested.hello.Outer4$1
익명 클래스를 활용하면 다음과 같이 공통적으로 실행되는 부분을 메서드로 만들고 익명 클래스로 구현한 구현체를 매개 변수로 넘김으로써 서로 다른 메서드를 인자로 전달하는 것 같은 효과를 볼 수 있다. → 다형성
public class AnonymousClass {
public static void main(String[] args) {
Shape circle = new Shape() {
@Override
public void draw() {
System.out.println("원 그리기");
}
};
Shape square = new Shape() {
@Override
public void draw() {
System.out.println("사각형 그리기");
}
};
Shape triangle = new Shape() {
@Override
public void draw() {
System.out.println("삼각형 그리기");
}
};
drawShape(circle);
drawShape(square);
drawShape(triangle);
}
public static void drawShape(Shape shape) {
shape.draw();
}
}
위 코드를 마치 메서드를 넘기는 것처럼 변경 가능
public class AnonymousClass {
public static void main(String[] args) {
drawShape(new Shape() {
@Override
public void draw() {
System.out.println("원 그리기");
}
});
drawShape(new Shape() {
@Override
public void draw() {
System.out.println("사각형 그리기");
}
});
drawShape(new Shape() {
@Override
public void draw() {
System.out.println("삼각형 그리기");
}
});
}
public static void drawShape(Shape shape) {
shape.draw();
}
}
람다
위 처럼 코드 조각을 전달하려면 기본형 타입의 변수나 인스턴스를 변수로 넘겨야만 했다.
자바 8에서는 메서드를 인자로 넘길 수 있는 람다 기능을 지원.
다음과 같이 new로 인스턴스를 생성해서 넘기는 것이 아니라 메서드만 넘긴다.
- 오직 인터페이스로 선언한 익명 클래스만이 람다식으로 표현 가능 → 함수형 인터페이스
- 함수형 인터페이스는 추상 클래스가 하나만 존재해야함
- (parameters) -> { 메서드 본문 } 형식으로 표현. 본문이 하나의 표현식일 경우에는 중괄호 생략 가능
public class AnonymousClass {
public static void main(String[] args) {
drawShape(() -> System.out.println("원 그리기"));
drawShape(() -> System.out.println("사각형 그리기"));
drawShape(() -> System.out.println("삼각형 그리기"));
}
public static void drawShape(Shape shape) {
shape.draw();
}
}
멤버 클래스는 되도록이면 static으로 만들어라
이펙티브 자바에서 아이템24 내용에 따르면 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙인 정적 멤버 클래스로 만들라는 이야기가 있다.
이러한 이유는 무엇일까?
위의 내부 클래스의 설명에서 내부 클래스는 외부 클래스의 참조를 가지고 있다고 하였다.
이 참조를 저장하려면 메모리가 낭비되며 또한 외부 클래스가 필요 없어진 상황에서도 내부 클래스가 외부 클래스를 참조하고 있기에 가비지 컬렉션이 외부 클래스의 인스턴스를 수거할 수 없다는 문제가 발생한다.
→ 메모리 누수 발생
정리
멤버 클래스의 인스턴스 각각이 바깥 인스턴스를 참조한다면 비정적, 그렇지 않다면 정적으로 만들 것.
중첩 클래스가 한 메서드 안에서만 쓰이면서 그 인스턴스를 생성하는 지점이 단 한 곳이고 해당 타입으로 쓰기에 적합한 클래스, 인터페이스가 있다면 익명 클래스, 그렇지 않다면 지역 클래스로 생성할 것