코린이의 소소한 공부노트

내부 클래스 본문

Java

내부 클래스

무지맘 2022. 5. 19. 02:44

내부 클래스는 클래스 안의 클래스이다.

// 1) 일반적인 클래스
class A { }
class B { }
class C { }

// 2) 내부 클래스
class A{	// B의 외부 클래스
   class B { }	// A의 내부 클래스
}
class C { }

내부 클래스를 이용하면 다음과 같은 장점이 있다.

1. 내부 클래스에서 외부 클래스의 멤버들에 쉽게 접근할 수 있다.

  - 1)의 경우 B에서 A의 멤버를 이용하려면 A 객체를 생성 후 접근해야 한다.

  - 2)의 경우 B에서 A의 멤버를 이용할 때 객체 생성 없이도 접근이 가능하다.

2. 코드의 복잡성을 줄일 수 있다. (일종의 캡슐화)

클래스 B를 클래스 A에서만 쓴다고 해보자.

1)의 경우

 - 클래스 A: 나만 B를 쓰는데 굳이 밖에 둘 이유가 있을까?

 - 클래스 B: 그러니까 말이야.

 - 클래스 C: 나도 B 별로 안 보고 싶거든?

2)의 경우

 - 클래스 A: 나만 쓰는 거 나만 보니 좋군.

 - 클래스 B: 내가 쓰이는 곳에 들어와 있으니 좋군.

 - 클래스 C: 불필요한 게 안 보이니 좋군.

 

결과를 확인해볼 수 있는 코드로 예시를 들어보겠다.

1. 내부 클래스 사용 X

class A{ // B를 사용하는 클래스
    int i = 5;
    B b = new B();
}

class B{
    void method() {
        A a = new A(); // A의 멤버에 접근하려면 객체 생성 필요
        System.out.println(a.i);
    }
}

class C { } // B를 사용하지 않는 클래스

// 메인 내부
B b = new B();
b.method(); // 5

  - A는 B를 사용하고, C는 B를 사용하지 않는다.

  - B에서 A의 멤버 변수 i에 접근하려면 A 객체를 생성 후 접근해야 한다.

2. 내부 클래스 사용 O

class A{ // B의 외부 클래스
    int i = 5;
    B b = new B();

    class B{ // A의 내부 클래스
        void method() {
            System.out.println(i); // A 객체 생성 불필요
        }
    }
}

// 메인 내부
B b = new B(); // 에러. A 객체를 먼저 생성해야 B 객체 생성 가능
b.method() // 당연히 사용 불가
// 실제 사용 방법은 글을 쭉 읽다보면 나옴
// 잘 모르겠다면 맨 아래 답이 있음

  - B가 A의 내부 클래스가 됐으므로 B 내부에서 A의 멤버 변수 i에 접근할 때 객체를 생성할 필요가 없어졌다.

 

내부 클래스의 종류와 유효 범위는 변수와 동일하다.

// 일반적인 클래스 선언
class Outer{
    int iv = 1;		// 인스턴스 변수
    static int cv = 2;	// 클래스 변수
    void method(){
        int lv = 3;	// 지역 변수
    }
}

// 내부 클래스 선언
class Outer{
    class InstanceInner {}	// 인스턴스 내부 클래스
    static class StaticInner {}	// 스태틱 내부 클래스
    void method(){
        class LocalInner {}	// 지역 내부 클래스
    }
}

1) 인스턴스 내부 클래스: iv와 유사

  - 외부 클래스의 멤버 변수 선언 위치에 선언

  - 외부 클래스의 인스턴스 멤버처럼 사용됨

  - 외부 클래스의 iv, im들과 관련된 작업에 사용됨

2) 스태틱 내부 클래스: cv와 유사

  - 외부 클래스의 멤버 변수 선언 위치에 선언

  - 외부 클래스의 스태틱 멤버처럼 사용됨

  - 외부 클래스의 cv, cm(=sv, sm)들과 관련된 작업에 사용됨

3) 지역 내부 클래스: lv와 유사

  - 외부 클래스의 메서드 or 초기화 블록 안에 선언

  - 선언된 영역 안에서만 사용됨

4) 익명 클래스(anonymous class)

  - 클래스의 선언과 객체의 생성을 동시에 하는 1회성 이름 없는 클래스

  - 이벤트 처리에 주로 쓰임

  - 자세한 설명은 2022.05.19 - [Java] - 익명 클래스

 

내부 클래스의 제어자는 변수에 사용 가능한 제어자와 동일하다.

// 일반적인 클래스 선언
class Outer{ // 접근 제어자가 없음 = default
    int iv = 1;
    static int cv = 2;
    void method(){
        int lv = 3;
    }
}

// 내부 클래스 선언
class Outer{
    private class InstanceInner {}		// 접근 제어자가 private
    protected static class StaticInner {}	// 접근 제어자가 protected
    void method(){
        class LocalInner {}			// 접근 제어자가 default
    }
}

1) 일반적인 클래스에는 접근 제어자를 default나 public만 사용 가능하다.

2) 내부 클래스는 외부 클래스의 멤버처럼 쓰이기 때문에, 모든 접근 제어자를 쓸 수 있다.

 

내부 클래스에서 static 멤버를 선언하고 싶다면 static 내부 클래스에서 선언해야 한다.

class Outer{
    class InstanceInner {	// 인스턴스 내부 클래스
        int iv = 4;		// iv 선언 가능
        static int cv = 5;	// static변수 선언 불가
        final static int CON = 6; // 상수 선언 가능
    }
    static class StaticInner {	// 스태틱 내부 클래스
        int iv = 7;		// iv 선언 가능
        static int cv = 8;	// static변수 선언 가능
        final static int CON = 9; // 상수 선언 가능
    }
    void method(){
        class LocalInner {	// 지역 내부 클래스
            int iv = 10;	// iv 선언 가능
            static int cv = 11;	// static변수 선언 불가
            final static int CON = 12;	// 상수 선언 가능
        }
    }
}

  - 사실 당연한 것이다. static 멤버는 객체 생성 없이 사용 가능하다. 그렇기 때문에 static 멤버를 쓰고 싶다면 객체 생성 없이 쓸 수 있는 static 클래스 내에 선언해야 하는 것이다.

  - 상수의 경우 객체의 상관없이 값이 변하지 않아야 할 경우 static을 붙이기 때문에, 스태틱 내부 클래스가 아니어도 static으로 선언되는 것을 허용한다.

 

위의 코드에서 각 내부 클래스의 접근성을 확인해보겠다.

1. 각 내부 클래스의 상수에 접근

// Outer 클래스의 메인 내부
System.out.println(InstanceInner.CON); // 6
System.out.println(StaticInner.CON); // 9
System.out.println(LocalInner.CON); // 에러

  - 지역 내부 클래스의 경우, 해당 클래스가 선언된 영역(method() 내부)에서만 사용이 가능하기 때문에 그 안에 선언된 상수에 접근할 수 없다.

2. 각 내부 클래스 간의 접근

class Outer{
    class InstanceInner {}
    static class StaticInner {}
    
    // 1) 인스턴스 멤버끼리 접근 가능
    InstanceInner iv = new InstanceInner(); // OK
    
    // 2) 스태틱 멤버끼리 접근 가능
    StaticInner cv = new StaticInner(); // OK
    
    // 3) 스태틱 멤버는 인스턴스 멤버에 직접 접근 불가
    StaticInner cv = new InstanceInner(); // 에러
    static void sm(){
        InstanceInner ob1 = new InstanceInner(); // 에러
        StaticInner ob2 = new StaticInner(); // OK
        
        // 4) 인스턴스 내부 클래스는 외부 클래스를 먼저 생성해야 생성 가능
        Outer out = new Outer();
        InstanceInner ob1 = out.new InstanceInner();  // OK
    }
    
    // 5) 인스턴스 메서드는 인스턴스 멤버와 스태틱 멤버 모두 접근 가능
    void im(){
        InstanceInner ob1 = new InstanceInner(); // OK
        StaticInner ob2 = new StaticInner(); // OK    
        
        // 6) 지역 내부 클래스는 외부에서 접근 불가
        LocalInner lv = new LocalInner(); // 에러
    }
    
    // 7) 지역 내부 클래스는 해당 메서드 내부에서만 접근 가능
    void im2(){
        class LocalInner {}
        LocalInner lv = new LocalInner(); // OK
    }
}

3. 각 내부 클래스 안에 선언된 변수에 접근 제어자가 있을 때의 접근

class Outer{
    private int outerIv = 0; // Outer클래스 내부에서만 접근 가능
    static int outerCv = 1;

    // 1) 인스턴스 내부 클래스는 외부 클래스의 private 멤버에 접근 가능
    class InstanceInner {
        int iiv1 = outerIv; // OK
        int ii2 = outerCv; // OK
    }

    // 2) 스태틱 내부 클래스는 외부 클래스의 iv에 접근 불가
    static class StaticInner {
        int siv = outerIv; // 에러
        static int scv = outerCv;  // OK
    }

    // 3) 지역 내부 클래스는 외부 클래스의 private 멤버에 접근 가능
    void method(){
        int lv = 2;
        final int LV = 3; // jdk 1.8부터 final 생략 가능
        class LocalInner {
            int liv1 = outerIv; // OK
            int liv2 = outerCv; // OK
            // 4) 외부 클래스의 지역변수의 경우, final이 붙은 것(상수)만 접근 가능
            int liv3 = lv; // 원래는 에러 -> jdk 1.8부터는 에러가 아님
            int liv4 = LV; // OK
        }
    }
}

4)처럼 조건을 붙여놨던 이유는

  - 메서드 내에 선언된 lv는 메서드가 종료됨과 동시에 없어진다.

  - 메서드 내에 선언된 상수는 상수 풀(constant pool)에 저장되기 때문에 메서드가 종료돼도 그 값이 살아있다.

  - 내부 클래스(LocalInner)의 객체가 lv보다 오래 살아있을 가능성이 있기 때문에 lv를 사용할 수 없게 해 둔 것이다.

그런데 jdk 1.8부터 final이 붙어 있지 않는 변수가 메서드 끝까지 값이 바뀌지 않을 경우 이를 상수로 간주하게 되었다. 즉, final이 없지만 final이 있는 것으로 취급하기 때문에 liv3 선언이 에러가 나지 않는 것으로 바뀌었다. 만약 method() 내부가 아래와 같이 바뀐다면 에러가 발생한다. 그렇기 때문에 웬만하면 final을 붙이는 방향으로 코딩하는 것이 좋다.

void method(){
    int lv = 2;
    final int LV = 3; // jdk 1.8부터 final 생략 가능
    lv = 5; // lv 변경
    class LocalInner {
        int liv1 = outerIv; // OK
        int liv2 = outerCv; // OK
        int liv3 = lv; // 에러
        int liv4 = LV; // OK
    }
}

4. 다른 클래스에서 내부 클래스에 접근

class Outer{
    class InstanceInner{
        int iv = 1;
    }

    static class StaticInner{
        int iv = 2;
        static int cv = 3;
    }
    
    void method() {
        class LocalInner{
            int iv = 4;
        }
    }
}

// Test 클래스의 메인 내부
// 1) 외부 클래스의 인스턴스를 생성해야 인스턴스 내부 클래스 객체 생성 가능
Outer out = new Outer();
Outer.InstanceInner ii = out.new InstanceInner();
System.out.println(ii.iv); // 1
		
// 2) 스태틱 내부 클래스 객체는 외부 클래스의 인스턴스 생성 불필요
Outer.StaticInner si = new Outer.StaticInner(); // 외부 클래스 이름 필요
System.out.println(si.iv); // 2
System.out.println(Outer.StaticInner.cv); // 3

  - 애초에 Outer 클래스에서만 쓰려고 만든 내부 클래스인데, 다른 클래스(Test)에서 쓰려니까 사용법 자체가 불편하다.

  - 위의 코드를 컴파일하면 Test.class, Outer.class, Outer$InstanceInner.class, Outer$StaticInner.class, Outer$1LocalInner.class의 클래스 파일 5개가 생성된다.

  - 여기서 $가 붙은 것은 내부 클래스라는 뜻이다.

  - $와 지역 내부 클래스 이름 사이에는 숫자가 붙는데, 이는 다른 메서드에 존재하는 같은 이름을 가진 지역 내부 클래스와 구분 짓기 위한 것이다.

5. 변수 이름이 같은데 선언 위치가 다를 때의 접근

class Outer {
    int value = 1;	// 외부 클래스의 iv
    class Inner {
        int value = 2;   // 내부 클래스의 iv
        void method() {
            int value = 3;
            System.out.println(value + ", " + this.value + ", " + Outer.this.value);
        }
    }
}

// Test 클래스의 메인 내부
Outer out = new Outer();
Outer.Inner inn = out.new Inner();
inn.method(); // 3, 2, 1

  - inn이 Inner 클래스의 객체이기 때문에 Inner 클래스를 기준으로 value가 결정된다.

  - 첫 번째 value의 값: 메서드 내의 lv인 3

  - 두 번째 value의 값: Inner 클래스의 iv인 3

  - 세 번째 value의 값: Outer 클래스의 iv인 1

 

[쿠키글] 그래서.. 아까 그 문제의 답은..?

class A{ // B의 외부 클래스
    int i = 5;
    B b = new B();

    class B{ // A의 내부 클래스
        void method() {
            System.out.println(i);
        }
    }
}

// 메인 내부
// 일단 외부 클래스의 객체 생성
A a = new A();

// 1) A 클래스 내부에 iv로 선언된 B 객체 이용
a.b.method(); // 5

// 2) B 객체를 만들어서 메서드 호출
A.B b = a.new B();
b.method(); // 5

// 3) 2)를 한 줄로 만들기
a.new B().method(); // 5

'Java' 카테고리의 다른 글

프로그램 오류의 종류와 예외 처리  (0) 2022.05.19
익명 클래스  (0) 2022.05.19
디폴트 메서드  (0) 2022.05.15
인터페이스의 장점  (0) 2022.05.13
인터페이스 선언, 상속, 구현  (0) 2022.05.13