목차
<내부 클래스 정의와 유형>
'내부 클래스(inner class)'는 말 그대로 '클래스 내부에 선언한 클래스'다.
내부에 클래스를 선언하는 이유는 대개 이 클래스와 외부 클래스가 밀접한 관련이 있기 때문. 또한 그 밖의 다른 클래스와 협력할 일이 없는 경우에 내부 클래스로 선언해서 사용한다.
내부 클래스를 간단히 표현하면 아래 그림과 같다.
내부 클래스는 선언하는 위치나 예약어에 따라 크게 네가지 유형으로 나누어진다.
- 인스턴스 내부 클래스
- 정적(static) 내부 클래스
- 지역(local) 내부 클래스
먼저 위 세 가지는 클래스 내부에 선언하는 변수의 유형(인스턴스, 저억, 지역 변수)과 유사하다.
마지막으로 클래스 이름 없이 선언하고 바로 생성하여 사용할 수 있는
- 익명(anonymous) 내부 클래스
가 있다.
그럼 변수 유형과 내부 클래스 유형을 비교하여 살펴보자.
[변수]
class ABC {
int n1; //인스턴스 변수
static int n2; //정적 변수
public void abc(){
int i; //지역 변수
}
}
[내부 클래스]
Class ABC { //외부 클래스
class In { //인스턴스 내부 클래스
static class Sin { //정적 내부 클래스
}
}
public void abc() {
class Local { //지역 내부 클래스
}
}
}
두 번째 코드를 보면,
- 가장 바깥에 선언한 ABC 클래스를 '외부 클래스',
ABC 클래스 내부에 선언한 클래스를 '내부 클래스 또는 중첩된 클래스'라고 한다. - 내부 클래스는 멤버 변수처럼 클래스 내부에 정의하는 '인스턴스 내부 클래스'
- static 키워드를 사용하는 '정적 내부 클래스'
- 메서드 내부에 정의하는 '지역 내부 클래스'
로 클래스를 나눌 수 있다. 이 코드에 사용하지 않은 익명 클래스는 나중에 예를 보면서 설명하겠다.
'내부 클래스'는 유형에 따라 만드는 방법이 다를뿐더러 클래스 내부에 선언할 수 있는 변수 유형과 사용할 수 있는 외부 클래스 변수 유형도 다르다.
그러면 각 유형 별로 확인해보도록 하자.
<인스턴스 내부 클래스>
'인스턴스 내부 클래스(instance inner class)'는 인스턴스 변수를 선언할 때와 같은 위치에 선언하며, 외부 클래스 내부에서만 생성하여 사용하는 객체를 선언할 때 쓴다.
예를 들어 어떤 클래스 내에 여러 변수가 있고 이들 변수 중 일부를 모아 클래스로 표현할 수 있다.
이 클래스를 다른 외부 클래스에서 사용할 일이 없는 경우 '내부 인스턴스 클래스'로 정의한다.
'인스턴스 내부 클래스'는 외부 클래스가 생성된 후 생성된다. 따라서 외부 클래스를 먼저 생성하지 않고 인스턴스 내부 클래스를 사용할 수 없다. (이 점은 '정적 내부 클래스'와 반대)
간단한 예제를 보며 확인해보자.
OutClass 외부 클래스를 생성하고, 그 안에 InClass 인스턴스 내부 클래스를 선언하자.
package innerclass;
class OutClass { //외부 클래스
private int num = 10; //외부 클래스의 private 변수
private static int sNum = 20; //외부 클래스 정적(static) 변수
private InClass inClass; //내부 클래스 자료형인 변수를 먼저 선언
/*외부 클래스 디폴트 생성자.
외부 클래스가 생성된 후에 내부 클래스 생성*/
public OutClass() {
inClass = new InClass();
}
//인스턴스 내부 클래스
class InClass {
int inNum = 100; //내부 클래스의 인스턴스 변수
//*오류발생* 인스턴스 내부 클래스에 정적 변수 선언 불가능. (주석처리)
//static int sInNum = 200;
void inTest() {
System.out.println("OutClass num = " + num + "(외부 클래스의 인스턴스 변수)");
System.out.println("OutClass snum = " + sNum + "(외부 클래스의 정적 변수)");
}
//*오류발생* 정적 메서드 역시 정의 불가능. (주석처리)
/* static void sTest() {
} */
}
public void usingClass() {
inClass.inTest();
}
}
public class InnerTest {
public static void main(String[] args) {
OutClass outClass = new OutClass();
System.out.println("외부 클래스 이용하여 내부 클래스 기능 호출");
outClass.usingClass(); //내부 클래스 기능 호출
}
}
OutClass 외부 클래스를 만들고 내부 클래스로 InClass를 선언했다.
외부 클래스를 먼저 생성해야 내부 클래스를 사용할 수 있다. 그래서 내부 클래스 자료형 변수를 먼저 선언만 한 뒤, OutClass 생성자에서 내부 클래스를 생성한다.
인스턴스 내부 클래스에서의 변수와 메서드
'인스턴스 내부 클래스'에서는 어떤 유형의 변수를 선언하고 사용할 수 있는지, 외부 클래스 변수는 어떻게 활용하는지 보자.
- 외부 클래스 안에 private 예약어로 변수 num과 정적 변수 sNum을 선언했다.
이 두 변수는 private로 선언했지만 외부 클래스 안에 있기 때문에 내부 클래스에서도 당연히 사용할 수 있다.
내부 클래스 안에 정의한 inTest() 메서드에서 변수 num과 sNum을 사용한다. - 내부 클래스 InClass 안에 인스턴스 변수 inNum과 정적 변수 sInNum을 선언했는데 정적 변수 부분에 오류가 발생한다. 인스턴스 내부 클래스는 외부 클래스를 생성한 이후에 사용해야 하기 때문.
따라서 클래스의 생성과 상관없이 사용할 수 있는 정적 변수는 인스턴스 내부 클래스에서 선언이 불가능하다. - 정적 메서드도 마찬가지 이유로 인스턴스 내부 클래스에 선언이 불가능하다.
- 외부 클래스의 usingClass() 메서드에서 내부 클래스의 inTest()메서드를 사용할 수 있는 것을 볼 수 있다.
정리하면 '인스턴스 내부 클래스'는 외부 클래스가 먼저 생성되어야 사용이 가능하다.
그리고 '인스턴스 내부 클래스의 메서드'는 외부 클래스의 메서드가 호출될 때 사용할 수 있다.
다른 클래스에서 인스턴스 내부 클래스 생성
내부 클래스를 생성하는 이유는 그 클래스를 감싸고 있는 외부 클래스에서만 사용하기 위해서다.
그러므로 내부 클래스를 그 밖의 다른 클래스에서 생성해서 사용하는 것은 사실 맞지 않다. 하지만 외부 클래스 외의 다른 클래스에서 private이 아닌 내부 클래스를 생성하는 것이 문법적으로 가능하기는 하다.
일반적인 인스턴스 내부 클래스 사용 방법은 앞서 본 예제와 같다.
OutClass 클래스를 생성하고 인스턴스 변수를 이용하여 outClass.usingClass(); 문장으로 내부 클래스 기능을 호출하는 것이다.
이때 만약 내부 클래스를 private으로 선언하지 않았다면 외부 클래스가 아닌 다른 클래스에서도 다음처럼 내부 클래스를 생성할 수 있다.
OutClass outClass = new OutClass();
OutClass.InClass inclass = outClass.new InClass();
먼저 OutClass를 만들고, 생성한 참조 변수를 사용하여 내부 클래스를 생성한다.
내부 클래스를 private으로 선언했다면 다른 클래스에서 InClass를 사용할 수 없다. 따라서 어떤 클래스의 내부에서만 사용할 목적이라면 내부 클래스를 private으로 선언한다.
<정적 내부 클래스>
인스턴스 내부 클래스는 외부 클래스가 먼저 생성되어야 생성할 수 있기 때문에 정적 변수나 정적 메서드를 사용할 수 없다.
그런데 내부 클래스가 외부 클래스 생성과 무관하게 사용할 수 있어야 하고 정적 변수도 사용할 수 있어야 한다면,
'정적 내부 클래스'를 사용하면 된다.
정적 내부 클래스는 인스턴스 내부 클래스처럼 외부 클래스의 멤버 변수와 같은 위치에 정의하며 static 예약어를 함께 쓴다.
다음 예제에서 기존에 만들어 둔 외부 클래스에 정적 내부 클래스를 정의하고, InnerTest 클래스에 테스트 코드를 추가해보자.
package innerclass;
class OutClass {
private int num = 10;
private static int sNum = 20;
//정적 내부 클래스
static class InStaticClass{
int inNum = 100; //정적 내부 클래스의 인스턴스 변수
static int sInNum = 200; //정적 내부 클래스의 정적 변수
//정적 내부 클래스의 일반 메서드
void inTest() {
//num += 10; //외부 클래스의 인스턴스 변수는 사용할 수 없으므로 주석처리
System.out.println("InStaticClass inNum = " + inNum + "(내부 클래스의 인스턴스 변수 사용)");
System.out.println("InStaticClass sInNum = " + sInNum + "(내부 클래스의 인스턴스 변수 사용)");
System.out.println("OutClass sNum = " + sNum + "(외부 클래스의 정적 변수 사용)");
}
//정적 내부 클래스의 정적 메서드
static void sTest() {
//num += 10; //
//inNum += 10; //외부 클래스와 내부 클래스의 인스턴스 변수는 사용할 수 없으므로 주석처리
System.out.println("OutClass sNum = " + sNum + "(외부 클래스의 정적 변수 사용)");
System.out.println("InStaticClass sInNum = " + sInNum + "(내부 클래스의 정적 변수 사용)");
}
}
}
public class InnerTest {
public static void main(String[] args) {
//외부 클래스를 생성하지 않고 바로 정적 내부 클래스 생성 가능.
OutClass.InStaticClass sInClass = new OutClass.InStaticClass();
System.out.println("정적 내부 클래스 일반 메서드 호출");
sInClass.inTest();
System.out.println();
System.out.println("정적 내부 클래스의 정적 메서드 호출");
OutClass.InStaticClass.sTest();
}
}
- 정적 내부 클래스(InStaticClass)를 보면, 인스턴스 변수 inNum과 정적 변수 sInNum이 있고,
일반 메서드 inTest()와 정적 메서드 sTest()가 있다. - 앞서 배웠듯이 정적 메서드에서는 인스턴스 변수를 사용할 수 없다.
따라서 정적 내부 클래스에서도 외부 클래스의 인스턴스 변수는 사용할 수 없다.
정리하면.
정적 내부 클래스 메서드 | 변수 유형 | 사용 가능 여부 |
일반 메서드 void inTest() |
외부 클래스의 인스턴스 변수 (num) | X |
외부 클래스의 정적 변수 (sNum) | O | |
정적 내부 클래스의 인스턴스 변수 (inNum) | O | |
정적 내부 클래스의 정적 변수 (sInNum) | O | |
정적 메서드 static void sTest() |
외부 클래스의 인스턴스 변수 (num) | X |
외부 클래스의 정적 변수 (sNum) | O | |
정적 내부 클래스의 인스턴스 변수 (inNum) | X | |
정적 내부 클래스의 정적 변수 (sInNum) | O |
예제와 표에서 알 수 있듯
'정적 내부 클래스'에서 사용하는 메서드가 정적 메서드인 경우에는 외부 클래스와 정적 내부 클래스에 선언된 변수 중 정적 변수만 사용할 수 있다.
다른 클래스에서 정적 내부 클래스 사용
정적 내부 클래스는 외부 클래스를 생성하지 않고도 내부 클래스 자료형으로 바로 선언하여 생성할 수 있다.
OutClass.InStaticClass sInClass = new OutClass.InStaticClass();
또 정적 내부 클래스에 선언한 메서드(정적 메서드 포함)나 변수는 private가 아닌 경우에는 다른 클래스에서도 바로 사용할 수 있다.
OutClass.InStaticClass.sTest();
따라서 내부 클래스를 만들고 외부 클래스와 무관하게 다른 클래스에서도 사용하려면 정적 내부 클래스로 생성하면 된다.
하지만 정적 내부 클래스를 private로 선언했다면 이것 역시 다른 클래스에서 사용할 수 없다.
<지역 내부 클래스>
'지역 내부 클래스'는 지역 변수처럼 메서드 내부에 클래스를 정의하여 사용하는 것을 말한다.
따라서 이 클래스는 메서드 안에서만 사용할 수 있다.
다음은 Runnable 인터페이스를 구현하는 클래스를 지역 내부 클래스로 만든 예제이다.
'Runnable 인터페이스'는 자바에서 스레드를 만들 때 사용하는 인터페이스로 java.lang 패키지에 선언되어 있으며 반드시 run() 메서드를 구현해야 한다.
package innerclass;
class Outer{
int outNum = 100;
static int sNum = 200;
Runnable getRunnable(int i) {
int num = 100; //지역 변수
//지역 내부 클래스
class MyRunnable implements Runnable {
int localNum = 10; //지역 내부 클래스의 인스턴스 변수
@Override
public void run() {
//num = 200; //지역 변수는 상수로 바뀌므로 값을 변경X
//i = 100; //매개변수도 지역 변수처럼 상수로 바뀌므로 값을 변경X
System.out.println("i = " + i);
System.out.println("num = " + num);
System.out.println("localNum = " + localNum);
System.out.println("outNum = " + outNum + "(외부 클래스 인스턴스 변수");
System.out.println("Outer.sNum = " + Outer.sNum + "(외부 클래스 정적 변수");
}
}
return new MyRunnable();
}
}
public class LocalInnerTest {
public static void main(String[] args) {
Outer out = new Outer();
Runnable runner = out.getRunnable(10); //메서드 호출
runner.run();
}
}
- getRunnable() 메서드를 보면, 이 메서드의 반환형은 Runnable이다. 즉 이 메서드에서는 Runnable 자료형의 객체를 생성하여 반환해야 한다. 그래서 이 메서드 내부에 클래스를 하나 정의한다.
이름은 MyRunnable, Runnable 인터페이스를 구현한 클래스다.
메서드 안에 정의한 MyRunnable 클래스가 바로 '지역 내부 클래스'. - 자바 스레드가 실행될 때 호출되는 run() 메서드를 구현했다.
- 이 메서드에서 Runnable 자료형을 반환해야 하므로 return new Runnable(); 문장으로 MyRunnable 클래스를 생성한 후 반환한다.
- LocalInnerTest 클래스를 확인해보면,
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() 메서드 호출이 끝나고 스택 메모리에서 지워진 변수를 이후에 또 참조할 수 있다는 것이다.
즉 '지역 내부 클래스에서 사용하는 지역 변수는 상수로 처리'된다.
상수를 처리하기 위해 자바 7까지는 final 예약어를 꼭 함께 써주어야 했지만, 자바 8부터 직접 써 주지 않아도 컴파일과정에서 final 예약어가 자동으로 추가된다.
그러므로 num과 i 변수의 값을 다른 값으로 바꾸려고 하면 오류가 발생한다.
정리하면, 지역 내부 클래스에서 사용하는 메서드의 지역 변수는 모두 상수로 바뀐다.
<익명 내부 클래스>
지금까지의 클래스는 모두 이름이 있었다.
그런데 클래스 이름을 사용하지 않는 클래스가 있다. '익명 클래스'
먼저 지역 내부 클래스에서 사용한 코드를 보자.
class Outer{
...
Runnable getRunnable(int i) {
...
//지역 내부 클래스
class MyRunnable implements Runnable {
...
@Override
public void run() {
...
}
}
return new MyRunnable();
//지역 내부 클래스 이름은 클래스를 생성하여 반환할 때만 사용
}
}
지역 내부 클래스 MyRunnable을 선언했지만, 이 클래스 이름을 사용하는 곳은 맨 마지막에 클래스를 생성하여 반환할 때 뿐이다.
그래서 다음 예제처럼 이름을 생략한 Runnable 인터페이스를 바로 생성해서 반환하는 '익명 클래스 형식'으로 새롭게 선언한다.
package innerclass;
class Outer2 {
Runnable getRunnable(int i) {
int num = 100;
//MyRunnable 클래스 이름을 빼고 클래스를 바로 생성하는 방법
return new Runnable() { //익명 내부 클래스. Runnable 인터페이스 생성
@Override
public void run() {
//num = 200;
//i = 10;
System.out.println(i);
System.out.println(num);
}
}; //클래스 끝에 ;를 씀
}
//인터페이스나 추상 클래스형 변수를 선언하고 클래스를 생성해 대입하는 방법
Runnable runner = new Runnable() { //익명 내부 클래스를 변수에 대입
@Override
public void run() {
System.out.println("Runnable이 구현된 익명 클래스 변수");
}
}; //클래스 끝에 ;를 씀
}
public class AnonymousInnerTest {
public static void main(String[] args) {
Outer2 out = new Outer2();
Runnable runnable = out.getRunnable(10);
runnable.run();
out.runner.run();
}
}
- '익명 내부 클래스'는 단 하나의 인터페이스 또는 단 하나의 추상 클래스를 바로 생성할 수 있다.
그런데 앞에서 인터페이스는 인스턴스로 생성할 수 없다고 했다.
Runnable 인터페이스를 생성할 수 있으려면 인터페이스 몸체가 필요한데, Runnable 인터페이스에서 반드시 구현해야 하는 'run() 메서드'가 포함되어 있다.
마지막에 세미콜론(;)을 사용해서 익명 내부 클래스가 끝났다는 것을 알려준다. - '익명 내부 클래스'는 인터페이스나 추상 클래스 자료형으로 변수를 선언한 후 내부 클래스를 생성해 대입할 수도 있다.
여기에 마찬가지로 추상 메서드나 인터페이스를 구현한 후 세미콜론으로 클래스 끝을 나타낸다. - 마지막으로 익명 클래스를 사용하는 Test 코드 부분의 사용 방법은 지역 내부 클래스와 동일하다.
즉 Runnable 인터페이스 자료형으로 변수를 선언하고, 인터페이스의 익명 내부 클래스가 구현된 메서드를 호출하면 인스턴스를 반환한다.
그리고 runnable.run() 또는 out.runner.run()으로 인터페이스의 메서드를 호출할 수 있다.
정리하면 '익명 내부 클래스'는
변수에 직접 대입하는 경우도 있고,
메서드 내부에서 인터페이스나 추상 클래스를 구현하는 경우도 있다.
이때 사용하는 지역 변수는 상수화되므로 메서드 호출이 끝난 후에도 사용할 수 있다.
익명 내부 클래스의 쓰임새
익명 내부 클래스는 예전 자바 UI에서 이벤트를 처리하는 데 많이 사용되었다. 현재는 안드로이드 프로그래밍에서 위젯의 이벤트를 처리하는 핸들러를 구현할 때 사용한다.
안드로이드에서 사용하는 버튼, 텍스트 상자 등을 위젯(widget)이라고 하는데, 위젯은 사용자가 터치하거나 키 값을 입력하면 이벤트를 일으킨다.
이때 발생한 이벤트를 처리해 주는 코드를 이벤트 핸들러라고 한다.
앞서 말했듯 안드로이드 이벤트 핸들러는 대부분 익명 내부 클래스로 구현된다.
다음은 버튼을 눌렀을 때 'hello' 메시지를 하나 띄우는 코드다.
button1.setOnClickListener(new View.OnClickListener() {
public boolean onClick(View v) {
Toast.makeText(getBaseContext(), "hello", Toast.LENGTH_LONG).show();
return true;
}
});
버튼을 누르면 발생하는 이벤트 핸들러는 new View.OnClickListener()이다.
그리고 이 핸들러의 구현 메서드는 onClick이다.
new부터 시작된 익명 내부 클래스는 맨 마지막 세미콜론(;)에서 구현을 마친다.
이와 같이 안드로이드 위젯의 이벤트 핸들러를 익명 내부 클래스로 구현한다.
정리:
지금까지 배운 내부 클래스 내용을 표로 정리하면,
종류 | 구현 위치 | 사용할 수 있는 외부 클래스 변수 |
생성 방법 |
인스턴스 내부 클래스 | -외부 클래스 멤버 변수와 동일 | -외부 인스턴스 변수 -외부 전역 변수 |
외부 클래스를 먼저 만든 후 내부 클래스 생성 |
정적 내부 클래스 | -외부 클래스 멤버 변수와 동일 | -외부 전역 변수 | 외부 클래스와 무관하게 생성 |
지역 내부 클래스 | -메서드 내부에 구현 | -외부 인스턴스 변수 -외부 전역 변수 |
메서드를 호출할 때 생성 |
익명 내부 클래스 | -메서드 내부에 구현 -변수에 대입하여 직접 구현 |
-외부 인스턴스 변수 -외부 전역 변수 |
-메서드를 호출할 때 생성 -인터페이스 타입 변수에 대입할 때 new 예약어를 사용하여 생성 |
전역 변수 (global variable) : 프로그램 전체에서 사용 가능한 변수 ( ↔ 지역변수 )
지역 변수 (local variable) : 블록 내에서 선언되고 블록 내에서만 사용 가능한 변수. 처리가 끝난 후에는 사용X