포스트

중첩 클래스, 내부 클래스

중첩 클래스, 내부 클래스

중첩 클래스

사용하는 이유

  • 논리적 그룹화: 특정 클래스가 다른 하나의 클래스 안에서만 사용되는 경우 해당 클래스 안에 포함하는 것이 논리적으로 더 그룹화 됨
  • 캡슐화: 중첩 클래스는 바깥 클래스의 private 멤버에 접근할 수 있다. 이렇게 해서 둘을 긴밀하게 연결하고 불필요한 public 메서드를 제거

당연히 일반 클래스 처럼 인터페이스를 구현하거나 부모 클래스를 상속 할 수 있음

정적 중첩 클래스

  • static이 붙음
  • 바깥 클래스의 인스턴스에 소속되지 않음

바깥 클래스의 인스턴스 접근

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class NestedOuter {

  private static int outClassValue = 3;
  private int outInstanceValue = 2;

  static class Nested {
    private int nestedInstanceValue = 1;

    public void print() {
      // 자신의 멤버에 접근
      System.out.println(nestedInstanceValue);

      // 바깥 클래스의 인스턴스 멤버에 접근에는 접근할 수 없다.
      //System.out.println(outInstanceValue);

      // 바깥 클래스의 클래스 멤버에는 접근할 수 있다. private도 접근 가능
      System.out.println(NestedOuter.outClassValue);
    }
  }
}

사용 방법

1
2
3
4
5
6
7
public class NestedOuterMain {

  public static void main(String[] args) {
    NestedOuter outer = new NestedOuter();
    NestedOuter.Nested nested = new NestedOuter.Nested();
  }
}

내부 클래스

  • static이 붙지 않음
  • 바깥 클래스의 인스턴스에 소속

바깥 클래스의 인스턴스 접근

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class InnerOuter {

  private static int outClassValue = 3;
  private int outInstanceValue = 2;

  class Inner {
    private int innerInstanceValue = 1;

    public void print() {
      // 자기 자신에 접근
      System.out.println(innerInstanceValue);

      // 외부 클래스의 인스턴스 멤버에 접근 가능, private도 접근 가능
      System.out.println(outInstanceValue);

      // 외부 클래스의 클래스 멤버에 접근 가능, private도 접근 가능
      System.out.println(outClassValue);
    }
  }
}

사용 방법

1
2
3
4
5
6
7
public class InnerOuterMain {

  public static void main(String[] args) {
    InnerOuter outer = new InnerOuter();
    InnerOuter.Inner inner = outer.new Inner();
  }
}

같은 이름의 바깥 변수 접근

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ShdowingMain {

  public int value = 1;

  class Inner {
    public int value = 2;

    void go() {
      int value = 3;
      System.out.println("value = " + value);
      System.out.println("this.value = " + this.value);
      System.out.println("ShdowingMain.value = " + ShdowingMain.this.value);
    }
  }
}

지역 클래스

  • 지역 클래스는 지역 변수처럼 코드 블럭 안에 클래스를 선언
  • 지역 클래스는 지역 변수에 접근할 수 있음
  • 접근 제한자를 사용할 수 없음

사용 방법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class LocalOuterV1 {

  private int outInstanceVar = 3;

  public void process(int paramVar) {

    int localVar = 1;

    class LocalPrinter {
      int value = 0;

      public void printData() {
        System.out.println("value=" + value);
        System.out.println("localVar=" + localVar);
        System.out.println("paramVar=" + paramVar);
        System.out.println("outInstanceVar=" + outInstanceVar);
      }
    }
  }
}

의문: 메서드 실행이 종료 된 후 지역 클래스로 생성한 객체에서 지역 변수에 접근한다면?

변수의 생명 주기

  • 클래스 변수: 프로그램 종료 까지. (메서드 영역)
  • 인스턴스 변수: 인스턴스의 생존 기간(힙 영역)
    • 본인이 소속된 인스턴스가 GC 되기 전까지 존재
  • 지역 변수: 메서드 호출이 끝나면 사라짐(스택 영역)

지역 변수는 스택 영역에 있기 때문에 메서드 실행이 종료되면 사라지기 때문에 접근이 불가능 해야 합니다.

1
2
3
public interface Printer {
  void print();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class LocalOuterV3 {

  private int outInstanceVar = 3;

  public Printer process(int paramVar) {

    int localVar = 1; //지역 변수는 스택 프레임이 종료되는 순간 함께 제거된다.

    class LocalPrinter implements Printer {
      int value = 0;

      @Override
      public void print() {
        System.out.println("value=" + value);

        //인스턴스는 지역 변수보다 더 오래 살아남는다.
        System.out.println("localVar=" + localVar);
        System.out.println("paramVar=" + paramVar);
        System.out.println("outInstanceVar=" + outInstanceVar);
      }
    }

    LocalPrinter printer = new LocalPrinter();

    //printer.print();를 여기서 실행하지 않고 Printer 인스턴스만 반환한다.
    return printer;
  }

  public static void main(String[] args) {
    LocalOuterV3 localOuter = new LocalOuterV3();
    Printer printer = localOuter.process(2);
    //printer.print()를 나중에 실행한다. process()의 스택 프레임이 사라진 이후에 실행
    printer.print();
  }
}
1
2
3
4
value=0
localVar=1
paramVar=2
outInstanceVar=3

실행 결과 여전히 지역변수 값을 참조할 수 있습니다. 이는 지역 클래스가 객체를 생성할 때, 지역 변수를 캡처하기 때문에 일어나는 현상입니다.

지역 변수 캡처

지역 클래스의 인스턴스 생성 시

  • 복사한 지역 변수를 인스턴스에 포함
  • 복사한 지역 변수를 포함해서 인스턴스 생성 완료

이렇게 생성된 인스턴스에 있는 캡처한 변수에 접근하기 때문에 지역 변수의 생명 주기와 무관하게 캡처 변수에 접근 할 수 있습니다.

지역 변수 값 변경

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class LocalOuterV4 {

  private int outInstanceVar = 3;

  public Printer process(int paramVar) {

    int localVar = 1; //지역 변수는 스택 프레임이 종료되는 순간 함께 제거된다.

    class LocalPrinter implements Printer {

      int value = 0;

      @Override
      public void print() {
        System.out.println("value=" + value);

        //인스턴스는 지역 변수보다 더 오래 살아남는다.
        System.out.println("localVar=" + localVar);
        System.out.println("paramVar=" + paramVar);
        System.out.println("outInstanceVar=" + outInstanceVar);
      }
    }

    LocalPrinter printer = new LocalPrinter();
    // localVar = 10; 컴파일 오류
    // paramVar = 20; 컴파일 오류

    return printer;
  }
}

캡처 변수의 값을 변경하지 못하는 이유

  • 지역 변수의 값과 인스턴스에 있는 캡처 변수의 값을 서로 동기화 해야 하는데, 멀티쓰레드 상황에서 이런 동기화는 매우 어렵고, 성능에 나쁜 영향을 미칠 수 있음
  • 예상하지 못한 곳에서 값이 변경 될수 있고, 이는 디버깅을 어렵게 함

익명 클래스

사용 방법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Ex1RefMainV3 {

  public static void hello(Process process) {
    System.out.println("프로그램 시작");

    //코드 조각 시작
    process.run();
    //코드 조각 종료

    System.out.println("프로그램 종료");
  }

  public static void main(String[] args) {

    Process dice = new Process() {
      @Override
      public void run() {
        int randomValue = new Random().nextInt(6) + 1;
        System.out.println("주시위 = " + randomValue);
      }
    };

    Process sum = new Process() {

      @Override
      public void run() {
        for (int i = 0; i < 3; i++) {
          System.out.println("i = " + i);
        }
      }
    };

    System.out.println("Hello 실행");
    hello(dice);
    hello(sum);
  }
}

람다를 사용한 더 간결한 문법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Ex1RefMainV5 {

  public static void hello(Process process) {
    System.out.println("프로그램 시작");

    //코드 조각 시작
    process.run();
    //코드 조각 종료

    System.out.println("프로그램 종료");
  }

  public static void main(String[] args) {

    System.out.println("Hello 실행");
    hello(() -> {
      int randomValue = new Random().nextInt(6) + 1;
      System.out.println("주시위 = " + randomValue);
    });

    hello(() -> {
      for (int i = 0; i < 3; i++) {
        System.out.println("i = " + i);
      }
    });
  }
}

참고

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.