지난 글: [프로그래밍 언어/JAVA] - JAVA 입문 - Map 인터페이스
내부 클래스 정의와 유형
내부 클래스(inner class)는 말 그대로 '클래스 내부에 선언한 클래스'이다. 내부에 클래스를 선언하는 이유는 대개 이 클래스와 외부 클래스가 밀접한 관련이 있기 때문이다. 또한 그 밖의 다른 클래스와 협력할 일이 없는 경우에 내부 클래스로 선언해서 사용한다.
내부 클래스를 간단하게 표현하면 아래와 같다.
class Out { //외부 클래스
class In { //내부 클래스
...
}
}
내부 클래스는 선언하는 위치나 예약어에 따라 크게 네 가지 유형으로 나눌 수 있다. 먼저 인스턴스 내부 클래스, 정적(static) 내부 클래스, 지역(local) 내부 클래스가 있는데, 이들은 클래스 내부에 선언하는 변수의 유형(인스턴스 변수, 정적 변수, 지역 변수)과 유사하다. 마지막으로 클래스 이름 없이 선언하고 바로 생성하여 사용할 수 있는 익명(anonymous) 내부 클래스가 있다. 변수 유형과 내부 클래스 유형을 비교하여 살펴보자.
위 표의 오른쪽 코드를 보면 가장 바깥에 선언한 ABC 클래스를 외부 클래스, ABC 클래스 내부에 선언한 클래스를 내부 클래스 또는 중첩된 클래스라고 한다. 또 내부 클래스는 멤버 변수를 클래스처럼 내부에 정의하는 인스턴스 내부 클래스, static 키워드를 사용하는 정적 내부 클래스, 그리고 메서드 내부에 정의하는 지역 내부 클래스로 나눌 수 있다. 이 코드에 사용하지 않은 익명 클래스는 잠시 후에 배우자. 내부 클래스는 유형에 따라 만드는 방법도 다르고 클래스 내부에 선언할 수 있는 변수 유형과 사용할 수 있는 외부 클래스 변수 유형도 다르다. 각 유형의 예를 보며 내부 클래스 선언 방법과 외부 클래스와의 관계 그리고 생성 방법을 알아보자.
인스턴스 내부 클래스
인스턴스 내부 클래스(instance inner class)는 인스턴스 변수를 선언할 때와 같은 위치에 선언하며, 외부 클래스 내부에서만 생성하여 사용하는 객체를 선언할 때 사용한다. 예로 어떤 클래스 내에 여러 변수가 있고 이들 변수 중 일부를 모아 클래스로 표현할 수 있다. 이 클래스를 다른 외부 클래스에서 사용할 일이 없는 경우 내부 인스턴스 클래스로 정의한다. 인스턴스 내부 클래스는 외부 클래스 생성후 생성된다. 따라서 외부 클래스를 먼저 생성하지 않고 인스턴스 내부 클래스를 사용할 수는 없다.
간단한 예제와 함께 문법을 보자. 먼저 OutClass 외부 클래스를 만들고, 그 안에 InClass 인스턴스 내부 클래스를 선언한다.
인스턴스 내부 클래스에서 사용하는 변수와 메서드
외부 클래스 안에 private 예약어로 변수 num과 정적 변수 sNum을 선언했다. 이 두 변수는 private으로 선언했지만 외부 클래스 안에 있기에 당연히 내부 클래스에서도 사용할 수 있다. 17~20행 inTest( ) 메서드에서 변수 num돠 sNum을 사용한다. 14~15행 내부 클래스 InClass 안에 인스턴스 변수 inNum과 정적 변수 sInNum을 선언했는데 정적 변수 부분에서는 오류가 난다. 인스턴스 내부 클래스는 외부 클래스를 생성한 이후에 사용해야 하기 때문이다. 따라서 클래스의 생성과 상관없이 사용할 수 있는 정적 변수는 인스턴스 내부 클래스에서 선언할 수 없다. 마찬가지로 정적 메서드도 인스턴스 내부에 선언할 수 없다. 25~27행을 보면 외부 클래스의 usingClass( ) 메서드에서 내부 클래스의 inTest( ) 메서드를 사용하는 것을 알 수 있다.
정리하면 인스턴스 내부 클래스는 외부 클래스가 먼저 생성되어야 사용할 수 있고, 인스턴스 내부 클래스의 메서드는 외부 클래스의 메서드가 호출될 때 사용할 수 있다.
다른 클래스에서 인스턴스 내부 클래스 생성하기
내부 클래스는 그 클래스를 감싸고 있는 외부 클래스에서만 사용하기 위해 만든다. 그러므로 내부 클래스를 그 밖의 다른 클래스에서 생성해서 사용하는 것은 사실 맞지 않다. 하지만 외부 클래스 외의 다른 클래스에서 private이 아닌 내부 클래스를 생성하는 것이 문법적으로는 가능하다. 일반적인 인스턴스 내부 클래스 사용 방법은 위 코드 34행과 같다. OutClass 클래스를 생성하고 인스턴스 변수를 이용하여 outClass.usingClass(); 문장으로 내부 클래스 기능을 호출하는 것이다. 이때 만약 내부 클래스를 private으로 선언하지 않았다면 외부 클래스가 아닌 다른 클래스에서도 다음처럼 내부 클래스를 생성할 수 있다. 먼저 OutClass를 만들고, 생성한 참조 변수를 사용하여 내부 클래스를 생성한다.
OutClass outClass = new OutClass();
OutClass.InClass inClass = outClass.new InClass();
내부 클래스를 private으로 선언했다면 다른 클래스에서 InClass를 사용할 수 없다. 따라서 어떤 클래스의 내부에서만 사용할 목적이라면 내부 클래스를 private으로 선언한다.
정적 내부 클래스
인스턴스 내부 클래스는 외부 클래스가 먼저 생성되어야 생성할 수 있기에 정적 변수나 정적 메서드를 사용할 수 없다. 그런데 내부 클래스가 외부 클래스 생성과 무관하게 사용할 수 있어야 하고, 정적 변수도 사용할 수 있어야 한다면 정적 내부 클래스(static inner class)를 사용하면 된다. 정적 내부 클래스는 인스턴스 내부 클래스처럼 외부 클래스의 멤버 변수와 같은 위치에 정의하며 static 예약어를 함께 사용한다. 만들어둔 외부 클래스(OutClass)에 정적 내부 클래스를 정의하고, InnerTest 클래스에 테스트 코드를 추가해보자.
7~26행 정적 내부 클래스를 보면 인스턴스 변수 inNum과 정적 변수 sInNum이 있고, 일반 메서드 inTest( )와 정적 메서드 sTest( )가 있다. 앞에서도 배웠듯 정적 메서드에서는 인스턴스 변수를 사용할 수 없다.따라서 정적 내부 클래스에서도 외부 클래스의 인스턴스 변수를 사용할 수 없다. 이 내용을 표로 정리하면 다음과 같다.
예제와 표에서 알 수 있듯 정적 내부 클래스에서 사용하는 메서드가 정적 메서드인 경우에는 외부 클래스와 정적 내부 클래스에 선언된 변수 중 정적 변수만 사용할 수 있다.
다른 클래스에서 정적 내부 클래스 사용하기
정적 내부 클래스는 외부 클래스를 생성하지 않고도 내부 클래스 자료형으로 바로 선언하여 생성할 수 있다.
OutClass.InStaticClass sInClass = new OutClass.InStaticClass();
또 정적 내부 클래스에 선언한 메서드(정적 메서드 포함)나 변수는 private이 아닌 경우에 다른 클래스에서도 바로 사용할 수 있다.
OutClass.InStaticClass.sTest();
따라서 내부 클래스를 만들고 외부 클래스와 무관하게 다른 클래스에서도 사용하려면 정적 내부 클래스로 생성하면 된다. 하지만 정적 내부 클래스를 private으로 선언했다면 이것 역시 다른 클래스에서 사용할 수 없다.
지역 내부 클래스
지역 내부 클래스는 지역 변수처럼 메서드 내부에 클래스를 정의하여 사용하는 것이다. 따라서 이 클래스는 메서드 안에서만 사용할 수 있다.
아래는 Runnable 인터페이스를 구현하는 클래스를 지역 내부 클래스로 만든 예제다. Runnable 인터페이스는 자바에서 스레드를 만들 때 사용하는 인터페이스로 java.lang 패키지에 선언되어 있으며 반드시 run( ) 메서드를 구현해야 한다.
7행 getRunnable( ) 메서드의 반환형은 Runnable형이다. 즉 이 메서드에서는 Runnable 자료형의 객체를 생성하여 반환해야 한다. 그래서 이 메서드 내부에 클래스를 하나 정의했다. Runnable 인터페이스를 구현한 MyRunnable이다. 메서드 안에 정의한 MyRunnable 클래스가 지역 내부 클래스이다. 14~22행에 자바 스레드가 실행될 때 호출되는 run( ) 메서드를 구현했다. 이 메서드에서 Runnable 자료형을 반환해야 하므로 return MyRunnable( ); 문장으로 MyRunnable 클래스를 생성한 후 반환한다. MyRunnable 지역 내부 클래스의 사용법은 LocalInnerTest 클래스의 30~32행을 보면, Outer 클래스를 생성한 후 Runnable형 객체로 getRunnable( )을 호출한다.즉 MyRunnable을 사용하려면 이 클래스를 직접 생성하는 것이 아니라 getRunnable( ) 메서드 호출을 통해 생성된 객체를 반환받아야 한다.
지역 내부 클래스에서 지역 변수의 유효성
여기서 변수의 유효성에 대해 잘 살펴봐야 한다. 지역 변수는 메서드가 호출될 때 스택 메모리에 생성되고 메서드의 수행이 끝나면 메모리에서 사라진다. 그런데 지역 내부 클래스에 포함된 getRunnable( ) 메서드의 매개변수 i와 메서드 내부에 선언한 변수 num은 지역 변수이다. 이 두 변수를 사용하는 부분의 코드를 다시 보자.
Outer out = new Outer();
Runnable runner = out.getRunnable(10); //getRunnable() 메서드의 호출이 끝남
runner.run(); //run()이 실행되면서 getRunnable() 메서드의 지역 변수가 사용됨
run( ) 메서드는 getRunnable( ) 메서드의 지역 변수 i와 num을 사용한다. 그런데 지역 내부 클래스를 가지고 있는 getRunnable( ) 메서드 호출이 끝난 후에도 run( ) 메서드가 정상적으로 호출된다. 이는 getRunnable( ) 메서드 호출이 끝나고 스택 메모리에서 지워진 변수를 이후에 또 참조할 수 있다는 것이다. 즉 지역 내부 클래스에서 사용하는 지역 변수는 상수로 처리된다.
익명 내부 클래스
지금까지 만든 클래스는 모두 이름이 있었다. 그런데 클래스 이름을 사용하지 않는 클래스가 있다. 이런 클래스를 익명 클래스라고 부른다. 먼저 지역 내부 클래스에서 사용한 코드를 보자.
지역 내부 클래스 MyRunnable을 선언했지만, 이 클래스 이름을 사용하는 곳은 맨 마지막에 클래스를 생성하여 반환할 때 뿐이다. 그래서 아래 예처럼 이름을 생략한 Runnable 인터페이스를 바로 생성해서 반환하는 익명 클래스 형식으로 새롭게 선언한다.
익명 내부 클래스는 단 하나의 인터페이스 또는 단 하나의 추상 클래스를 바로 생성할 수 있다. 그런데 이전 글에서 인터페이스는 인스턴스로 생성할 수 없다 했다. Runnable 인터페이스를 생성할 수 있으려면 인터페이스 몸체가 필요하다. 9~14행에는 Runnable 인터페이스에서 반드시 구현해야 하는 run( ) 메서드가 포함되어 있다. 마지막에 세미콜론을 사용해서 익명 내부 클래스가 끝났다는 것을 알려준다. 익명 내부 클래스는 19~24행처럼 인터페이스나 추상 클래스 자료형으로 변수를 선언한 후 익명 내부 클래스를 생성해 대입할 수도 있다. 여기에도 마찬가지로 추상 메서드나 인터페이스를 구현한 후 세미콜론으로 클래스 끝을 나타낸다. 마지막으로 29~32행은 익명 클래스를 사용하는 코드이며, 방법은 지역 내부 클래스와 동일하다. 즉 Runnable 인터페이스 자료형으로 변수를 선언하고, 인터페이스의 익명 내부 클래스가 구현된 메서드를 호출하면 인스턴스를 반환한다. 그리고 runnable.run( ) 또는 out,runner.run( ) 으로 인터페이스의 메서드를 호출할 수 있다.
정리하면 익명 내부 클래스는 변수에 직접 대입하는 경우도 있고 메서드 내부에서 인터페이스나 추상 클래스를 구현하는 경우도 있다. 이때 사용하는 지역 변수는 상수화되므로 메서드 호출이 끝난 후에도 사용할 수 있다.
참고 서적: 자바 프로그래밍 입문 - 박은종
'프로그래밍 언어 > JAVA' 카테고리의 다른 글
JAVA 입문 - 스트림 (0) | 2022.06.10 |
---|---|
JAVA 입문 - 람다식 (0) | 2022.06.09 |
JAVA 입문 - Map 인터페이스 (0) | 2022.06.07 |
JAVA 입문 - Set 인터페이스 (0) | 2022.06.06 |
JAVA 입문 - List 인터페이스 (0) | 2022.06.05 |