Posts 학원 #35일차: 중첩 클래스: static nested class, inner class, local class
Post
Cancel

학원 #35일차: 중첩 클래스: static nested class, inner class, local class

클래스 종류에 따른 사용 범위

IMG_0099

스태틱 중첩 클래스와 내부 클래스가 접근 가능한 바깥 클래스 멤버 IMG_0102

top level class

  • 패키지에 소속된 클래스를 top level class(패키지 멤버 클래스)라고 부른다.
  • top level class의 접근 제한자
    • public: 다른 패키지에서도 접근 가능
    • (pakage-private): 다른 패키지에서 접근 불가

컴파일러는 public class의 이름과 소스 파일명이 일치해야 컴파일해준다. 한 소스파일의 top-level 클래스 중 오직 한 개의 클래스만이 public이 될 수 있다.

nested class

특정 클래스 안에서만 사용되는 클래스가 있다면 중첩 클래스로 선언하라. 즉 노출범위를 좁히는 것이 유지보수에 도움이 된다.

종류

  • static nested class
    • 바깥 클래스의 인스턴스에 종속되지 않는 클래스
    • top level class와 동일하게 사용된다.
  • non-static nested class
    • 바깥 클래스의 인스턴스에 종속되는 클래스
    • 바깥 클래스의 인스턴스 없이 생성할 수 없다.
  • local class
    • 특정 메서드 안에서만 사용되는 클래스
  • anonymous class
    • 클래스를 정의하는 동시에 인스턴스를 생성해야 한다.
    • 클래스 이름이 없기 때문에 생성자를 정의할 수 없다.
    • 인스턴스의 값을 초기화시키기 위해 복잡한 코드를 작성해야 한다면 인스턴스 블록에 작성하자.
    • 단 한 개의 인스턴스만 생성해서 사용할 경우 익명 클래스를 사용한다.
    • 문법
      • new 수퍼클래스() {클래스 정의}
      • new 인터페이스() {클래스 정의}
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
public class Exam0210 {
    // 1) static nested class
    static class A {}
    static int a;
    static void m1() {}
    static {}
    
    // 2) non-static nested class = inner class
    class B {}
    int b;
    void m2() {}
    {}
    
    public static void main(String[] args) {
        // 3) local class
        class C {}
        
        // 4) anonymous class
        Object obj = new Object() {
            // Object 클래스를 상속 받은 익명 클래스를 만든다.
            public void m1() {
                System.out.println("Hello!");
            }
        }
    }
}

접근 제어

top level class는 public과 public이 아닌 클래스로 나뉘어진다. public 으로 공개된 클래스는 다른 패키지에서 접근할 수 있지만 public으로 공개되지 않은 클래스는 다른 패키지에서 접근할 수 없다.

한편, 중첩클래스는 클래스의 멤버이기 때문에 필드나 메서드처럼 접근 제한자를 사용할 수 있다. 다만 로컬 클래스는 로컬 변수처럼 접근 제어 modifier를 붙일 수 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Exam0310 {
  private static class A1 {} 
  static class A2 {}
  protected static class A3 {}
  public static class A4 {}

  private class B1 {} 
  class B2 {}
  protected class B3 {}
  public class B4 {}
  
  // 로컬 클래스는 접근 제어 modifier를 붙일 수 없다.
  static void m1() {
    class C1{}
  }
  void m2() {
    class C2{}
  }
}

static nested class

클래스 정의와 인스턴스 생성

1
2
3
4
5
6
7
8
9
10
11
class A {
  static class X {}
}
public class Exam0110 {
  public static void main(String[] args) {
    // 레퍼런스 선언
    A.X obj;
    // 인스턴스 생성
    obj = new A.X();
  }
}

선언할 수 있는 멤버

static nested class는 static 멤버와 non-static 멤버 모두 선언할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
class A2 {
  static class X {
    static int v1;
    static void m1() {}
    static {}
    
    int v2;
    void m2() {}
    {}
  }
}

다른 멤버에 접근하기

static nested class는 같은 클래스 멤버에는 접근할 수 있고, 바깥 클래스 이름을 생략할 수 있다. 그러나 인스턴스 멤버에는 접근할 수 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class B {
    static int v1;
    static void m1() {}
    
    int v2;
    void m2() {}
    
    static class X {
        void test() {
            // 같은 클래스 멤버 접근 가능
            B.v1 = 100;
            B.m1();
            
            // 바깥 클래스 이름 생략 가능
            v1 = 200;
            m1();
            
            // 스태틱 멤버는 인스턴스 멤버에 접근 불가능
            //v2 = 100; // 컴파일 오류!
            //m2(); // 컴파일 오류!
        }
    }
}

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

같은 스태틱 멤버도, 인스턴스 멤버도 스태틱 멤버를 사용할 수 있으므로 static nested class를 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class C {
    static void m1() {
        // 스태틱 멤버가 static nested class 사용 가능
        X obj = new X();
        obj.test();
    }
    
    void m2() {
        // 인스턴스 멤버가 static nested class 사용 가능
        X obj = new X();
        obj.test();
    }
    
    static class X {
        void test() {
            System.out.println("X.test()");
        }
    }
}

import

중첩 클래스를 import할 때는 static을 적지 않는다.

와일드 카드로 전체 스태틱 멤버를 한꺼번에 지정하면, static nested class도 함께 import 된다. 그러나 실무에서는 어떤 변수인지 직관적으로 이해할 수 없기 때문에 와일드 카드 사용을 지양한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.eomcs.oop.ex11.b.sub;

public class M {
  // 다른 패키지에서 접근 할 수 있도록 public 으로 공개
  public static int v2;

  public static void m2() {}

  public static class Y {
    public void test() {
      System.out.println("M.Y.test()");
    }
  }
}
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
// import static 사용 전
package com.eomcs.oop.ex11.b;
import com.comcs.oop.ex11.b.sub.M;

class D {
  static int v1;
  static void m1(){}
  static class X {
    void test() {
      System.out.println("test()");
    }
  }
}

public class Exam0410 {
  public static void main(String[] args) {
	 // 같은 패키지 클래스
    D.v1 = 100;
    D.m1();
    D.X obj = new D.X();
    
    // 다른 패키지 클래스
    M.v2 = 200;
    M.m2();
    M.Y obj2 = new M.Y();
  }
}
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
// import static 사용 후
package com.eomcs.oop.ex11.b;

import static com.eomcs.oop.ex11.b.E.m1;
import static com.eomcs.oop.ex11.b.E.v1;
//import static com.eomcs.oop.ex11.b.E.*; // 와일드카드 지정 가능
import static com.eomcs.oop.ex11.b.sub.M.m2;
import static com.eomcs.oop.ex11.b.sub.M.v2;
//import static com.eomcs.oop.ex11.b.sub.M.*; // 와일드카드 지정 가능
// 와일드 카드로 전체 스태틱 멤버를 한꺼번에 지정할 수 있다.

// 중첩 클래스를 import 할 때는 static을 적지 않는다.
import com.eomcs.oop.ex11.b.E.X;
import com.eomcs.oop.ex11.b.sub.M.Y;

class E {
  static int v1;

  static void m1() {}

  static class X {
    void test() {
      System.out.println("test()");
    }
  }
}

public class Exam0420 {

  public static void main(String[] args) {
    // 같은 패키지 클래스의 스태틱 멤버
    v1 = 100;
    m1();
    X obj = new X();

    // 다른 패키지 클래스의 스태틱 멤버
    v2 = 200;
    m2();
    Y obj2 = new Y();
  }
}

inner class (non-static nasted class)

클래스 정의와 인스턴스 생성

  • 레퍼런스 선언 명칭은 static nested class와 같다: A.X obj
  • 인스턴스 생성은 다르다
    • 1) 바깥 클래스의 인스턴스 준비: A outer = new A();
      • 이때 A outer = null로 지정하면 nullPointException 발생
    • 2) 바깥 클래스의 인스턴스를 통해 생성: obj = outer.new A();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A { class X {} }

public class Exam0110 {
  public static void main(String[] args) {
    // 레퍼런스 선언
    A.X obj;
    
    // 인스턴스 생성
    //obj = new A.X(); -> 컴파일 오류
    
    // 1) 바깥 클래스의 인스턴스 준비
    A outer = new A();
    // 2) 바깥 클래스의 인스턴스 주소를 사용하여 inner class의 인스턴스 생성
    obj = outer.new X();
  }
}

선언할 수 있는 멤버

  • inner class는 static 멤버를 가질 수 없다.
  • static 멤버는 오직 top level class 나 static nested class 만이 가질 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A2 {
    class X {
        //static int v1;
        //static void m1();
        //static {}
        
        int v2;
        void m2();
        {}
    }
}

public class Exam0111 {
  public static void main(String[] args) {
    //A2 outer = new A2();
    //A2.X obj = outer.new X();
    
    // 위의 두 줄을 다음 한 줄로 표현할 수 있다.
    A2.X obj = new A2().new X();
  }
}

다른 멤버에 접근하기

inner class의 가장 큰 특징바깥 클래스의 다른 멤버에 손쉽게 접근할 수 있다는 것이다. 이것이 가능한 이유는 바깥 클래스의 인스턴스 멤버이기 때문이다. inner class는 tihs라는 내장변수를 통해 외부 클래스의 인스턴스 멤버에 접근할 수 있다. 주의할 점은 inner class의 this는 outer class를 가리키고, inner class의 메서드의 this는 inner class를 가리킨다는 사실이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class B {
    // 클래스 멤버
    static int v1;
    static void m1() {}
    
    // 인스턴스 멤버
    int v2;
    void m2(){}
    
    class X {
        void test() {
            // 같은 클래스 멤버 접근 가능
            B.v1 = 100;
            B.m1();
            // 바깥 클래스 이름 생략 가능
            v1 = 200;
            m1();
            // 인스턴스 멤버에 접근 가능
            v2 = 100;
            m2();
        }
    }
}

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

IMG_0103

static 멤버는 inner class에 접근할 수 없다. 인스턴스 멤버를 사용하려면 인스턴스 주소가 있어야 하는데, static 멤버는 인스턴스 주소를 담고 있는 this 변수가 없기 때문이다. 한편 인스턴스 메서드는 인스턴스 주소를 담고 있는 this 변수가 있다. 따라서 inner class를 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class C {
  static void m1() {
    // 스태틱 멤버는 인스턴스 멤버를 사용할 수 없다.
    // X obj = new X(); // 컴파일 오류!
    // obj.test();
  }

  void m2() {
    // 인스턴스 메서드는 inner class 를 사용할 수 있다.
    X obj = this.new X();
    obj.test();
  }

  class X {
    void test() {
      System.out.println("X.test()");
    }
  }
}

import 사용

1
2
3
4
5
6
7
8
9
package com.eomcs.oop.ex11.c.sub;

public class M {
    public class Y {
        public void test() {
            System.out.println9("M.Y.test()");
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.eomcs.oop.ex11.c;

import com.eomcs.oop.ex11.c.D.X;
import com.eomcs.oop.ex11.c.sub.M;
import com.eomcs.oop.ex11.c.sub.M.Y;

class D {
  class X {
    void test() { System.out.println("test()"); }
  }
}

public class Exam0410 {
  public static void main(String[] args) {
    D outer = new D();
    X obj = outer.new X();
    obj.test();
    
    M outer2 = new M();
    Y obj2 = outer2.new Y();
    obj2.test();
  }
}

바깥 클래스의 인스턴스를 보관할 this 변수와 생성자

인스턴스 주소를 저장하는 장소

인스턴스 메서드는 this라는 내장 변수에 인스턴스 주소를 보관한다. 그렇다면 inner 클래스는 this를 어디에 보관할까?

자바 컴파일러는 바깥 클래스의 인스턴스 주소를 저장하기 위해 필드를 추가한다. 컴파일 완료된 inner class의 .class 파일을 열어 보면, 바깥 클래스의 객체 주소를 저장할 인스턴스 필드, final synthetic com.eomcs.oop.ex11.c.E this$0;가 추가되어 있다. 개발자는 바깥 클래스의 인스턴스 주소를 저장하기 위해 바깥 클래스명.this으로 코드를 작성해야 한다.

1
2
3
4
5
6
7
8
9
10
class E {
    class X {}
}

public class Exam0510 {
    public static void main(String[] args) {
        E outer = new E();
        E.X obj = new outer.new X();
    }
}

인스턴스 주소를 저장하는 시점

인스턴스 메서드에 있는 this 변수는 인스턴스 메서드를 호출할 때 객체 주소를 저장한다. 그렇다면 inner class에서 바깥 클래스의 인스턴스 주소는 언제 저장할까?

생성자의 파라미터로 받은 바깥 클래스의 객체 주소컴파일할 때 자동으로 추가된 인스턴스 필드에 보관된다. 바깥 클래스의 인스턴스를 가지고 inner class의 인스턴스를 생성할 때, 즉 inner class 의 생성자를 호출할 때 바깥 클래스의 인스턴스 주소를 파라미터로 전달한다.

이를 위해 컴파일러는 inner class 를 컴파일할 때 생성자를 변형한. 예를 들어, 개발자가 public X(){}라고 기본 생성자를 정의했다면, 컴파일러는 public X(E outer){}로 바꿀 것이다. 실제 컴파일한 .class 파일을 보면 생성자가 다음과 같이 되어 있다.

E$X(com.eomcs.oop.ex11.c.E arg0);
0  aload_0 [this] // 바깥 클래스의 인스턴스 주소를 받는 파라미터
1  aload_1 [arg0]
// this$0: 컴파일러가 바깥 클래스의 인스턴스 주소를 저장하기 위해 추가한 변수
// this$0에 바깥 클래스의 인스턴스 주소가 들어 있는 arg0값 저장
2  putfield com.eomcs.oop.ex11.c.E$X.this$0 : com.eomcs.oop.ex11.c.E [10]
5  aload_0 [this]
6  invokespecial java.lang.Object() [12]
9  return
Line numbers:
  [pc: 0, line: 5]
Local variable table:
// 0번째 로컬 변수 this: 다른 로컬변수(파라미터 포함)들보다 우선한다
  [pc: 0, pc: 10] local: this index: 0 type: com.eomcs.oop.ex11.c.E.X

변수를 선언만 하면 컴파일러가 컴파일하지 않는다. 선언하고 사용해야 컴파일을 한다. 즉 바이트코드에서 선언만 한 변수를 확인할 수 없다.

inner 클래스는 기본 생성자를 추가하는데 바깥 클래스의 인스턴스 주소를 받는 기본 생성자를 추가한다. 내부에 저장한다. 그러나 스태틱 메서드는 this라는 내장 변수가 없다.

중첩 클래스에 바깥 클래스의 인스턴스 주소를 전달 하는 방법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 중첩 클래스의 바깥 클래스의 인스턴스 주소를 전달하는 방법

// 1) 바깥 클래스의 인스턴스를 준비한다.
Exam0213 outer = new Exam0213();

// 2) 중첩 클래스의 레퍼런스 준비
Exam0213.A obj;

// 3) 중첩 클래스의 인스턴스 생성
obj = Exam0213.new A();

// 내부 구동 원리
// - 실제 중첩 클래스 A에 대해 호출되는 생성자는 기본 생성자가 아니다.
// - 바깥 클래스의 인스턴스 주소를 받는 생성자가 호출된다.
// - 즉 실제 컴파일러는 위의 코드를 다음과 같이 변경한다.
//   obj = new A(outer);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Exam0215 {
    class A {
        // 기존에 생성자가 있다면
        // 바깥 클래스의 인스턴스 주소를 받는 파라미터를 추가한다.
        
        public A() {}
        // 위 생성자는 다음의 생성자로 변경된다.
        // => public A(Exam0215 outer) { this.outer = outer; }
        
        public A(int value) {
            this.value = value;
        }
        // 위 생성자는 다음의 생성자로 변경된다.
        /*
        public A (Exam0213 outer, int value) {
        	this.outer = outer;
        	this.value = value;
        }
       */
        
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Exam0216 {
    public static void main(String[] args) {
        Exam0213 outer = new Exam0213();
        Exam0213.A obj;
        
        // 개발자가 작성한 코드
        obj = outer.new A()1;
        
        // 컴파일러가 컴파일하면서 바꾼 코드
        // obj = new A(outer);
        
        // 개발자가 작성한 코드
        obj = outer.new A();
        
        // 컴파일러가 컴파일하면서 바꾼 코드
        // obj = new A(outer, 100);
    }
}

inner 클래스에서 변수를 찾는 순서

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
class G {
  int v1 = 1;
  int v2 = 2;
  int v3 = 3; 

  class X {
    int v1 = 10;
    int v2 = 20;

    void m1(int v1) {

      // 중첩 클래스의 메서드에서 필드를 사용하기 
      System.out.println("G 객체:");
      System.out.printf("G.this.v1 = %d\n", G.this.v1); //1
      System.out.printf("G.this.v2 = %d\n", G.this.v2); //2
      System.out.printf("G.this.v3 = %d\n", G.this.v3); //3

      System.out.println("-------------------------");

      // inner 클래스의 인스턴스 필드 접근
      System.out.println("G.X 객체:");
      System.out.printf("this.v1 = %d\n", this.v1); //10
      System.out.printf("this.v2 = %d\n", this.v2); //20

      System.out.println("-------------------------");

      // 로컬 변수 접근
      System.out.println("로컬:");
      System.out.printf("v1 = %d\n", v1); //100

      System.out.println("-------------------------");
    }
  }
}

public class Exam0610 {
  public static void main(String[] args) {
    G outer = new G();
    
    G.X obj = outer.new X();
    obj.m1(100);
  }
}

this를 명시하지 않았을 때 변수를 찾는 순서

  • 1) 로컬 변수를 찾는다.
  • 2) 메서드가 소속된 클래스의 인스턴스 필드를 찾는다.
  • 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
class H {
    int v1 = 1;
    int v2 = 2;
    int v3 = 3;
    
    class X {
        int v1 = 10;
        int v2 = 20;
        //static int s1 = 11; => 컴파일에러!
        
        void m1(int v1) {
            System.out.printf("v1 = %d\n", v1); // 100
            System.out.printf("v2 = %d\n", v2); // 20
            System.out.printf("v3 = %d\n". v3); // 3
            
            // inner 클래스의 v1을 사용하고 싶을 때
            System.out.printf("this.v1 = %d\n", this.v1); // 10
        }
    }
    
    public static void main(String[] args) {
        H outer = new H();
        H.X obj = outer.new X();
        obj.m1(100);
    }
}

inner class (non-static nested class)에서는 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
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
import java.util.ArrayList;
import java.util.List;

public class Exam0710 {
  public satic void main(String[] args) {
    final Musics m1 = new Musicss(); 
    m1.add("aaa.mp3");
    m1.add("bbb.mp3");
    m1.add("bbb.mp3");
    
    final Musics.Player p1 = m1.createPlayer();
    final Musics.Player p2 = m1.createPlayer();
    
    p1.play();
    p2.play();
    
    final Musics m2 = new Musics();
    m2.add("xxx.mp3");
    m2.add("yyy.mp3");
    
    final Musics.Player p3 = m2.new Player();
    p3.play();
  }
}

class Musics {
  class Player {
    public void play() {
      // 이 메서드가 호출되려면 Player 객체가 존재해야 한다.
      // Player 객체가 존재하려면 Musics 객체가 존재해야 한다.
      // 따라서 이 메서드가 호출될 때는 이미 Musics 객체가 존재한 상태이다.
      // 그 Musics 객체로 Player 객체를 만든 것이다.
      // 따라서 inner class의 객체는 항상 자신을 만든 객체의 주소를
      // 바깥_클래스명.this 내장 변수에 보관한다.
      for (final String song : songs) {
        System.out.println(song);
      }
      System.out.println("-------------------")
    }
  }
  
  List<String> songs = new ArrayList<>();
  
  public void add(final String song) {
    songs.add(song);
  }
  
  public Player createPlayer() {
    return /*this.*/new Player();
  }
  
  public void delete(final int index) {
    songs.remove(index);
  }
}

/* 
결과

aaa.mp3
bbb.mp3
bbb.mp3
-----------------------------
aaa.mp3
bbb.mp3
bbb.mp3
-----------------------------
xxx.mp3
yyy.mp3
-----------------------------
*/
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 static void main(final String[] args) {
  final ArrayList<String> m1 = new ArrayList<>();
  m1.add("aaa.mp3");
  m1.add("bbb.mp3");
  m1.add("bbb.mp3");

  // ArrayList 도 Iterator 구현체를 inner class 로 갖고 있다.
  // iterator() 메서드는 이 구현체를 생성하여 리턴한다.
  //
  final Iterator<String> i1 = m1.iterator();
  final Iterator<String> i2 = m1.iterator();

  while (i1.hasNext()) {
    System.out.println(i1.next());
  }
  System.out.println("--------------------");

  while (i2.hasNext()) {
    System.out.println(i2.next());
  }
  System.out.println("--------------------");
}

/*
결과

aaa.mp3
bbb.mp3
bbb.mp3
--------------------
aaa.mp3
bbb.mp3
bbb.mp3
--------------------
*/

로컬 클래스

클래스의 정의와 인스턴스 생성

  • 메서드 안에 정의하는 클래스를 ‘local class’라고 한다.
  • 특정 메서드 안에서만 사용되는 경우 로컬 클래스로 정의한다.
  • 쓸 데 없이 외부에 노출하지 않기 위해 사용한다. 노출을 줄이면 유지보수에 좋다.
  • 로컬 클래스에서 로컬이라는 말은 이 메서드에서만 사용할 수 있다는 말이다. 그냥 사용 범위에 대한 제한을 가리키는 말이다. 메서드를 호출할 때 클래스가 정의된다는 뜻이 아니다. 로컬 변수와 다르다.
  • 클래스는 별도 파일로 존재한다. 클래스는 명령어이다. 그 명령어가 메서드 안에서만 쓰이느냐, 바깥에서 쓰이느냐, 내부에서 쓰이느냐의 차이이다. 사용 범위를 제한하는 것이지 메서드를 호출할 때 생성되고 생성되지 않는 게 아니다.

사용 범위

  • 다른 메서드에 정의된 클래스는 사용할 수 없다.

.class 파일명

[바깥클래스명]$[정의된순서][로컬클래스명].class

1
2
3
4
5
6
7
8
9
10
11
12
class B2 {
  void m1() {
    class X {} // B2$1X.class
    class Y {} // B2$1Y.class
  }
  
  static void m2() {
    class Y {} // B2$2Y.class
    class X {} // B2$2X.class
    class Z {} // B2$1Z.class
  }
}

인스턴스 메서드와 로컬 클래스

인스턴스 메서드는 속해 있는 클래스의 인스턴스 주소를 this 변수에 저장한다. 그래서 인스턴스 메서드 안에 정의된 로컬 클래스는 바깥 클래스의 인스턴스를 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class C {
  int v1 = 1;
  void m1() {
    class X {
      void f() {
        System.out.printf("v1 = %d\n", C.this.v1);
      }
    }
    X obj = new X();
    obj.f();
  }
}

public class Exam0210 {
  public static void main(String[] args) {
    C c = new C();
    c.m1();
  }
}

.class 파일을 확인해보면 바깥 클래스에 인스턴스 주소를 저장하는 필드가 선언되어 있다. 또한 생성자에서 바깥 클래스의 인스턴스 주소를 받는 파라미터가 선언되어 있다.

class com.eomcs.oop.ex11.d.C$1X {
	final synthetic com.eomcs.oop.ex11.d.C this$0;

	C$1X(com.eomcs.oop.ex11.d.C arg0);
		0  aload_0 [this]
		1  aload_1 [arg0]
		2  putfield com.eomcs.oop.ex11.d.C$1X.this$0 : com.eomcs.oop.ex11.d.C [10]

스태틱 메서드와 로컬 클래스

스태틱 메서드는 속해 있는 클래스의 인스턴스 주소를 담을 변수가 없다. 따라서 로컬 클래스는 바깥 클래스의 인스턴스를 사용할 수 없다.

1
2
3
4
5
6
7
8
9
10
11
12
class C2 {
  int v1 = 1;
  static void m1() {
    class X {
      void f() {
        //System.out.println("v1 = %d\n", C2.this.v1); // 컴파일 오류
      }
    }
    X obj = new X();
    obj.f();
  }
}

.class 파일을 확인해보면 바깥 클래스를 보관할 필드가 선언되어 있지 않다. 또한 생성자도 바깥 클래스의 인스턴스를 받는 파라미터가 선언되어 있지 않다.

class com.eomcs.oop.ex11.d.C2$1X {

	C2$1X();
		0  aload_0 [this]
		1  invokespecial java.lang.Object() [8]

변수 접근

로컬 클래스는 바깥 클래스의 인스턴스 주소를 저장할 필드가 있을 뿐만 아니라 인스턴스 메서드 안에 선언된 로컬 변수의 값을 저장할 필드도 있다.

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 D {
  int v1 = 1;
  void m1() {
    int v2 = 2;
    class X {
      int v3 = 3;
      void f() {
        int v4 = 4;
        System.out.println("v4 = %d\n", v4);
        System.out.println("v3 = %d\n", this.v3);
        System.out.println("v2 = %d\n", v2);
        System.out.println("v1 = %d\n", D.this.v1);
      }
    }
    X obj = new X();
    obj.f();
  }
}

public class Exam0310 {
  public static void main(String[] args) {
    D obj = new D();
    obj.m1();
  }
}

this로는 해당 인스턴스의 필드에 접근할 수 있다. 로컬 변수에 접근할 수 있는 것이 아니다.

.class 파일의 코드는 다음과 같다.

class com.eomcs.oop.ex11.d.D$1X {

	int v3; <== 로컬 클래스의 인스턴스 필드
	final synthetic com.eomcs.oop.ex11.d.D this$0;  <== 바깥 클래스의 인스턴스 주소 저장
	private final synthetic int val$v2; <== 바깥 메서드의 로컬 변수 저장

변수를 찾는 순서

this를 생략했을 때 다음 순서로 변수를 찾는다.

  • 1) 로컬 변수
  • 2) 인스턴스 변수
  • 3) 메서드에 선언된 로컬 변수
  • 4) 바깥 클래스의 인스턴스 변수 또는 스태틱 변수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class D2 {
  int v1 = 1;
  void m1() {
    //int v1 = 10;
    class X {
      //int v1 = 100;
      void f() {
        //int v1 = 1000;
        System.out.println("v1 = %d\n", v1); //1
      }
    }
  }
}

public class Exam0320 {
  public static void main(String[] args) {
    D2 obj = new D2();
  }
}

메서드에 선언된 로컬 변수 접근하기

인스턴스 메서드에 정의된 local class

상수 값이거나 상수에 준하는 경우(값을 한 번만 할당한 경우) 로컬 클래스에서 메서드의 로컬 변수를 사용할 수 있다. 즉 로컬 클래스에서 바깥 메서드의 로컬 변수를 사용할 경우에는 값을 조회하는 용도로 사용하는 것이다. 로컬 클래스의 객체가 사용하는 로컬 변수는 로컬 클래스의 메서드 호출이 끝났을 때 제거되기 때문이다.

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
class D3 {
  void m1() {
    final int v1 = 1;
    int v2 = 2;
    int v3 = 3;
    v3 = 30;
    
    class X {
      void f1() {
        // 1) final로 선언된 경우
        System.out.println("v1 = %d\n", v1);
        // 2) final로 선언한 것은 아니지만 값을 한 번만 할당한 경우
        System.out.println("v2 = %d\n", v2);
        // 3) 값을 여러 번 할당한 경우에는 접근할 수 없다. 
        //System.out.println("v3 = %d\n", v3);
      }
    }
    X obj = new X();
    obj.f();
  }
}

public class Exam0330 {
  public static void main(String[] args) {
    D3 obj = new D3();
    obj.m1();
  }
}

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

바깥 메서드의 로컬 변수는 메서드 호출이 완료되는 순간 스택 메모리에서 제거되기 때문에 로컬 클래스의 객체에서 사용할 수 없는 상황이 발생할 것이다. 그래서 컴파일러는 바깥 메서드의 로컬 변수 값을 저장할 필드를 클래스에 추가한다. 또한 로컬 변수의 값을 받는 생성자를 만든다. 따라서 아래 코드에서 run() 메서드를 호출하는 시점에는 A 로컬 객체에 name 변수 값이 들어있기 때문에 그래도 사용할 수 있는 것이다.

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
public class Exam0340 {
  public static void main(final String[]) {
    final Exam0340 obj = new Exam0340();
    final Runner r = obj.createRunner("홍길동");
    // createRuner() 호출이 끝나면 이 메서드가 실행되는 동안 생성되었던
    // 모든 로컬 변수는 스택 메모리에서 제거된다.
    // 즉 createRunner()의 name 파라미터가 제거된다는 뜻이다.
    r.run();
  }
  
  Runner createRunner(final String name) {
    class A implements Runner {
      /*
      <컴파일러가 추가한 필드와 생성자?
      Exam0340 outer; // 바깥 클래스의 객체 주소를 받을 필드
      String paramName; // run()에서 사용할 로컬 변수의 값을 받을 필드
      public A(Exam340 obj, String str) {
      	outer = obj;
      	paramName = str;
      }
      */
      @Override
      public void run() {
        // 여기서 name은 createRunner() 메서드의 파라미터가 아니다.
        // A 인스턴스가 생성될 때 값을 따로 복제한 필드를 가리키는 것이다.
        System.out.println("%s님이 달립니다!", name);
        
        // 위 문자은 다음과 같이
        // 컴파일러가 로컬 클래스의 필드(paramName)를 사용하는 문장으로 바꾼다.
        // 그래서 createRunner() 메서드 호출이 끝나더라도
        // name 값을 사용할 수 있는 것이다. 
        //System.out.printf("%s님이 달립니다!", this.paramName);
      }
    }
    return new A();
    // 컴파일러는 로컬 클래스의 객체를 생성할 때
    // 바깥 클래스의 객체 주소화 
    // 로컬 객체가 사용하는 메서드의 로컬 변수 값을 전달하기 위해
    // 다음과 같이 컴파일러가 만든 생성자를 호출한다.
    // return new A(this, name)
  }
}

interface Runner {
  void main();
}

mini pms v.26-b: inner class

내부 클래스(inner class; non-static nested class)

바깥 클래스의 인스턴스 멤버이기 때문에 바깥 클래스의 다른 인스턴스 멤버(필드나 메서드)에 직접 접근할 수 있다. 따라서 바깥 클래스의 멤버를 사용하기 위해 내부 클래스의 객체를 생성할 때 생성자 파라미터로 바깥 클래스의 인스턴스를 받을 필요가 없다.

IMG_0098

실습

1단계 - ListIterator 구현체를 inner class로 정의한다.

논스태틱 중첩 클래스로 정의한 Iterator 구현체는 바깥 클래스(AbstractList)의 인스턴스 멤버를 직접 접근할 수 있다. 생성자에서 바깥 클래스의 인스턴스를 받을 필요가 없다. 중첩 클래스가 인스턴스 멤버가 되는 순간 바깥 클래스의 영향을 받기 때문에 바깥 클래스의 타입 파라미터와 중첩 클래스의 타입 파라미터와 이름이 충돌이 발생하게 된다. 따라서 중첩 클래스의 타입 파라미터 이름을 변경하여 바깥 클래스의 타입 파라미터와 구분하도록 한다.

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 abstract class AbstractList<E> implements List<E> {
  //...
  
  private class ListIterator<T> implements Iterator<T> {
    int cursor;

    @Override
    public boolean hasNext() {
      return cursor < AbstractList.this.size();
    }

    @SuppressWarning("unchecked")
    @Override
    public T next() {
      if (cursor == /*AbstractList.this.*/size())
        throw new NoSuchElementException();
      
      // 바깥 클래스의 타입 파라미터 E와 중첩 클래스의 타입 파라미터 T가 
      // 결국 같은 타입을 가리키지만
      // 컴파일러 입장에서는 서로 다른 값이 될 수 있으므로
      // 다음과 같이 명시적으로 형변환해야 한다.
      return (T) /*AbstractList.this.*/get(cursor++);
    }
  }
}

그러나 논스태틱 중첩 클래스로 정의한 Iterator 구현체는 바깥 클래스의 인스턴스 멤버를 직접 접근할 수 있고, 생성자에서 바깥 클래스의 인스턴스를 받을 필요가 없다. 바깥 클래스의 타입 파라미터를 그대로 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class AbstractList<E> implements List<E> {
  //...
  
  private class ListIterator implements Iterator<E> {
    int cursor;
    
    @Override
    public boolean hasNext() {
      return cursor < size();
    }
    
    @Override
    public E next() {
      if (cursor == size())
        throw new NoSuchElementException();
      return get(cursor++);
    }
  }
}

2단계 - StackIterator 구현체를 inner class로 정의한다.

AbstractList에서 구현한 iterator()Stack 자료 구조에 맞춰 오버라이딩한다. StackIterator의 경우 ListIterator와 달리 생성자를 지우면 안 된다. 생성자에서 outer class의 stack 객체를 클론해야 하기 때문이다.

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 Stack<E> extends LinkedList<E> {
  @Override
  public Iterator<E> iterator() {
    return new StackIterator();
  }
  
  private class StackIterator implements Iterator<E> {
    Stack<E> stack;
    
    public StackIterator() {
      try {
        this.stack = Stack.this.clone();
      } catch (Exception e) {
        throw new RuntimeException("큐를 복제하는 중에 오류 발생!");
      }
    }
    
    @Override
    public boolean hasNext() {
      return !stack.empty();
    }
    
    @Override
    public E next() {
      if (stack.empty())
        throw new NoSuchElementException();
      return stack.pop();
    }
  }
}

inner class는 바깥 클래스의 멤버에 접근할 목적으로 사용된다.

3단계 - QueueIterator 구현체를 논스태틱 중첩 클래스로 정의한다.

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 Queue<E> extends LinkedList<E> {
  //..
  public Iterator<E> iterator() {
    return new QueueIterator();
  }
  
  private class QueueIterator implements Iterator<E> {
    Queue<E> queue;
    public QueueIterator() {
      try {
        this.queue = Queue.this.clone();
      } catch (Exception e) {
        throw new RuntimeException("큐를 복제하는 중에 오류 발생!");
      }
    }
    
    @Override
    public boolean hasNext() {
      return queue.size() > 0;
    }
    @Override
    public E next() {
      if (queue.size() == 0)
        throw new NoSuchElementException();
      return queue.poll();
    }
  }
}

mini pms v.26-c: local class

local class

  • 메서드의 로컬 변수와 사용법이 유사하다.
  • 특정 메서드에서만 사용할 클래스라면 그 메서드 내부에 정의하는 것이 코드를 읽기 쉽고 유지보수 하기 쉽게 만든다.
  • 로컬 클래스도 바깥 클래스의 멤버에 직접 접근할 수 있다.
  • 인스턴스 메서드는 this 라는 내장 변수에 인스턴스 주소를 담고 있다.
  • 인스턴스 메서드에 정의된 로컬 클래스에서도 이 this 변수를 사용할 수 있다.

실습

1단계 - ListIterator 구현체를 iterator() 메서드 안에 정의한다.

로컬 클래스는 바깥 클래스의 객체 주소 없이 그냥 만들 수 있다.

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 abstract class AbstractList<E> implements List<E> {
  protected int size;
  @Override
  public int size() {
    return size;
  }
  // Iterator 구현체를 리턴하는 메서드를 정의
  @Override
  public Iterator<E> iterator() {
    // 접근제어자 사용 불가능 
    class ListIterator implements Iterator<E> {
      int cursor;
      
      @Override
      public boolean hasNext() {
        return cursor < size();
      }
      
      @Override
      public E next() {
        if (cursor == size())
          throw new NoSuchElementException();
        return get(cursor++);
      }
    }
    return new ListIterator();
  }
}

2단계 - StackIterator 구현체를 iterator() 메서드 안에 정의한다.

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
public class Stack<E> extends LinkedList<E> {
  //..
  @SuppressWarning("unchecked")
  @Override
  public Stack<E> clone() throws CloneNotSupportedException {
    Stack<E> newSTack = new Stack<>();
    Object[] values = this.toArray();
    for (Object value : values) {
      newStack.push((E) value);
    }
    return newStack;
  }
  
  @Override
  public Iterator<E> iterator() {
    class StackIterator implements Iterator<E> {
      Stack<E> stack;
      
      public StackIterator() {
        try {
          this.stack = Stack.this.clone();
        } catch (Exception e) {
          throw new RuntimeException("큐를 복제하는 중에 오류 발생!");
        }
      }
      
      @Override
      public boolean hasNext() {
        return !stack.empty();
      }
      
      @Override
      public E next() {
        if (stack.empty())
          throw new NoSuchElementException();
      }
    }
  }
}

3단계 - QueueIterator 구현체를 iterator() 메서드 안에 정의한다.

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
public class Queue<E> extends LinkedList<E> {
	//...
  @SuppressWarnings("unchecked")
  @Override
  public Queue<E> clone() throws CloneNotSupportedException {
    Queue<E> newQueue = new Queue<>();
    Object[] values = this.toArray();
    for (Object value : values) {
      newQueue.offer((E) value);
    }
    return newQueue;
  }

  @Override
  public Iterator<E> iterator() {
    class QueueIterator implements Iterator<E> {
      Queue<E> queue;
      public QueueIterator() {
        try {
          this.queue = Queue.this.clone();
        } catch (Exception e) {
          throw new RuntimeException("큐를 복제하는 중에 오류 발생!");
        }
      }

      @Override
      public boolean hasNext() {
        return queue.size() > 0;
      }

      @Override
      public E next() {
        if (queue.size() == 0)
          throw new NoSuchElementException();
        return queue.poll();
      }
    }

    return new QueueIterator();
  }
}

객체를 사용한다는 것은, 메서드를 호출한다는 것이다. 객체를 생성하는 것도 객체를 사용하는 것이다. 왜냐하면 클래스의 메서드의 일종인 생성자를 호출하고 있기 때문이다.

예를 들어, BoardHandler 클래스에 Iterator<Board> iterator = boardList.iterator();라는 코드가 있다고 하면, 여기서 BoardHandler는 boardList, 즉 List의 iterator() 메서드를 사용하고 있다. 따라서 BoardHandler는 List 객체를 사용하고 있는 것이다. 레퍼런스는 상관이 없다.

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

학원 #34일차: iterator 디자인 패턴, static nested class

학원 #38일차: 익명 클래스, 커맨드 디자인 패턴, 상수 다루기

Loading comments from Disqus ...