Programming/Java

자바 기초부터 모던 자바까지 - 내부&중첩 클래스편

KOOCCI 2022. 7. 2. 15:57

목표 : 자바의 내부&중첩 클래스에 대해 알아본다.


Inner Class

클래스에 전반적인 내용을 모두 적진 않을 것이다.

객체 지향의 거대한 3요소인 상속, 다형성, 캡슐화 등에 대해서는 너무나도 많은 자료가 있으니 별도로 알아보도록 하자.

지금 알아볼 것은 Class안의 Class의 동작에 대해 알아볼 에정이다.

 

Inner Class(내부 클래스)중첩 클래스라고도 표현하는 사람도 많지만, 약간 다르게 설명하는 경우도 있다.

중첩 클래스는 클래스나 인터페이스 내부에 static으로 선언된 클래스다.

 

포스팅을 위해 참고한 자료에서도 중첩 클래스는 내부 클래스와 동일 시하여 표현하고 있어, 하기 내용이 조금 헤깔릴 수 있으나 명확하게 구분하는 사람도 있으니 참고하도록 하자.

 

클래스 내부에 선언한 클래스로 이 클래스를 감싸고 있는 외부 클래스와 밀접한 연관이 있는 경우가 많고, 다른 외부 클래스에서 사용할 일이 거의 없는 경우에 내부 클래스로 선언해서 사용하기도 한다.

 

그 종류로는 4가지 정도로 구분될 수 있다.

  • 인스턴스 내부 클래스
  • 정적(static) 내부 클래스
  • 지역(local) 내부 클래스
  • 익명(anonymous) 내부 클래스

한가지씩 알아보도록 하자.

인스턴스 내부 클래스

내부적으로 사용할 클래스를 선언하는 것이다.

내부적으로 사용하다 보니, private로 선언하는 것을 권장한다.

외부 클래스가 생성된 후에 생성되며, public으로 선언하면 외부 클래스에서도 호출을 할 수 있게 된다.

아래에 예시 코드는 static 변수나 메서드를 함께 확인해볼 것이다.

어떤 차이가 있는지 비교해보면서 보도록 하자.

class OuterClass {
    private int num = 10;
    private static int sNum = 20;
    private InnerClass innerClass;

    public OuterClass(){
        innerClass = new InnerClass(); // 내부 클래스 생성
    }
    class InnerClass {
        int inNum = 100;
        //        static int staticInNum = 200; // 에러남
        void inTest() {
            System.out.println("OuterClass num = " + num + "(외부 클래스의 인스턴스 변수)");
            System.out.println("OuterClass sNum = " + sNum + "(외부 클래스의 스태틱 변수)");
            System.out.println("InnerClass inNum = " + inNum + "(내부 클래스의 인스턴스 변수)");
        }
    }

    public void usingClass() {
        innerClass.inTest();
    }
}


public class Main {
    public static void main(String[] args) {
        OuterClass outClass = new OuterClass();
        outClass.usingClass(); // inClass의 inTest가 실행

        // private가 아니면 외부 실행 가능
        OuterClass.InnerClass inner = outClass.new InnerClass(); // 만들 수 있지만, 사용하길 지양해야 함
        inner.inTest();

    }
}

그 결과는 아래와 같이 출력된다

더보기

OuterClass num = 10(외부 클래스의 인스턴스 변수)
OuterClass sNum = 20(외부 클래스의 스태틱 변수)
InnerClass inNum = 100(내부 클래스의 인스턴스 변수)
OuterClass num = 10(외부 클래스의 인스턴스 변수)
OuterClass sNum = 20(외부 클래스의 스태틱 변수)
InnerClass inNum = 100(내부 클래스의 인스턴스 변수)

위 예시에서 보면, InnerClass내의 static 변수는 선언할 수 없다.

앞서 말했지만, OuterClass가 먼저 생성되고 InnerClass가 생성되게 된다. (생성자로 InnerClass를 생성해야 한다)

static 변수는 OuterClass랑 상관없이 사용하게 되버린다. (메소드도 마찬가지다) (앞선 포스팅을 확인하자)

 

정적 내부 클래스 (Static Inner Class)

내부 클래스 선언에 static을 붙여보자.

외부 클래스와 무관하게 사용할 수 있다. (static 변수, static 메소드 사용)

class OuterClass {
    private int num = 10;
    private static int sNum = 20;

    static class InnerStaticClass {
        int inNum = 100;
        static int sInNum = 200;

        void inTest() {
//            num ++; // 에러남
            sNum ++;
            inNum ++;
            sInNum ++;
//            System.out.println("OuterClass num = " + num + "(외부 클래스의 인스턴스 변수)"); // 에러남
            System.out.println("OuterClass sNum = " + sNum + "(외부 클래스의 스태틱 변수)");
            System.out.println("InStaticClass inNum = " + inNum + "(내부 클래스의 인스턴스 변수 사용)");
            System.out.println("InStaticClass sInNum = " + sInNum + "(내부 클래스의 스태틱 변수 사용)");
        }

        static void sTest() {
            //num += 10;   // 외부 클래스의 인스턴스 변수는 사용할 수 없음.
            //inNum += 10; // 내부 클래스의 인스턴스 변수는 사용할 수 없음.
            sInNum += 10;

//            System.out.println("OuterClass num = " + num + "(외부 클래스의 인스턴스 변수)"); // 에러남
            System.out.println("OuterClass sNum = " + sNum + "(외부 클래스의 스태틱 변수)");
//            System.out.println("InStaticClass inNum = " + inNum + "(내부 클래스의 인스턴스 변수 사용)"); // 에러남
            System.out.println("InStaticClass sInNum = " + sInNum + "(내부 클래스의 스태틱 변수 사용)");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        //외부 클래스 생성하지 않고 바로 정적 내부 클래스 생성
        OuterClass.InnerStaticClass sInClass = new OuterClass.InnerStaticClass();
        System.out.println("정적 내부 클래스 일반 메서드 호출");
        sInClass.inTest();
        System.out.println();
        System.out.println("정적 내부 클래스의 스태틱 메소드 호출");
        OuterClass.InnerStaticClass.sTest();
    }
}
더보기

정적 내부 클래스 일반 메서드 호출
OuterClass sNum = 21(외부 클래스의 스태틱 변수)
InStaticClass inNum = 101(내부 클래스의 인스턴스 변수 사용)
InStaticClass sInNum = 201(내부 클래스의 스태틱 변수 사용)

정적 내부 클래스의 스태틱 메소드 호출
OuterClass sNum = 21(외부 클래스의 스태틱 변수)
InStaticClass sInNum = 211(내부 클래스의 스태틱 변수 사용)

static 이라는 용어의 성질을 알면 에러나는 이유를 유추할 수 있을 것이다.

언제 메모리에 로드되는지 잘 생각해보자.

외부의 멤버 변수는 당연히 호출할 수 없고, 자신의 멤버변수라고 하더라도, static 메소드에서는 호출 할 수 없다.

 

지역 내부 클래스

지역 변수와 같이 메서드 내부에서 정의한다.

메서드의 호출이 끝나면 메서드에 사용된 지역변수의 유효성은 사라진다.

메서드 호출 이후에도 사용해야 하는 경우가 있을 수 있으므로 지역 내부 클래스에서 사용하는 메서드의 지역 변수나 매개 변수는

final로 선언된다.

하기의 Runnable을 활용한 예시를 보도록 하자.

class OuterClass {
    private int outNum = 10;
    private static int sNum = 20;

    Runnable getRunnable(int i) {
        int num = 100; // 로컬 변수 = stack 메모리

        class MyRunnable implements Runnable {
            int localNum = 10; // 클래스 멤버 변수

            @Override
            public void run() {
//                num = 200; // 에러남. 지역변수는 상수로 바뀐다.
//                i = 100; // 에러남. 매개 변수 역시 지역변수처럼 상수로 바뀐다.
                outNum ++;
                sNum ++;

                System.out.println("i = " + i);
                System.out.println("num = " + num);
                System.out.println("localNum = " + localNum);

                System.out.println("outNum = " + outNum + "(외부 클래스 인스턴스 변수)");
                System.out.println("sNum = " + OuterClass.sNum + "(외부 클래스 정적 변수)");
            }
        }
        return new MyRunnable();
    }

}

public class Main {
    public static void main(String[] args) {
        OuterClass out = new OuterClass();
        Runnable runner = out.getRunnable(10);
        runner.run();
    }
}
더보기

i = 10
num = 100
localNum = 10
outNum = 11(외부 클래스 인스턴스 변수)
sNum = 21(외부 클래스 정적 변수)

getRunnable함수 자체는 생성되고 사라지지만, run함수는 다시 호출될 수 있다.

stack메모리에 잡히면 쓸 수 없으므로, final처리가 된다.

 

익명 내부 클래스

이름이 없는 클래스다. (위 지역 내부 클래스의 MyRunnable 클래스 이름은 실제로 호출되는 경우가 없다)

클래스의 이름을 생략하고 주로 하나의 인터페이스나 하나의 추상 클래스를 구현하여 반환한다.

인터페이스나 추상 클래스 자료형의 변수에 직접 대입하여 클래스를 생성하거나 지역 내부 클래스의 메서드 내부에서 생성하여 반환 할 수 있다.

안드로이드 widget의 이벤트 핸들러에 활용된다.

 

 

class OuterClass {
    private int outNum = 10;
    private static int sNum = 20;

    Runnable getRunnable(int i) {
        int num = 100; // 로컬 변수 = stack 메모리

        return new Runnable() {
            int localNum = 10; // 클래스 멤버 변수

            @Override
            public void run() {
//                num = 200; // 에러남. 지역변수는 상수로 바뀐다.
//                i = 100; // 에러남. 매개 변수 역시 지역변수처럼 상수로 바뀐다.
                outNum ++;
                sNum ++;

                System.out.println("i = " + i);
                System.out.println("num = " + num);
                System.out.println("localNum = " + localNum);

                System.out.println("outNum = " + outNum + "(외부 클래스 인스턴스 변수)");
                System.out.println("sNum = " + OuterClass.sNum + "(외부 클래스 정적 변수)");
            }
        };
    }

    Runnable runner = new Runnable() {
        @Override
        public void run() {
            System.out.println("Runnable 이 구현된 익명 클래스 변수");
        }
    };

}

public class Main {
    public static void main(String[] args) {
        OuterClass out = new OuterClass();
        Runnable runnable = out.getRunnable(10);
        runnable.run();
        out.runner.run();
    }
}
더보기

i = 10
num = 100
localNum = 10
outNum = 11(외부 클래스 인스턴스 변수)
sNum = 21(외부 클래스 정적 변수)
Runnable 이 구현된 익명 클래스 변수

이렇게 inner class에 대해 간단히 알아보았다.