⭐ 용어 정리
중첩 클래스 (nested class) : 다른 클래스 안에 정의된 클래스.
중첩 클래스는 자신을 감싼 바깥 클래스에서만 쓰여야 하며, 그 외의 경우는 톱레벨 클래스로 만들어야 한다.
톱레벨 클래스 : 소스파일에서 가장 바깥에 존재하는 클래스.
중첩 클래스의 종류는
- 정적 멤버 클래스
- (비정적) 멤버 클래스 (*)
- 익명 클래스 (*)
- 지역 클래스 (*)
이 중 첫 번째를 제외한 나머지는 내부 클래스(inner class)에 해당한다. (위에서 * 표시 한 부분)
이번 아이템에서는 각각의 중첩 클래스를 언제 그리고 왜 사용하는지에 대해 다룬다.
⭐ 1. 정적 멤버 클래스
- 다른 정적 멤버와 똑같은 접근 규칙을 적용받아서, private으로 선언하면 바깥 클래스에서만 접근할 수 있다.
- 정적 멤버 클래스는 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스로 쓰인다.
- 바깥클래스$중첩클래스 형태로 컴파일이 된다.
- private 정적 멤버 클래스는 바깥 클래스가 표현하는 객체의 구성요소를 나타낼 때 쓴다.
- Map 인스턴스의 경우 Entry 객체들을 가지고 있고, 모든 Entry가 맵과 연관되어 있지만 Entry의 key, value를 가져오거나 수정하는데는 Map을 직접 사용하지 않으므로, 정적 멤버 클래스가 알맞다.
public class 바깥_클래스 {
private static String 바깥_Private_문자열 = "이것은 바깥 private 문자열 입니다.";
public void 바깥_메서드() {
내부_Private_정적_클래스 인스턴스 = new 내부_Private_정적_클래스();
인스턴스.메서드();
}
private static class 내부_Private_정적_클래스 {
public void 메서드() {
System.out.print("내부 private 클래스에서 실행됨 : ");
System.out.println(바깥_Private_문자열);
}
}
// 바깥_클래스$내부_Public_정적_클래스
public static class 내부_Public_정적_클래스 {
public static void 메서드() {
System.out.print("내부 public 클래스에서 실행됨 : ");
System.out.println(바깥_Private_문자열);
}
}
}
바깥 클래스 내부에 정적 private/public 멤버 클래스가 있다. 각각 내부에는 바깥 클래스의 private 문자열을 가져와서 출력해주는 간단한 코드다. 바깥 클래스의 메서드는 정적 private 클래스를 인스턴스화 하여 메서드를 실행할 수도 있다.
public class 정적_클래스_테스트 {
public static void main(String[] args) {
// public 정적 클래스의 정적 메서드 실행
바깥_클래스.내부_Public_정적_클래스.메서드();
// 바깥 클래스의 메서드로 우회하여
// private 정적 클래스의 메서드를 실행
바깥_클래스 인스턴스 = new 바깥_클래스();
인스턴스.바깥_메서드();
}
}
// 출력 결과
내부 public 클래스에서 실행됨 : 이것은 바깥 private 문자열 입니다.
내부 private 클래스에서 실행됨 : 이것은 바깥 private 문자열 입니다.
⭐ 2. 비정적 멤버 클래스
- 비정적 멤버 클래스의 인스턴스는 바깥 인스턴스로부터 생성된다.
- 비정적 멤버 클래스는 어댑터를 정의할 때도 자주 쓰이는데, 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 사용하기도 한다.
- Map 인터페이스의 구현체들이 (keySet, entrySet, values 등의) 자신의 컬렉션 뷰를 구현할 때
- Set, List 같은 컬렉션의 인터페이스 구현체에서도 반복자를 구현할 때
- 다만, 비정적 멤버 클래스는 바깥 인스턴스로의 숨은 외부참조를 갖게되어 시간과 공간이 소비되며, 가비지 컬렉터가 바깥 인스턴스를 수거하지 못하여 메모리 누수가 발생한다.
- 따라서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여 정적 멤버 클래스로 만들어야 한다.
- 비정적 멤버 클래스가 공개된 클래스의 public이나 protected 라면 멤버 클래스도 공개 API가 되니 향후 변경시 하위 호환성이 깨질 수 있으니 정적이냐 아니냐는 상당히 중요하다.
public class 바깥_클래스 {
private String 바깥_Private_문자열 = "이것은 바깥 private 문자열 입니다.";
public void 바깥_메서드1() {
System.out.println("이 메서드는 바깥 클래스의 public 메서드 입니다.");
}
public class 내부_클래스 {
public void 내부_메서드1() {
System.out.print("내부 클래스에서 실행하는 바깥 메서드 : ");
바깥_클래스.this.바깥_메서드1();
}
public void 내부_메서드2() {
System.out.print("내부 클래스에서 가져온 바깥 private 문자열 : ");
System.out.println(바깥_클래스.this.바깥_Private_문자열);
}
}
}
내부 클래스에서 바깥 클래스의 멤버들을 가져올 때는, 클래스명.this 를 통해 바깥 클래스의 이름을 명시해서 사용하며, 이는 정규화된 this 라고 한다.
public class 비정적_클래스_테스트 {
public static void main(String[] args) {
바깥_클래스 바깥_인스턴스 = new 바깥_클래스();
바깥_인스턴스.바깥_메서드1();
// 바깥 인스턴스 없이는 내부 클래스 생성 불가능 !
// 멤버 클래스와의 관계를 위한 시간과 공간이 소모되고, **숨은 참조로 GC의 대상이 되지 않음**
// 따라서 바깥 인스턴스에 접근할 일이 없다면 정적 멤버 클래스로 정의하는 것이 좋음
바깥_클래스.내부_클래스 내부_인스턴스 = new 바깥_클래스().new 내부_클래스();
내부_인스턴스.내부_메서드1();
내부_인스턴스.내부_메서드2();
}
}
// 출력 결과
이 메서드는 바깥 클래스의 public 메서드 입니다.
내부 클래스에서 실행하는 바깥 메서드 : 이 메서드는 바깥 클래스의 public 메서드 입니다.
내부 클래스에서 가져온 바깥 private 문자열 : 이것은 바깥 private 문자열 입니다.
⭐ 3. 익명 클래스
- 바깥 클래스의 멤버 클래스가 아니다. (getClass().getName() 시 $숫자 가 붙음)
- 쓰이는 시점에 선언과 동시에 인스턴스가 만들어지며 재사용이 불가능하다.
- 익명 객체 필드, 로컬 변수, 매개변수 등, 코드 어디서든 만들 수 있다.
- 비 정적인 문맥에서만 사용될 때만 바깥 클래스의 인스턴스를 참조할 수 있다
- 정적 문맥에서라도 상수 변수 이외의 정적 멤버는 가질 수 없다.
- 인터페이스 구현할 수 없다. 다른 클래스를 상속할 수 없다.
- 익명 클래스가 바깥 클래스가 상속한 멤버 외에는 호출할 수 없다.
- 표현식 중간에 등장하므로 짧지 않으면 가독성이 떨어진다. 보통 10줄 이내이나, 람다가 나온 이후로는 람다를 사용한다.
- 정적 팩터리 메서드를 구현할 때 사용한다.
@FunctionalInterface
public interface Keyboard {
void typing();
}
세상에는 수만가지 키보드가 있고, 이걸 일일이 클래스로 만드려면 상당히 많은 코드를 작성해야 한다. 하지만 키보드의 본질은 타이핑이다. 그래서 이를 인터페이스로 만들어주었다.
public class 개발자 {
String 이름 = "홍길동";
// 방법 1 : 필드에 익명 객체를 생성
// 익명 클래스: class 개발자$1 (멤버 클래스 아님)
Keyboard 키보드1 = new Keyboard() {
String brand = "A브랜드";
@Override
public void typing() {
System.out.println(brand + ": 타닥타닥");
}
};
// 방법 2 : 로컬 변수의 초기값으로 대입
void work1() {
Keyboard 키보드2 = new Keyboard() {
@Override
public void typing() {
System.out.println(이름 + ": 토도토도");
}
};
키보드2.typing();
}
// 방법 3 : 익명 객체를 매개변수로 받음
public void work2(Keyboard keyboard) {
keyboard.typing();
}
}
그리고 여기 개발자가 있다. 개발자는 키보드로 일을 한다. 개발자는 자기 소유의 키보드를 가질 수도 있고(방법 1), 일을 할 때마다 키보드를 만들어 쓸 수도 있고(방법 2), 키보드를 받아서 일할 수도 있다.(방법 3)
public class 익명_클래스_테스트 {
public static void main(String[] args) {
개발자 홍길동 = new 개발자();
// 방법 1 : 익명 객체 필드 사용
홍길동.키보드1.typing();
// 방법 2 : 익명 객체 로컬 변수 사용
홍길동.work1();
// 방법 3 - 1 : 매개변수로 익명 객체 사용
홍길동.work2(new Keyboard() {
@Override
public void typing() {
System.out.println("타라라라라라라락!");
}
});
// 방법 3 - 2 : 람다 사용
홍길동.work2(() -> System.out.println("타라라라라라라락!"));
}
}
// 출력 결과
A브랜드: 타닥타닥
홍길동: 토도토도
타라라라라라라락!
타라라라라라라락!
홍길동이라는 개발자가 있다. 방법 1과 2는 위에서 설명했으므로, 방법 3을 위주로 보면 work2 메서드에 익명 클래스로 키보드를 만들어서 준다. 이는 자바8에서 람다가 나오면서 3-2로 바꿀 수 있게 되었다. (키보드 인터페이스에서 @FunctionalInterface 어노테이션으로 이를 명시적으로 알릴 수 있다.)
⭐ 4. 지역 클래스
- 지역 클래스는 블록 안에 정의된 클래스로, 4가지 중첩 클래스 중 가장 드물게 사용된다.
- 지역 클래스는 지역 변수를 선언할 수 있는 곳이면 실질적으로 어디서든 선언할 수 있고, 유효 범위도 지역 변수와 같다.
- 멤버 클래스처럼 이름이 있고, 반복해서 사용할 수 있다.
- 익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있으며, (“상수 변수 이외의”) 정적 멤버는 가질 수 없으며, 가독성을 위해 짧게 작성해야 한다.
- 정적 멤버를 가질 수 없다고 되어 있지만, 사실 공식 문서에서도 가질 수 있다고 되어있다.
- 다만 상수 변수만 가능하며, 상수 변수란 compile-time constant expression으로 초기화 되거나 final로 선언된 기본형, String, 산술연산 이다.
- 다시 책을 읽어보니, 책에서 익명 클래스를 설명할 때 “상수 변수 이외의” 정적 멤버는 가질 수 없다. 라고 되어있네요. 지역 클래스에서 설명할 때는 큰 따옴표 부분을 생략해서 착각 했었네요.
public class 지역_클래스_테스트 {
void 메서드() {
class 지역_클래스 {
static final int 정적_멤버_변수 = 10;
public 지역_클래스() {
// 정적 멤버 가질 수 있는데?
System.out.println("로컬 클래스 내의 정적 멤버는 다음과 같다 : " + 정적_멤버_변수);
}
public static void 정적_메서드() {
System.out.println("정적 메서드 실행");
}
// 이름이 있고,
// 반복 사용 가능
new 지역_클래스();
new 지역_클래스();
지역_클래스.정적_메서드();
}
public static void main(String[] args) {
지역_클래스_테스트 인스턴스 = new 지역_클래스_테스트();
인스턴스.메서드();
}
}
// 출력 결과
로컬 클래스 내의 정적 멤버는 다음과 같다 : 10
로컬 클래스 내의 정적 멤버는 다음과 같다 : 10
정적 메서드 실행
설명한 대로, 이름이 있어서 이를 여러 인스턴스로 만들 수 있으며, 상수 변수를 정적 멤버로 가질 수도 있다.
⭐ 핵심 정리
중첩 클래스에는 네 가지가 있으며 각각의 쓰임이 다르다.
- 멤버 클래스 : 메서드 밖에서도 사용해야 하거나 메서드 안에 정의하기엔 너무 길 경우 사용
- 멤버 클래스의 각 인스턴스가 바깥 인스턴스를 참조한다면 비정적으로, 아니라면 정적으로 만든다.
- 익명 클래스 : 중첩 클래스가 한 메서드 안에서만 쓰이면서 그 인스턴스를 생성하는 지점이 단 한 곳이고, 해당 타입으로 쓰기에 적합한 클래스나 인터페이스가 이미 있을 경우 사용
- 지역 클래스 : 익명 클래스를 쓰기 부적절한 경우에 사용
또한 비정적 멤버 클래스는 바깥 인스턴스로의 숨은 참조가 발생하여 시간과 공간이 소비되며 가바지 컬렉션이 수거하지 못하므로 메모리 누수가 생긴다.