내부 클래스
내부클래스는 GUI 어플리케이션(AWT, Swings 등)의 이벤트처리 외에는 잘 사용하지 않을 정도로 사용빈도가 높지 않다.
내부 클래스란?
두 클래스가 서로 긴밀한 관계에 있을 때 내부 클래스로 선언한다. 내부 클래스는 외부 클래스를 제외하고는 다른 클래스에서 잘 사용되지 않는 것이어야 한다.
내부 클래스의 장점
- 내부 클래스에서 외부 클래스의 멤버들을 쉽게 접근할 수 있다.
- 코드의 복잡성을 줄일 수 있다.
내부 클래스의 종류와 특징
변수의 선언위치에 따라 변수의 종류와 특징이 달리지는 것처럼. 내부 클래스도 마찬가지이다. 내부 클래스의 유효범위와 성질이 변수와 유사하므로 서로 비교하면서 이해하자
내부 클래스 | 특징 |
---|---|
instance class | 외부 클래스의 멤버변수 선언위치에 선언하며, 외부 클래스의 인스턴스 멤버처럼 다루어진다. 주로 외부 클래스의 인스턴스 멤버들과 관련된 작업에 사용될 목적으로 선언된다. |
static class | 외부 클래스의 멤버변수 선언위치에 선언하며, 외부 클래스의 static 멤버처럼 다루어진다. 주로 외부 클래스의 static 멤버, 특히 static 메서드에서 사용될 목적으로 선언된다. |
local class | 외부 클래스의 메서드나 초기화블럭 안에 선언하며, 선언된 영역 내부에서만 사용될 수 있다. |
anonymous class | 클래스의 선언과 객체의 생성을 동시에 하는 이름없는 클래스(일회용) |
내부 클래스의 선언
변수가 선언된 위치에 따라 인스턴스 필드, 스태틱 필드, 지역변수로 나뉘듯, 내부 클래스도 선언된 위치에 따라 나뉜다. 그리고 각 내부 클래스의 선언위치에 따라 같은 선언위치의 변수와 동일한 유효범위(scope)와 접근성 (accessability)를 갖는다.
내부 클래스의 제어자와 접근성
내부 클래스 중에서 스태틱 클래스만 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
class InnerEx1 {
class InstanceInnter {
int iv = 100;
//static int cv = 100; => 에러
final static int CONST = 100; // => 허용
}
class StaticInner {
int iv = 200;
static int cv = 200;
}
void myMethod() {
class LocalInner {
int iv = 300;
//static int cv = 300; => 에러
final static int CONST = 300; // => 허용
}
}
public static void main(String[] args) {
System.out.println(InstanceInner.CONST); //100
System.out.println(StaticInner.cv); // 200
}
}
final이 붙은 변수는 상수(constant)이기 때문에 어떤 경우라도 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
class InnerEx2 {
class InstanceInner {}
static class StaticInner{}
InstanceInner iv = new InstanceInner();
static StaticInner cv = new StaticInner();
static void staticMethod() {
//static 멤버는 인스턴스 멤버에 직접 접근할 수 없다.
//InstanceInner obj1 = new InstanceInner();
StaticInner obj2 = new StaticInner();
// 굳이 접근하려면 아래와 같이 객체를 생성해야 한다.
// 인스턴스 클래스는 외부 클래스를 먼저 생성해야만 생성할 수 있다.
InnerEx2 outer = new InnerEx2();
InstanceInner obj1 = outer.new InstanceInner();
void instanceMethod() {
// 인스턴스 메서드에서는 인스턴스멤버와 static 멤버 모두 접근 가능하다.
InstanceInner obj1 = new InstanceInner();
StaticInner obj2 = new StaticInner();
// 메서드 내에 지역적으로 선언된 내부 클래스는 외부에서 접근할 수 없다.
//LocalInner lv = new LocaInner();
}
void myMethod() {
class LocalInner {}
LocalInner lv = new LocalInner();
}
}
}
다른 멤버에 접근하기
인스턴스 클래스는 외부 클래스의 인스턴스 멤버이기 때문에 인스턴스 멤버와 static 멤버를 모두 사용할 수 있다.
스태틱 클래스는 외부 클래스의 static 멤버이기 때문에 인스턴스 멤버를 사용할 수 없다. 단지 static 멤버만을 사용할 수 있다.
지역 클래스는 외부 클래스의 인스턴스 멤버와 static 멤버를 모두 사용할 수 있으며, 지역 클래스가 포함된 메서드에 정의된 지역변수도 사용할 수 있다. 단, final이 붙은 지역변수만 접근 가능하다. 즉 로컬 클래스에서 바깥 메서드의 로컬 변수를 사용할 경우에는 값을 조회할 용도로 사용하는 것이다.
인스턴스 클래스는 바깥 클래스의 인스턴스 주소를 저장할 필드가 있다. 생성자의 파라미터로 받은 바깥 클래스의 객체 주소는 컴파일 할 때 자동으로 추가된 인스턴스 필드에 보관된다.
로컬 클래스는 바깥 클래스의 인스턴스 주소를 저장할 필드가 있을 뿐만 아니라 인스턴스 메서드 안에 선언된 로컬 변수의 값을 저장할 필드도 있다.
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
class InnerEx3 {
private int outerIv = 0;
static int outerCv = 0;
class InstanceInner {
int iiv = outerIv; // 외부 클래스의 private 멤버에도 접근 가능
int iiv2 = outerCv;
}
static class StaticInner {
// 스태틱 클래스는 외부 클래스의 인스턴스 멤버에 접근할 수 없다.
int siv = outerIv;
static int scv = outerCv;
}
void myMethod() {
int lv = 0;
final int LV = 0;
int lv2 = 0;
lv2 = 1;
class LocalInner {
int liv = outerIv;
int liv2 = outerCv;?
int liv3 = lv;
int liv4 = LV;
// 값을 여러번 할당한 경우에는 접근할 수 없다.
//int liv5 = lv2; => 에러!
}
}
}
로컬 클래스가 로컬 변수를 조회용으로만 사용하는 이유
메서드가 수행을 마쳐서 지역변수가 소멸된 시점에도, 지역 클래스의 인스턴스가 소멸된 지역변수를 참조하려는 경우가 발생할 수 있기 때문이다. JDK 1.8부터 지역 클래스에서 접근하는 지역변수 앞에 final을 생략할 수 있기 바뀌었다. 대신 컴파일러가 자동으로 붙여준다. 즉, 편의상 final을 생략할 수 있게 한 것일 뿐 해당 변수의 값이 바뀌는 문장이 있으면 컴파일 에러가 발생한다.
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
72
73
74
75
76
77
// 인스턴스 메서드에 정의된 local class : 로컬 클래스가 로컬 변수를 조회용으로만 사용하는 이유?
package com.eomcs.oop.ex11.d;
public class Exam0340 {
public static void main(final String[] args) {
final Exam0340 obj = new Exam0340();
final Runner r = obj.createRunner("홍길동");
// createRunner()의 리턴 값은
// 이 메서드에 선언된 로컬 클래스인 A의 객체이다.
// A 객체는 Runner 규칙에 따라 만들었기 때문에
// Runner 레퍼런스에 저장할 수 있다.
// 이 레퍼런스를 사용하여 A 객체의 run() 메서드를 호출해 보자!
r.run();
// 로컬 클래스의 객체가 로컬 변수의 값을 조회용으로만 사용하는 이유?
// => 위의 코드에서 createRunner() 호출이 끝나면 이 메서드가 실행되는 동안
// 생성되었던 모든 로컬 변수는 스택 메모리에서 제거된다.
// 즉 createRunner()의 name 파라미터가 제거된다는 것이다.
}
Runner createRunner(final String name) {
// Exam0340 객체 주소를 받는 내장 변수 this 가 있다.
//
class A implements Runner {
// 컴파일러는 바깥 클래스의 객체 주소를 받을 필드는 추가한다.
// 또한 바깥 클래스의 객체 주소를 받는 생성자를 추가한다.
/*
* Exam0340 outer; // 바깥 클래스의 주소를 받을 필드
*
* String paramName; // run() 메서드에서 사용할 로컬 변수을 값을 받을 필드
*
* public A(Exam0365 obj, String str) { outer = obj; paramName = str;}
*
*/
@Override
public void run() {
// 바깥 메서드의 로컬 변수는 메서드 호출이 완료되는 순간
// 스택 메모리에서 제거되기 때문에
// 로컬 클래스의 객체에서 사용할 수 없는 상황이 발생할 것이다.
// 그래서 컴파일러는 바깥 메서드의 로컬 변수 값을 저장할
// 필드를 클래스에 추가한다.
// 또한 로컬 변수의 값을 받는 생성자를 만든다.
// 따라서 run() 메서드를 호출하는 시점에는
// A 로컬 객체에 name 변수의 값이 들어 있기 때문에
// 그래도 사용할 수 있는 것이다.
// => 다음 코드의 name은 createRunner() 메서드의 파라미터가 아니다.
// => A 인스턴스가 생성될 때 값을 따로 복제한 필드를 가리키는 것이다.
//
System.out.printf("%s님이 달립니다!", name);
// 위 문장은 다음과 같이
// 컴파일러가 로컬 클래스의 필드(예: paramName)를 사용하는 문장으로 바꾼다.
// 그래서 createRunner() 메서드 호출이 끝나더라도
// name 값을 사용할 수 있는 것이다.
//
// System.out.printf("%s님이 달립니다!", this.paramName);
}
}
return new A();
// 컴파일러는 로컬 클래스의 객체를 생성할 때
// 바깥 클래스의 객체 주소와 로컬 객체가 사용하는
// 메서드의 로컬 변수 값을 전달하기 위해
// 다음과 같이 컴파일러가 만든 생성자를 호출한다.
//
// return new A(this, name);
}
}
interface Runner {
void run();
}
내부 클래스 생성
인스턴스 클래스의 인스턴스를 생성하려면 외부클래스의 인스턴스를 먼저 생성해야 한다. 스태틱 내부 클래스의 인스턴스는 외부 클래스를 먼저 생성하지 않아도 된다. 실제로 이런 경우가 발생했다는 것은 내부 클래스로 선언해서는 안 되는 클래스를 내부 클래스로 선언했다는 의미이다.
컴파일 시 생성되는 파일명
외부클래스명$내부클래스명.class
로컬클래스: 다른 메서드에 같은 이름의 내부 클래스가 존재할 수 있기 때문에 내부 클래스명 앞에 숫자가 붙는다
외부클래스명$1내부클래스명.class
외부클래스명$2내부클래스명.class
외부 클래스명.this
내부 클래스와 외부 클래스에 선언된 변수 이름이 같을 때 변수 앞에 this
또는 외부클래스명.this
를 붙여서 서로 구별할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Outer {
int value = 10;
class Inner {
int value = 20;
void method1() {
int value = 30;
System.out.println("value:" + value);
System.out.println("this.value:" + this.value);
System.out.println("Outer.this.value:" + Outer.this.value);
}
}
}
class InnerEx5 {
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.method();
}
}
// 결과
// value: 30
// this.value: 20
// Outer.this.value: 10
익명 클래스
클래스의 선언과 객체의 생성을 동시에 하기 때문에 단 한 번만 사용될 수 있고, 오직 하나의 객체만을 생성할 수 있는 일회용 클래스이다.
1
2
new 조상클래스이름() { /*멤버선언*/ }
new 구현인터페이스이름() { /*멤버선언*/ }
이름이 없기에 생성자도 가질 수 없고, 조상 클래스의 이름이나 구현하고자 하는 인터페이스의 이름을 사용해서 정의하기 때문에 오로지 하나의 클래스를 상속받거나 단 하나의 인터페이스만을 구현할 수 있다.
이름이 없기 때문에 외부 클래스명$숫자.class
의 형식으로 클래스 파일명이 결정된다.