중첩 클래스는 해당 클래스를 감싸고 있는 클래스 안에서만 사용될 수 있으며 그 외의 클래스에서 사용되기 위해서는 Top Level 클래스로 만들어야 한다.
중첩클래스의 종류
정적 멤버 클래스, 비정적 멤버 클래스, 익명 클래스, 지역 클래스
정적 멤버 클래스 빼고 모두 inner class에 해당한다?
각각의 중첩 클래스를 언제, 왜 사용해야 하는지 살펴보자.
정적 멤버 클래스
다른 클래스 내부에서 선언되고, 바깥 클래스의 private 멤버에 접근할 수 있다.
public class OuterClass {
private String name;
static class StaticMemberClass {
void hello() {
OuterClass outerClass = new OuterClass();
outerClass.name = "홍길동";
}
}
}
위 특징을 제외하면 일반 클래스와 거의 동일하다.
일반적인 정적 필드와 동일한 접근 규칙을 갖는다
예 : private 으로 선언하면 바깥 클래스에서만 해당 클래스에 접근할 수 있다.
정적 멤버 클래스의 사용 예시
바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스로 사용될 수 있다.
예 : 계산기의 연산 종류를 나타내는 열거 타입 객체 Operation이 있다고 생각해보자.
Operation Enum은 Calculator 클래스의 public 정적 멤버 클래스가 되어야 한다.
그래야 Calculator 클래스 객체가 Calculator.Operation.PLUS나 Calculator.Operation.MINUS 등의 형태로 필요한 연산을 참조할 수 있다.
public class Calculator {
public static enum Operation {
PLUS("+", (x, y) -> x + y),
MINUS("-", (x, y) -> x - y);
...
}
}
비정적 멤버 클래스
정적 멤버 클래스와 비교했을 때 코드 상의 차이는 static 이 붙어 있느냐 없느냐 뿐이지만 의미적으로 비교해보면 꽤 큰 차이가 있다.
비정적 멤버 클래스의 특징
비정적 멤버 클래스 객체는 바깥 클래스 객체와 암묵적으로 연결된다.
비정적 멤버 클래스 객체의 메서드에서 정규화된 this(클래스명.this)를 이용해 바깥 클래스 객체의 메서드를 호출하거나 바깥 클래스 객체의 참조를 가져올 수 있다.
class OuterClass {
int x = 10;
// 비정적 멤버 클래스
public class InnerClass {
int x = 100;
public void run() {
System.out.println(OuterClass.this.x, this.x);
}
}
// 생성자 사용 방법 주목
public static void main(String[] args) {
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();
inner.run(); // 10, 100 출력
}
}
중첩 클래스의 객체가 바깥 클래스의 객체와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들자
비정적 멤버 클래스는 바깥 클래스 객체 없이는 생성할 수 없기 때문
변수명이 겹치지 않으면 this 사용하지 않고 바로 참조하는 것도 가능
public class OuterClass{
int outerfield;
void outerMethod(){ System.out.println(outerfield); }
// 비정적 멤버 클래스
public class InnerClass {
void innerMethod() {
outerfield = 3;
outerMethod();
}
}
}
비정적 멤버 클래스의 객체와 바깥 클래스 객체 사이의 이러한 암묵적인 관계는 멤버 클래스가 객체화될 때 확립되며 그 이후에는 변경할 수 없다.
이 관계는 보통 바깥 클래스 객체의 메서드에서 비정적 멤버 클래스의 생성자를 호출할 때 자동으로 만들어지지만, 드물게는 직접 바깥클래스.new 내부클래스(args) 를 호출해 수동으로 만들기도 한다.
이 관계 정보는 비정적 멤버 클래스 객체 안에 저장되어 메모리 공간을 차지하며, 생성 시간도 소모한다.
비정적 멤버 클래스의 사용 예시
비정적 멤버 클래스는 어댑터를 정의할 때 자주 쓰인다.
어댑터 : 어떤 클래스의 객체를 감싸 마치 다른 클래스의 객체처럼 보이게 하는 것
예) Map 인터페이스 : 자신의 컬렉션 뷰를 구현할 때 비정적 멤버 클래스를 사용한다.
예) Set, List 같은 컬렉션 인터페이스 : 자신의 반복자를 구현할 때 비정적 멤버 클래스를 사용한다.
public class MySet<E> extends AbstractSet<E> {
...
@Override public Iterator<E> iterator() { return new MyIterator(); }
private class MyIteratpr implements Iterator<E> { ... }
...
}
비정적 멤버 클래스의 단점
멤버 클래스에서 바깥 클래스 객체에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자.
static을 생략하면 바깥 객체로의 숨을 외부 참조를 갖게 된다.
이 참조를 저장하는데 시간과 공간이 소모된다.
GC가 바깥 클래스의 객체를 수거하지 못하는 메모리 누수 문제가 생길수도 있다.
참조가 눈에 보이지 않으니 문제 원인을 찾기가 어렵다.
private 정적 멤버 클래스
private 정적 멤버 클래스는 주로 바깥 클래스가 표현하는 객체의 한 부분을 나타낼 때 사용한다.
예) 키와 값을 매핑시키는 Map 객체
많은 Map 구현체는 각각의 키-값 쌍을 표현하는 Entry 객체를 가지고 있다.
모든 Entry가 Map과 연관되어 있지만 Entry의 메서드들(getKey, getValue, setValue 등)은 맵을 직접 사용하지는 않는다.
즉 Entry를 비정적 멤버 클래스로 표현하는 것은 낭비이고, private 정적 멤버 클래스가 가장 알맞다.
Entry를 선언할 때 static을 빼먹어도 Map은 여전히 동작하지만 모든 Entry가 바깥 Map으로의 참조를 갖게 되어 공간과 시간을 낭비할 것이다.
멤버 클래스가 공개된 클래스의 public이나 protected 멤버라면 static이냐 아니냐의 여부는 2배로 중요해진다. 멤버 클래스 역시 외부에 공개되므로, 개발 도중 static이 붙으면 하위 호환성이 깨질 수 있다.
익명 클래스
이름이 없는 클래스. 멤버 클래스와 달리 쓰이는 시점에 선언과 객체 생성이 동시에 이루어진다.
익명 클래스는 바깥 클래스의 멤버가 아니다.
왜냐하면 사용되는 시점에 인스턴스화 되고, 코드상의 어떤 위치에서든 만들 수 있기 때문이다.
비정적인 문맥에서 사용될 때만 바깥 클래스의 객체를 참조할 수 있다.
정적 문맥에서 사용될 때는 static 변수 이외의 static 필드는 가질 수 없다.
상수 표현을 위해 초기화된 final 기본 타입과 문자열 필드만 가질 수 있다.
익명 클래스의 단점
선언한 지점에서만 인스턴스를 만들 수 있다.
instanceof 검사나 클래스의 이름이 필요한 작업은 수행할 수 없다.
여러 인터페이스를 구현할 수 없고, 인터페이스를 구현하면서 다른 클래스를 상속받을 수 없다.
익명 클래스를 사용하는 외부 클래스는 해당 익명 클래스가 상속받은 상위 클래스 외에는 호출할 수 없다.
표현식 중간에 등장하므로 짧지 않으면(10줄 이하) 가독성이 떨어진다.
익명 클래스의 사용 예시
자바가 람드를 지원하기 전에는 즉석에서 작은 함수 객체나 처리객체(Process Object)를 만드는데 익명 클래스를 주로 사용했다.
현재는 람다가 그 역할을 대신하고 있다.
정적 팩토리 메서드를 구현할 때 사용될 수 있다.
static List<Integer> intArrayAsList(int[] a) {
Objects.requireNonNull(a);
return new AbstractList<>() {
@Override
public Interger get(int i) { return a[i]; }
@Override
public Integer set(int i, Integer val) {
int oldVal = a[i];
a[i] = val;
return oldVal;
}
@Override
public int size() { return a.length; }
}
}
지역 클래스
네가지 중첩 클래스 중 가장 드물게 사용된다.
지역변수를 선언할 수 있는 곳이면 어디서는 선언할 수 있고 유효 범위도 지역변수와 동일하다.
다른 중첩 클래스들과의 공통점
멤버 클래스처럼 이름이 있고 반복해서 사용할 수 있다.
익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 클래스 객체를 참조할 수 있으며, 정적 필드는 가질 수 없고 가독성을 위해 짧게 작성해야 한다.