Post

[JAVA] 중첩 클래스와 내부 클래스

[JAVA] 중첩 클래스와 내부 클래스

중첩 클래스

중첩 클래스는 바깥 클래스의 안에 있지만, 바깥 클래스와 상관 없는 전혀 다른 클래스

1
2
3
4
5
6
7
class Outer {
  ...
  //중첩 클래스
  class Nested {
  ...
  }
}

중첩 클래스의 종류

  • 정적 중첩 클래스 - static 이 붙고, 바깥 클래스의 인스턴스에 소속되지 않음
  • 내부 클래스 - static 이 붙지 않고, 바깥 클래스의 인스턴스에 소속
    • 내부 클래스 - 바깥 클래스의 인스턴스의 멤버에 접근
    • 지역 클래스 - 내부 클래스의 특징 + 지역 변수에 접근
    • 익명 클래스 - 지역 클래스의 특징 + 클래스의 이름이 없는 특별 클래스

중첩(Nested): 내 안에 있지만 내것이 아닌 것

내부(Inner): 나의 내부에 있는 나를 구성하는 요소

중첩 클래스의 선언 위치

  • 정적 중첩 클래스(static) → 정적 변수와 같은 위치
  • 내부 클래스 → 인스턴스 변수와 같은 위치
  • 지역 클래스 → 지역 변수와 같은 위치

정적 중첩 클래스는 바깥 클래스 안에 있지만 바깥 클래스와 관계없는 다른 클래스 내부 클래스는 바깥 클래스의 내부에 있으면서 바깥 클래스를 구성하는 요소

중첩 클래스를 왜 사용하는지?

  • 특정 클래스가 다른 하나의 클래스 안에서만 사용되거나, 둘이 긴밀하게 연결되어 있는 경우에 사용. 외부의 여러 클래스가 특정 중첩 클래스를 사용한다면 중첩 클래스로 만들면 안된다.
  • 논리적으로 그룹화가 가능하다. 다른 곳에 필요없는 중첩 클래스가 외부에 노출되지 않는 장점도 있다.
  • 중첩 클래스는 바깥 클래스의 private 멤버에 접근할 수 있다. 이로 인해 불필요한 public 메서드를 제거 할 수 있다. 즉 캡슐화가 가능하다.

중첩 클래스 예제

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 outInstancValue = 2;

  //정적 중첩 클래스는 앞에 static <- 붙음.
  static class Nested {
    private int nestedInstanceValue = 1;

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

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

      //바깥 클래스의 클래스 멤버에는 접근할 수 있다 private도 접근 가능
      System.out.println(NestedOuter.outClassValue);
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class NestedOuterMain {
    public static void main(String[] args) {
        NestedOuter.Nested nested = new NestedOuter.Nested(); //정적 중첩 클래스 생성
        nested.print();

        System.out.println("nestedClass = " + nested.getClass());
    }
}

//실행결과
1
3
nestedClass = class nasted.nasted.NestedOuter$Nested
  • 정적 중첩 클래스는 new 바깥클래스.중첩클래스() 로 생성가능
  • NestedOuter.Nested 와 같이 바깥 클래스.중첩클래스 로 접근 가능

private 접근 제어자

  • 같은 클래스 안에 있을 때만 private 에 접근할 수 있다.
  • 중첩 클래스도 바깥 클래스와 같은 클래스 안에 있으므로, 바깥 private 접근제어자에 접근할 수 있다.

정적 중첩 클래스는 다른 클래스를 중첩해 둔 것일 뿐. 둘은 아무런 관계가 없다. 그냥 클래스 두개를 만든 것과 같다.

내부 클래스

내부 클래스는 바깥 클래스의 인스턴스를 이루는 요소로 바깥 클래스의 인스턴스에 소속된다.

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;

  //static이 붙지 않음 즉 인스턴스 멤버가 된다.
  class Inner {
    private int innerInstanceValue = 1;

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

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

      // 외부 클래스의 클래스 멤버에는 접근 가능. private 접근 가능
      System.out.println(InnerOuter.outClassValue);
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
InnerOuter outer = new InnerOuter(); //x001
InnerOuter.Inner inner = outer.new Inner(); //x001 의 내부에다 inner를 만든다
inner.print(); //그래서 바깥 인스턴스 접근 가능
System.out.println(inner.getClass());

 //실행결과
1
2
3
class nasted.inner.InnerOuter$Inner
  • 내부 클래스는 바깥 클래스의 인스턴스에 소속되어 바깥 클래스의 인스턴스 정보를 알아야 생성가능
  • 내부 클래스는 바깥클래스의 인스턴스 참조.new 내부클래스() 로 생성 가능
    • 바깥 클래스의 인스턴스 참조가 필요
    • outer.new Inner(); 로 생성한 내부 클래스는 개념상 바깥 클래스의 인스턴스 내부에 생성
    • 따라서 바깥 클래스의 인스턴스를 먼저 생성해야 내부 클래스의 인스턴스 생성 가능

private 접근 제어자

  • 같은 클래스 안에 있을 때만 private 에 접근할 수 있다.
  • 내부/바깥 = 같은 클래스 따라서 내부 클래스는 바깥 클래스의 private 접근 제어자에 접근 가능

정적 중첩 클래스는 다른 클래스를 그냥 중첩해 둔 것일 뿐. 아무런 관계가 없다 반면 내부 클래스는 바깥 클래스의 인스턴스 내부에서 구성요소로 사용된다.

지역 클래스

지역 클래스는 내부 클래스의 종류 중 하나이므로 내부 클래스의 특징을 그대로 따른다.

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
package nasted.local;

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); // 바깥 클래스의 인스턴스 멤버
            }
        }
        LocalPrinter printer = new LocalPrinter();
        printer.printData();
    }

    public static void main(String[] args) {
        LocalOuterV1 localOuter = new LocalOuterV1();
        localOuter.process(2);
    }
}

  • 지역 클래스는 지역변수와 같이 코드블럭 안에 클래스를 선언한다.
  • 지역 클래스는 지역변수에 접근가능
  • 지역 변수와 같이 접근 제어자 사용 불가

지역 변수 캡쳐

자바는 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사해 생성된 인스턴스에 함께 넣어둔다. 이 과정을 변수 캡쳐라 한다.

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class LocalOuterV3 {
    private int outInstanceVar = 3;

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

				//localVar = 2; // <- 이렇게 바꾸면 컴파일 에러!
        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();

        //추가
        System.out.println("ㅡㅡㅡㅡ필드확인 ㅡㅡㅡㅡㅡ");
        Field[] fields = printer.getClass().getDeclaredFields();
        for (Field field : fields) {
            System.out.println("field = " + field);
        }
    }
}

//실행결과
value = 0
localVar = 1
paramVar = 2
outInstanceVar = 3
ㅡㅡㅡㅡ필드확인 ㅡㅡㅡㅡㅡ
//인스턴스 변수
field = int nasted.local.LocalOuterV3$1LocalPrinter.value
//캡처 변수
field = final int nasted.local.LocalOuterV3$1LocalPrinter.val$localVar
field = final int nasted.local.LocalOuterV3$1LocalPrinter.val$paramVar
//바깥 클래스 참조
field = final nasted.local.LocalOuterV3 nasted.local.LocalOuterV3$1LocalPrinter.this$0
  • process() 메서드가 종료된 이후 메인에서 LocalPrinter.print() 를 호출한다 하지만 print() 메서드는 지역 변수는 paramVar localVar 에 접근해야 한다.
  • 하지만 process() 메서드가 종료됨으로 지역변수는 제거됐다. 그러나 값들은 정상출력 된다.
  • 지역 클래스가 접근하는 지역 변수는 절대 값이 변해선 안된다.
  • 스택 영역에 존재하는 지역 변수의 값과 인스턴스에 캡처한 캡처 변수의 값이 서로 달라지는 동기화 문제가 발생할 수 있기 때문이다. 이로 인해 많은 문제가 발생할 수 있다

final 키워드를 넣지 않았을 뿐 사실상 final 키워드를 넣은 값을 변경하지 않는 지역변수

익명 클래스

익명 클래스는 지역 클래스 종류 중 하나. 익명 클래스는 지역 클래스인데, 클래스의 이름이 없는 특징이 있다.

지역 클래스의 선언과 생성을 한번에 가능

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
public class AnonymusOuter {
    private int outInstanceVar = 3;

    public void process(int paramVar) {
        int localVar = 1;   //지역 변수

        //익명 클래스는 클래스의 본문의 정의하며 생성
        //printer를 구현 하며 바로 생성
        Printer printer = new 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); // 바깥 클래스의 인스턴스 멤버
            }
        };
        printer.print();
        System.out.println("printer.class" + printer.getClass());
    }

    public static void main(String[] args) {
        AnonymusOuter main = new AnonymusOuter();
        main.process(2);
    }
}
  • 익명 클래스는 클래스의 바디를 정의하면서 동시에 생성
  • new 다음에 구현하거나 상속할 인터페이스나 추상 클래스를 바로 작성
  • Printer 를 생성하는 것 같지만 Printer 라는 이름의 인터페이스를 구현한 익명 클래스를 생성
  • 즉, 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
public class Ex1RefMain5 {
    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 = 1; i <= 3; i++) {
                System.out.println("i = " + i);
            }
        });
    }
}
  • 메서드(함수)를 인수로 전달할 수 있게 해주는 람다 사용
  • 람다식은 익명 클래스를 더 간결하게 표현한 문법
  • 내부적으로는 익명 클래스처럼 동작함


출처: 김영한의 실전 자바 - 중급 1편

This post is licensed under CC BY 4.0 by the author.