Posts :book: 자바의 정석 #7.8: 내부클래스
Post
Cancel

:book: 자바의 정석 #7.8: 내부클래스

내부 클래스

내부클래스는 GUI 어플리케이션(AWT, Swings 등)의 이벤트처리 외에는 잘 사용하지 않을 정도로 사용빈도가 높지 않다.

내부 클래스란?

두 클래스가 서로 긴밀한 관계에 있을 때 내부 클래스로 선언한다. 내부 클래스는 외부 클래스를 제외하고는 다른 클래스에서 잘 사용되지 않는 것이어야 한다.

내부 클래스의 장점

  • 내부 클래스에서 외부 클래스의 멤버들을 쉽게 접근할 수 있다.
  • 코드의 복잡성을 줄일 수 있다.

내부 클래스의 종류와 특징

변수의 선언위치에 따라 변수의 종류와 특징이 달리지는 것처럼. 내부 클래스도 마찬가지이다. 내부 클래스의 유효범위와 성질이 변수와 유사하므로 서로 비교하면서 이해하자

내부 클래스특징
instance class외부 클래스의 멤버변수 선언위치에 선언하며, 외부 클래스의 인스턴스 멤버처럼 다루어진다. 주로 외부 클래스의 인스턴스 멤버들과 관련된 작업에 사용될 목적으로 선언된다.
static class외부 클래스의 멤버변수 선언위치에 선언하며, 외부 클래스의 static 멤버처럼 다루어진다. 주로 외부 클래스의 static 멤버, 특히 static 메서드에서 사용될 목적으로 선언된다.
local class외부 클래스의 메서드나 초기화블럭 안에 선언하며, 선언된 영역 내부에서만 사용될 수 있다.
anonymous class클래스의 선언과 객체의 생성을 동시에 하는 이름없는 클래스(일회용)

내부 클래스의 선언

변수가 선언된 위치에 따라 인스턴스 필드, 스태틱 필드, 지역변수로 나뉘듯, 내부 클래스도 선언된 위치에 따라 나뉜다. 그리고 각 내부 클래스의 선언위치에 따라 같은 선언위치의 변수와 동일한 유효범위(scope)와 접근성 (accessability)를 갖는다.

내부 클래스의 제어자와 접근성

내부 클래스 중에서 스태틱 클래스만 static 멤버를 가질 수 있다.

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
class InnerEx1 {
  class InstanceInnter {
    int iv = 100;
    //static int cv = 100; => 에러
    final static int CONST = 100; // => 허용
  }
  
  class StaticInner {
    int iv = 200;
    static int cv = 200;
  }
  
  void myMethod() {
    class LocalInner {
      int iv = 300;
      //static int cv = 300; => 에러
      final static int CONST = 300; // => 허용
    }
  }
  
	public static void main(String[] args) {
    System.out.println(InstanceInner.CONST); //100
    System.out.println(StaticInner.cv); // 200
  }
}

final이 붙은 변수는 상수(constant)이기 때문에 어떤 경우라도 static을 붙이는 것이 가능하다.

다른 멤버가 중첩 클래스 사용하기

주의!

인스턴스 클래스는 외부 클래스의 인스턴스 멤버를 객체생성 없이 바로 사용할 수 있지만, 스태틱 클래스는 외부 클래스의 인스턴스 멤버를 객체생성 없이 사용할 수 없다. 인스턴스 클래스는 스태틱 클래스의 멤버들을 객체생성 없이 사용할 수 있지만, 스태틱 클래스에서는 인스턴스의 멤버들을 객체생성 없이 사용할 수 없다.

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
class InnerEx2 {
  class InstanceInner {}
  static class StaticInner{}
  
  InstanceInner iv = new InstanceInner();
  static StaticInner cv = new StaticInner();
  
  static void staticMethod() {
    //static 멤버는 인스턴스 멤버에 직접 접근할 수 없다.
    //InstanceInner obj1 = new InstanceInner();
    StaticInner obj2 = new StaticInner();
    
    // 굳이 접근하려면 아래와 같이 객체를 생성해야 한다.
    // 인스턴스 클래스는 외부 클래스를 먼저 생성해야만 생성할 수 있다.
    InnerEx2 outer = new InnerEx2();
    InstanceInner obj1 = outer.new InstanceInner();
    
    void instanceMethod() {
      // 인스턴스 메서드에서는 인스턴스멤버와 static 멤버 모두 접근 가능하다.
      InstanceInner obj1 = new InstanceInner();
      StaticInner obj2 = new StaticInner();
      // 메서드 내에 지역적으로 선언된 내부 클래스는 외부에서 접근할 수 없다.
      //LocalInner lv = new LocaInner();
    }
    
    void myMethod() {
      class LocalInner {}
      LocalInner lv = new LocalInner();
    }
  }
}

다른 멤버에 접근하기

인스턴스 클래스는 외부 클래스의 인스턴스 멤버이기 때문에 인스턴스 멤버와 static 멤버를 모두 사용할 수 있다.

스태틱 클래스는 외부 클래스의 static 멤버이기 때문에 인스턴스 멤버를 사용할 수 없다. 단지 static 멤버만을 사용할 수 있다.

지역 클래스는 외부 클래스의 인스턴스 멤버와 static 멤버를 모두 사용할 수 있으며, 지역 클래스가 포함된 메서드에 정의된 지역변수도 사용할 수 있다. 단, 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
29
30
31
class InnerEx3 {
  private int outerIv = 0;
  static int outerCv = 0;
  
  class InstanceInner {
    int iiv = outerIv; // 외부 클래스의 private 멤버에도 접근 가능
    int iiv2 = outerCv;
  }
  
  static class StaticInner {
    // 스태틱 클래스는 외부 클래스의 인스턴스 멤버에 접근할 수 없다.
    int siv = outerIv;
    static int scv = outerCv;
  }
  
  void myMethod() {
    int lv = 0;
    final int LV = 0;
    int lv2 = 0;
    lv2 = 1;
    
    class LocalInner {
      int liv = outerIv;
      int liv2 = outerCv;?
      int liv3 = lv;
      int liv4 = LV;
      // 값을 여러번 할당한 경우에는 접근할 수 없다.
      //int liv5 = lv2; => 에러!
    }
  }
}

로컬 클래스가 로컬 변수를 조회용으로만 사용하는 이유

메서드가 수행을 마쳐서 지역변수가 소멸된 시점에도, 지역 클래스의 인스턴스가 소멸된 지역변수를 참조하려는 경우가 발생할 수 있기 때문이다. JDK 1.8부터 지역 클래스에서 접근하는 지역변수 앞에 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
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// 인스턴스 메서드에 정의된 local class : 로컬 클래스가 로컬 변수를 조회용으로만 사용하는 이유?
package com.eomcs.oop.ex11.d;

public class Exam0340 {


  public static void main(final String[] args) {
    final Exam0340 obj = new Exam0340();
    final Runner r = obj.createRunner("홍길동");
    // createRunner()의 리턴 값은
    // 이 메서드에 선언된 로컬 클래스인 A의 객체이다.
    // A 객체는 Runner 규칙에 따라 만들었기 때문에
    // Runner 레퍼런스에 저장할 수 있다.
    // 이 레퍼런스를 사용하여 A 객체의 run() 메서드를 호출해 보자!
    r.run();

    // 로컬 클래스의 객체가 로컬 변수의 값을 조회용으로만 사용하는 이유?
    // => 위의 코드에서 createRunner() 호출이 끝나면 이 메서드가 실행되는 동안
    // 생성되었던 모든 로컬 변수는 스택 메모리에서 제거된다.
    // 즉 createRunner()의 name 파라미터가 제거된다는 것이다.
  }

  Runner createRunner(final String name) {
    // Exam0340 객체 주소를 받는 내장 변수 this 가 있다.
    //
    class A implements Runner {
      // 컴파일러는 바깥 클래스의 객체 주소를 받을 필드는 추가한다.
      // 또한 바깥 클래스의 객체 주소를 받는 생성자를 추가한다.
      /*
       * Exam0340 outer; // 바깥 클래스의 주소를 받을 필드
       *
       * String paramName; // run() 메서드에서 사용할 로컬 변수을 값을 받을 필드
       *
       * public A(Exam0365 obj, String str) { outer = obj; paramName = str;}
       *
       */

      @Override
      public void run() {
        // 바깥 메서드의 로컬 변수는 메서드 호출이 완료되는 순간
        // 스택 메모리에서 제거되기 때문에
        // 로컬 클래스의 객체에서 사용할 수 없는 상황이 발생할 것이다.
        // 그래서 컴파일러는 바깥 메서드의 로컬 변수 값을 저장할
        // 필드를 클래스에 추가한다.
        // 또한 로컬 변수의 값을 받는 생성자를 만든다.
        // 따라서 run() 메서드를 호출하는 시점에는
        // A 로컬 객체에 name 변수의 값이 들어 있기 때문에
        // 그래도 사용할 수 있는 것이다.
        // => 다음 코드의 name은 createRunner() 메서드의 파라미터가 아니다.
        // => A 인스턴스가 생성될 때 값을 따로 복제한 필드를 가리키는 것이다.
        //
        System.out.printf("%s님이 달립니다!", name);

        // 위 문장은 다음과 같이
        // 컴파일러가 로컬 클래스의 필드(예: paramName)를 사용하는 문장으로 바꾼다.
        // 그래서 createRunner() 메서드 호출이 끝나더라도
        // name 값을 사용할 수 있는 것이다.
        //
        // System.out.printf("%s님이 달립니다!", this.paramName);

      }
    }
    return new A();
    // 컴파일러는 로컬 클래스의 객체를 생성할 때
    // 바깥 클래스의 객체 주소와 로컬 객체가 사용하는
    // 메서드의 로컬 변수 값을 전달하기 위해
    // 다음과 같이 컴파일러가 만든 생성자를 호출한다.
    //
    // return new A(this, name);
  }
}


interface Runner {
  void run();
}

내부 클래스 생성

인스턴스 클래스의 인스턴스를 생성하려면 외부클래스의 인스턴스를 먼저 생성해야 한다. 스태틱 내부 클래스의 인스턴스는 외부 클래스를 먼저 생성하지 않아도 된다. 실제로 이런 경우가 발생했다는 것은 내부 클래스로 선언해서는 안 되는 클래스를 내부 클래스로 선언했다는 의미이다.

컴파일 시 생성되는 파일명

  • 외부클래스명$내부클래스명.class

  • 로컬클래스: 다른 메서드에 같은 이름의 내부 클래스가 존재할 수 있기 때문에 내부 클래스명 앞에 숫자가 붙는다

    • 외부클래스명$1내부클래스명.class
    • 외부클래스명$2내부클래스명.class

외부 클래스명.this

내부 클래스와 외부 클래스에 선언된 변수 이름이 같을 때 변수 앞에 this또는 외부클래스명.this를 붙여서 서로 구별할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Outer {
  int value = 10;
  class Inner {
    int value = 20;
    void method1() {
      int value = 30;
    	System.out.println("value:" + value);
    	System.out.println("this.value:" + this.value);
    	System.out.println("Outer.this.value:" + Outer.this.value);
    }
  }
}

class InnerEx5 {
  public static void main(String[] args) {
    Outer outer = new Outer();
    Outer.Inner inner = outer.new Inner();
    inner.method();
  }
}
// 결과
// value: 30
// this.value: 20
// Outer.this.value: 10

익명 클래스

클래스의 선언과 객체의 생성을 동시에 하기 때문에 단 한 번만 사용될 수 있고, 오직 하나의 객체만을 생성할 수 있는 일회용 클래스이다.

1
2
new 조상클래스이름() { /*멤버선언*/ }
new 구현인터페이스이름() { /*멤버선언*/ }

이름이 없기에 생성자도 가질 수 없고, 조상 클래스의 이름이나 구현하고자 하는 인터페이스의 이름을 사용해서 정의하기 때문에 오로지 하나의 클래스를 상속받거나 단 하나의 인터페이스만을 구현할 수 있다.

이름이 없기 때문에 외부 클래스명$숫자.class의 형식으로 클래스 파일명이 결정된다.

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

코어 자바스크립트 #2.8: 기본 연산자와 수학

코어 자바스크립트 #2.10: if와 '?'를 사용한 조건 처리

Loading comments from Disqus ...