본문으로 건너뛰기

Nested-Class

1 Nested Class

  • 클래스가 여러 클래스와 관계를 맺는 경우 독립적으로 선언하는 것이 좋으나 특정 클래스와 관계를 맺는 경우에는 관계 클래스를 클래스 내부에 선언하는 것이 좋다.
  • 중첩 클래스란(Nested Class)란 클래스 내부에 선언한 클래스를 말한다.
  • 중첩 클래스를 사용하면 두 클래스의 멤버들은 서로 쉽게 접근 할 수 있다는 장점과 외부에는 불필요한 관계 클래스를 감춤으로서 코드의 복잡성을 줄일 수 있다.

1.1 중첩 클래스의 용도

  • 클래스 그룹화
    • 한 곳에서만 사용되는 클래스를 논리적으로 그룹화하는데 사용된다.
    • 하나의 클래스가 오직 다른 하나의 클래스에만 유용한 경우 해당 클래스에 클래스를 내장하고 두 클래스를 함께 유지하는 것이 좋다.
    • 이러한 "helper class"를 중첩하면 패키지가 더 간소화된다.
  • 캡슐화 강화
    • 두 개의 최상위 클래스, A와 B를 고려해보자.
    • 여기서 B는 private으로 선언될 A의 멤버에 대한 액세스가 필요하다.
    • 클래스 A 내에서 클래스 B를 숨김으로써 A의 멤버는 private으로 선언되고 B가 액세스할 수 있다. 게다가, B 자체는 외부로부터 숨겨질 수 있다.
  • 읽기 쉽고 유지보수가 쉬운 코드
    • 최상위 클래스 내에 작은 클래스를 중첩하면 중첩 클래스의 코드가 사용되는 위치에 더 가깝게 배치되고 읽기가 쉬워진다.

1.2 Nested Class의 종류

  • Nested Class는 static의 유무로 static nested class 클래스와 non-static nested class로 나누어진다
  • non-static nested class는 이너 클래스라고 한다
    • 이너 클래스의 특별한 경우로 익명 클래스와 지역 클래스가 있다

Nested Class의 종류

  1. 정적 멤버 클래스(static nested class)
  2. 이너 클래스(inner class, non-static nested class)
    1. 이너 클래스(inner class): 클래스 내부에 선언된 클래스
    2. 지역 클래스(local class): 메서드의 바디 내에서 선언된 이너 클래스
    3. 익명 클래스(anonymous class): 이름이 없는 지역 클래스

2 Inner Class(non-static nested class)

  • static 키워드 없이 선언된 중첩 클래스를 말한다.
  • 이너 클래스는 외부 클래스의 인스턴스와 연결되어 있다
  • 이너 클래스는 인스턴스와 연관되어 있기 때문에 정적 멤버를 선언할 수 없다.
    • 인스턴스 필드와 메서드만 선언 가능함
  • 이너 클래스의 인스턴스 객체는 반드시 외부 클래스의 인스턴스와 함께 존재한다.
    • 이너 클래스의 인스턴스는 외부 클래스의 인스턴스의 메소드와 인스턴스 필드에 집적 접근이 가능하다
    • 이너 클래스는 private로 선언된 경우에도 바깥쪽 클래스의 다른 멤버에 액세스할 수 있다.

이너 클래스 객체 생성하기

  • 이너 클래스의 인스턴스를 만들기 위해선 먼저 외부 클래스의 인스턴스를 생성해야한다.
OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();

Inner Classes 셍성

  • InnerClass는 바깥 클래스의 인스턴스와 연결되어 있다.
  • 따라서 InnerClass에는 static 키워드 사용 불가
public class OuterClass {
public class InnerClass {
// 생성자
public InnerClass() { }
// 인스턴스 멤버
public int instanceField;
public int instanceMethod() { return 10; }

// static 멤버 선언 불가능
// public static int staticField;
// public static void staticMethod() {}
}
}

사용 예시

  • 바깥 클래스의 인스턴스를 만들어야만 Inner Class의 인스턴스를 만들 수 있다.
public class InnerClassTest {

@Test
void testInnerClass() {
//given
OuterClass outerClass = new OuterClass();
OuterClass.InnerClass innerClass = outerClass.new InnerClass();

//when
innerClass.instanceField = 10;
int result = innerClass.instanceMethod();

// then
assertThat(innerClass.instanceField).isEqualTo(10);
assertThat(result).isEqualTo(10);
}
}

3 Local Class

  • 메소드 내부에 선언되는 중첩 클래스를 로컬 클래스라고 한다.
  • 메소드 실행 시에만 사용되고, 메소드가 종료되면 없어진다.
  • 로컬 클래스는 접근 제한자 및 static을 붙일 수 없다.
    • 메소드 내부에서만 사용하므로 접근을 제한할 필요가 없다.
class OuterClass {
void method(){
class LocalClass{
LocalClass() {}
int instanceField;
void instanceMethod() {}

// static int staticField; 정적 필드 선언 불가능
// static void staticMethod(){} 정적 메소드 선언 불가능
}

// method 내에서 로컬 클래스를 사용한다.
LocalClass localClass = new LocalClass();
localClass.instanceField = 3;
localClass.instanceMethod();
}
}

3.1 외부 클래스 멤버 접근

  • 로컬 클래스는 외부 클래스 멤버에 접근이 가능하다
  • 로컬 클래스는 로컬 변수와 메소드의 파라미터에 접근도 가능하다
    • 로컬 클래스의 객체는 메소드 실행이 끝나도 힙 메모리에 존재해서 계속 사용할 수 있다. 그러나 로컬 변수와 매개 변수는 메소드 실행이 끝나면 스택 메모리에서 사라져 로컬 객체에서 사용할 경우 문제가 된다.
    • 이 문제를 해결하기 위해 컴파일 시 로컬 클래스에서 사용하는 매개 변수나 로컬 변수의 값을 로컬 클래스 내부에 복사해 두고 사용한다.
    • 매개 변수나 로컬 변수가 수정되어 값이 변경되면 로컬 클래스에 복사해둔 값과 달라지는 문제를 해결하기 위해 매개 변수나 로컬 변수를 final로 선언해서 수정을 막는다.
    • 즉 final로 선언된 로컬 변수와 매개변수를 사용할 수 있다.
  • 자바 8이후 final로 선언하지 않아도 로컬 변수가 effectively final이라면 접근 가능하다.
    • effectively final: 변수 혹은 파라미터가 초기화 이후 변하지 않은 경우
    • final 키워드가 있다면 로컬 클래스의 메소드 내부에 지역 변수로 복사
    • final 키워드가 없다면 로컬 클래스의 필드로 복사된다.

로컬 클래스 예시

class OuterClass {
// 바깥 클래스의 인스턴스 멤버과 static 멤버 선언
int outerInstanceField = 10;
int outerInstanceMethod() { return 10; }
static int outerStaticField = 10;
static int outerStaticMethod() { return 10; }

int method(int parameter) {
int localVariable = 10;

// 로컬 클래스 선언
class LocalClass {
int localInstanceMethod() {
int total = 0;

// 바깥 클래스의 모든 멤버(인스턴스, static 멤버) 접근 가능
total += outerInstanceField;
total += outerStaticField;
total += outerInstanceMethod();
total += outerStaticMethod();

// 메소드의 로컬 변수와 파라미터 접근 가능
total += localVariable;
total += parameter;

// 메소드의 로컬 변수와 파라미터 변경 불가능 값을 변경하면 effectively final하지 않아 컴파일 에러
// localVariable = 20;
// parameter = 20;

return total;
}
}

LocalClass localClass = new LocalClass();
return localClass.localInstanceMethod();
}


}

image-20211121164427515

4 Anonymous Class

  • 이름이 없는 Local Class
  • Local Class를 한번만 사용한다면 Anonymous Class를 사용하자
  • Anonymous-Class.md 참고

5 Static Nested Class

  • static 키워드로 선언된 중첩 클래스를 말한다.
  • static nested class는 바깥쪽 클래스와 연결되어있다.
    • A 클래스로 바로 접근할 수 있는 B 중첩 클래스
  • 클래스 메소드와 마찬가지로 바깥쪽 클래스에 정의된 인스턴스 변수 또는 메서드를 직접 참조할 수 없다
    • 객체 참조를 통해서만 사용할 수 있다.
  • 이너 클래스와 달리 모든 종류의 필드와 메서드를 선언할 수 있다
class OuterClass {
public static class StaticClass {
// 생성자, 인스턴스 멤버, static 멤버 모두 선언 가능
public StaticClass() {}
public int instanceField;
public int instanceMethod() { return 10; }
public static int staticField;
public static int staticMethod() { return 10; }
}
}

테스트

class StaticNestedClassTest {

@Test
void testInstanceMember() {
// given
OuterClass.StaticClass staticClass = new OuterClass.StaticClass();

// when
staticClass.instanceField = 10;
int result = staticClass.instanceMethod();

// then
assertThat(staticClass.instanceField).isEqualTo(10);
assertThat(result).isEqualTo(10);
}

@Test
void testStaticMemberByInstance() {
// given
OuterClass.StaticClass staticClass = new OuterClass.StaticClass();

// when
// 객체 참조를 통해 static 멤버 접근 가능 그러나 클래스 이름으로 직접 접근하는 것이 좋다
staticClass.staticField = 10;
int result = staticClass.staticMethod();

// then
assertThat(staticClass.staticField).isEqualTo(10);
assertThat(result).isEqualTo(10);
}

@Test
void testStaticMemberByClass() {
// when
OuterClass.StaticClass.staticField = 10;
int result = OuterClass.StaticClass.staticMethod();

// then
assertThat(OuterClass.StaticClass.staticField).isEqualTo(10);
assertThat(result).isEqualTo(10);
}

}

6 중첩 클래스 접근 제한

6.1 바깥 클래스에서 내부 클래스 접근 제한

  • 바깥 클래스에서 내부 클래스에 대한 접근 제한을 알아보자
  • 바깥 클래스 -> 이너 클래스
    • 바깥 클래스에서 인스턴스 필드나 인스턴스 메소드에서 이너 클래스 객체 생성 가능
    • 바깥 클래스에서 static 필드나 static 메소드에서 이너 클래스 객체 생성 불가능
  • 바깥 클래스 -> static 클래스
    • 외부 클래스에서 인스턴스 필드나 인스턴스 메소드에서 static 클래스 객체 생성 가능
    • 외부 클래스에서 정적 필드나 정적 메소드에서 static 클래스 객체 생성 가능
public class A {
// 인스턴스 필드
B f1 = new B(); // 이너 클래스 클래스 객체 생성 가능
C f2 = new C(); // static 클래스 객체 생성 가능

// 인스턴스 메소드
void method1(){
B var1 = new B(); // 이너 클래스 클래스 객체 생성 가능
C var2 = new C(); // static 클래스 객체 생성 가능
}

// static 필드
static B f3 = new B(); // 이너 클래스 클래스 객체 생성 불가능
static C f4 = new C(); // static 클래스 객체 생성 가능

// static 메소드
static void method2(){
B var1 = new B(); // 이너 클래스 클래스 객체 생성 불가능
C var2 = new C(); // static 클래스 객체 생성 가능
}

class B {}

static class C {}
}

6.2 내부 클래스에서 바깥 클래스 접근 제한

  • 내부 클래스에서 바깥 클래스의 필드와 메소드에 대한 접근 제한을 알아보자

Inner class -> 바깥 클래스

  • 모든 필드와 메소드 접근가능
class OuterClass2 {
private int outerInstanceField = 10;
private static int outerStaticField = 10;
public int outerInstanceMethod() { return 10; }
public static int outerStaticMethod() { return 10; }

class InnerClass {
int getTotal() {
int total = 0;
// 바깥 클래스의 모든 멤버 접근 가능
total += outerInstanceField;
total += outerStaticField;
total += outerInstanceMethod();
total += outerStaticMethod();
return total;
}
}
}
@Test
void testInnerClass() {
// given
OuterClass2 outerClass2 = new OuterClass2();
OuterClass2.InnerClass innerClass = outerClass2.new InnerClass();

// when
int result = innerClass.getTotal();

// then
assertThat(result).isEqualTo(40);
}

static class -> 바깥 클래스

  • 바깥 클래스의 static 필드와 static 메소드 접근 가능
  • 바깥 클래스의 인스턴스 필드와 인스턴스 메소드 접근 불가능
class OuterClass2 {
private int outerInstanceField = 10;
private static int outerStaticField = 10;
public int outerInstanceMethod() { return 10; }
public static int outerStaticMethod() { return 10; }

static class StaticClass {
int getTotal() {
int total = 0;
total += outerStaticField;
total += outerStaticMethod();
// 바깥 클래스의 인스턴스 멤버 접근 불가능
// total += outerInstanceField;
// total += outerInstanceMethod();
return total;
}
}
}
@Test
void testStaticClass() {
// when
int result = OuterClass2.StaticClass.getTotal();

// then
assertThat(result).isEqualTo(20);
}

참조