Java의 내부 클래스
내부 클래스는 클래스 내에 선언된 클래스이다. 어떤 클래스와 밀접한 관련이 있는 클래스가 있다면 클래스 내에 해당 클래스를 선언하면 된다. 이렇게 한 클래스를 다른 클래스의 내부에 선언하면 두 클래스 멤버들 간 쉬운 접근이 가능하며 외부에는 불필요한 클래스를 감춤으로써(캡슐화) 코드의 복잡성을 줄일 수 있다.
다음과 같이 A, B 두 개의 독립적인 클래스를 합치면 B는 A의 내부 클래스(inner class)가 되고 A는 B를 감싸는 외부 클래스(outer class)가 된다.
class A { ... }
class B { ... }
/////////////////////////////////////////////////////////////
class A { // 외부 클래스
...
class B { // 내부 클래스
...
}
...
}
이때 내부 클래스인 B는 외부 클래스인 A를 제외하고는 다른 클래스에서 잘 사용되지 않는 것이어야 한다.
내부 클래스의 종류는 변수의 선언 위치에 따른 종류와 같고 변수를 선언하는 것과 같은 위치에 선언할 수 있다.
내부 클래스 | 특징 |
---|---|
인스턴스 클래스(instance class) | 외부 클래스의 멤버 변수 위치에 선언되며, 외부 클래스의 인스턴스 멤버처럼 다루어진다. 주로 외부 클래스의 인스턴스 멤버들과 관련된 작업에 사용될 목적으로 선언된다. |
정적 클래스(static class) | 외부 클래스의 멤버 변수 위치에 선언되며, 외부 클래스의 static 멤버처럼 다루어진다. 주로 외부 클래스의 static 멤버, 특히 static 메서드에서 사용될 목적으로 선언된다. |
지역 클래스(local class) | 외부 클래스의 메서드나 초기화 블럭 안에 선언되며, 선언된 영역 내부에서만 사용될 수 있다. |
익명 클래스(anonymous class) | 클래스의 선언과 객체의 생성을 동시에 하는 이름 없는 일회용 클래스이다. |
내부 클래스는 다음과 같은 특징을 가진다.
final static
이 동시에 붙은 변수는 상수이므로 모든 내부 클래스에서 정의가 가능하다.final
이 붙은 변수만 가능한데 JDK 8부터 변수에 값이 한번 할당된 후 변경 사항이 없다면 불변 상태인 것으로 간주하고 final
키워드를 생략한 경우 컴파일러가 자동으로 제어자를 추가한다. 만약 지역 클래스에서 바깥 메서드의 지역 변수를 참조하고 있음에도 불구하고 지역 변수의 값을 변경하려고 하면 컴파일 에러가 발생한다.public class Outer {
private String outerInstanceVar = "1. 외부 클래스의 private 인스턴스 변수";
static String outerStaticVar = "2. 외부 클래스의 클래스 변수";
class InstanceInner { // 인스턴스 클래스
String instanceInnerVar = "3-1. 인스턴스 클래스의 인스턴스 변수";
// static String CONST = "* 인스턴스 클래스의 상수 *"; // 에러
// -> Static declarations in inner classes are not supported at language level '8' / 16부터 가능
static final String CONST = "3-2. 인스턴스 클래스의 상수"; // 상수는 허용
void instanceInnerMethod() {
System.out.println("-- 인스턴스 클래스의 메서드 시작--");
System.out.println(outerInstanceVar);
System.out.println(outerStaticVar);
System.out.println(instanceInnerVar);
System.out.println(StaticInner.staticInnerStaticVar);
System.out.println("-- 인스턴스 클래스의 메서드 끝--\n");
}
}
static class StaticInner { // 정적 클래스
// 정적 클래스만 static 멤버를 정의할 수 있음.
static String staticInnerStaticVar = "4-1. 정적 클래스의 클래스 변수";
String staticInnerVar = "4-2. 정적 클래스의 인스턴스 변수";
static void staticInnerStaticMethod() {
System.out.println("-- 정적 클래스의 클래스 메서드 시작--");
// System.out.println(outerInstanceVar); // 에러
// -> 클래스 멤버이므로 인스턴스 멤버에 바로 접근할 수 없음.
System.out.println(outerStaticVar);
System.out.println(staticInnerStaticVar);
System.out.println("-- 정적 클래스의 클래스 메서드 끝--\n");
}
}
static void staticMethod() {
System.out.println("-- 클래스 메서드 시작--");
StaticInner.staticInnerStaticMethod();
// new InstanceInner().instanceInnerMethod(); // 에러
// -> 인스턴스 클래스에 접근하려면 외부 클래스를 먼저 생성해야만 함.
System.out.println(new Outer().new InstanceInner().instanceInnerVar);
System.out.println(StaticInner.staticInnerStaticVar);
// System.out.println(StaticInner.staticInnerVar); // 에러
// -> 정적 클래스의 인스턴스 변수이므로 정적 클래스를 생성해야 한다.
System.out.println(new StaticInner().staticInnerVar);
System.out.println("-- 클래스 메서드 끝--\n");
}
void method() {
// JDK 8부터 final 생략 가능
String methodLocalVar = "5. 메서드의 지역 변수"; // == final String ...
class LocalInner { // 지역 클래스
String localInnerVar = "6-1. 지역 클래스의 인스턴스 변수";
// static String CONST = "지역 클래스의 상수"; -> 에러
static final String CONST = "6-2. 지역 클래스의 상수"; // 상수 허용
void localInnerMethod() {
System.out.println("-- 지역 클래스의 메서드 시작--");
System.out.println(outerInstanceVar);
System.out.println(outerStaticVar);
System.out.println(new InstanceInner().instanceInnerVar);
System.out.println(StaticInner.staticInnerStaticVar);
// System.out.println(StaticInner.staticInnerVar); // 에러
// -> staticInnerVar은 정적 클래스의 인스턴스 변수이므로 정적 클래스의 인스턴스를 통해 접근
System.out.println(new StaticInner().staticInnerVar);
System.out.println(methodLocalVar); // final이 붙은 상수만 접근 가능
System.out.println(localInnerVar);
System.out.println("-- 지역 클래스의 메서드 끝--\n");
}
}
LocalInner localInner = new LocalInner();
localInner.localInnerMethod();
System.out.println(LocalInner.CONST);
}
public static void main(String[] args) {
// 인스턴스 클래스의 인스턴스를 생성하려면
// 외부 클래스의 인스턴스를 먼저 생성해야 함.
Outer outer = new Outer();
outer.new InstanceInner().instanceInnerMethod();
// outer.new StaticInner().staticInnerMethod();
// -> 에러 Qualified new of static class
// : 클래스 멤버이기 때문에 외부 클래스의 인스턴스로 접근하지 않음.
StaticInner.staticInnerStaticMethod();
staticMethod();
outer.method();
System.out.println(InstanceInner.CONST);
}
}
컴파일 시 생성되는 클래스 파일은 다음과 같다.
내부 클래스와 외부 클래스에 선언된 변수명이 같을 경우 변수 앞에 this
또는 외부클래스명.this
처럼 구별할 수 있다.
익명 클래스는 말 그대로 이름이 없는 클래스로 클래스의 선언과 객체의 생성을 동시에 하기 때문에 오직 하나의 객체만을 생성하여 단 한번만 사용되는 일회용 클래스이다. 이름이 없기 때문에 생성자도 가질 수 없고 부모 클래스의 이름이나 구현하고자 하는 인터페이스의 이름을 사용해서 정의하기 때문에 하나의 클래스를 상속 받는 동시에 인터페이스를 구현한다거나 다중 인터페이스 구현은 할 수 없다. 오로지 단 하나의 클래스를 상속 받거나 인터페이스를 구현할 수 있다.
new 부모클래스명() {
// 멤버 선언
}
/////////////////////////////////////////////
new 구현인터페이스명() {
// 멤버 선언
}
예를 들어 캐리커처를 그려주는 이벤트가 열렸다고 하자. 캐리커처는 고객의 요청에 따라 항상 다르게 그려질 것이다. 그렇기에 익명 클래스로 구현하여 일회성으로 사용할 수 있다.
public class Caricature { ... }
//////////////////////////////////////////////////////////
public interface CaricatureService {
Caricature draw();
}
//////////////////////////////////////////////////////////
public class Customer {
void receive(Caricature caricature) {
System.out.println("감사합니다!");
};
}
//////////////////////////////////////////////////////////
import java.util.Scanner;
public class Event {
public static void main(String[] args) {
Customer customer = new Customer();
System.out.println("어떻게 그려드릴까요?");
Scanner scanner = new Scanner(System.in);
String request = scanner.nextLine();
scanner.close();
CaricatureService caricatureService = new CaricatureService() {
@Override
public Caricature draw() {
System.out.println("고객 요청사항: " + request);
System.out.println("요청사항에 따라 그리는 중...");
System.out.println("캐리커처 완성");
return new Caricature();
}
};
customer.receive(caricatureService.draw());
}
}