중첩 클래스의 종류는
- 정적 멤버 클래스
- (비정적) 멤버 클래스
- 익명 클래스
- 지역 클래스
이 중 첫번째를 제외한 나머지는 내부클래스(inner class) 에 해당한다
📌 멤버 클래스는 static 클래스로 선언하자!
static이 아닌 내부 인스턴스 클래스는 외부와 연결이 되어 있어 '외부 참조'를 갖게되어 메모리를 더 먹고, 느리며, 또한 GC 대상에서 제외되는 여러 문제점을 일으키기 때문이다.
-
다른 클래스 안에 선언되고, 바깥 클래스의
private
멤버에도 접근할 수 있다는 점만 제외하고는 일반 클래스와 똑같다 -
다른 정적 멤버와 똑같은 접근 규칙을 적용받는다 →
private
으로 선언하면 바깥 클래스에서만 접근할 수 있는 식 -
열거 타입도 암시적으로 static 이다.
정적 멤버 클래스와 비정적 멤버 클래스의 구문상 차이는 단지 static이 붙어 있고 없고 뿐이지만, 의미상 차이는 의외로 크다
-
비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다.
-
비 정적 멤버 클래스는 바깥 클래스가 표현하는 객체의 한 부분(구성요소)일 때 사용
→ 정적 멤버 클래스로 생성하자
→ 비정적 멤버 클래스는 바깥 인스턴스 없이는 생성할 수 없다.
→ private
정적 멤버 클래스를 사용하자
- 좋은 예시로
Map
의Entry
가 있다.- 많은
Map
구현체는 각각의키-값
쌍을 표현하는 엔트리 객체들을 가지고 있다. - 모든 엔트리가 맵과 연관되어 있지만 엔트리의 메서드들은 맵을 직접 사용하지는 않는다.
- 많은
멤버 클래스가 공개된 클래스의 public
이나 protected
멤버라면 정적이냐 아니냐도 중요하다
멤버 클래스 역시 공개 API
가 되니, 혹시라도 향후 릴리스에서 static
을 붙이면 하휘 호환성이 깨지기 때문이다.
비정적 멤버 클래스의 인스턴스와 바깥 인스턴스 사이의 관계는 멤버 클래스가 인스턴스화 될 때 확립되며, 더이상 변경할 수 없다.
- 이 관계 정보는 비정적 멤버 클래스의 인스턴스 안에 만들어져 메모리 공간을 차지하며, 생성 시간도 더 걸린다.
수동으로 비정적 멤버 클래스 생성 방법
-
어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 사용
-
ex: keySet()
등이 반환 → 자신의 컬렉션 뷰를 반환public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { ... /** * Returns a {@link Set} view of the keys contained in this map. * The set is backed by the map, so changes to the map are * reflected in the set, and vice-versa. If the map is modified * while an iteration over the set is in progress (except through * the iterator's own {@code remove} operation), the results of * the iteration are undefined. The set supports element removal, * which removes the corresponding mapping from the map, via the * {@code Iterator.remove}, {@code Set.remove}, * {@code removeAll}, {@code retainAll}, and {@code clear} * operations. It does not support the {@code add} or {@code addAll} * operations. * * @return a set view of the keys contained in this map */ public Set<K> keySet() { Set<K> ks = keySet; if (ks == null) { ks = new KeySet(); keySet = ks; } return ks; } ... final class KeySet extends AbstractSet<K> { public final int size() { return size; } public final void clear() { HashMap.this.clear(); } public final Iterator<K> iterator() { return new KeyIterator(); } ... }
static을 생략하면 바깥 인스턴스로의 숨은 외부 참조를 갖게된다 그로인해,
- 바깥 인스턴스 - 멤버 클래스 관계를 위한 시간과 공간 소모 (참조를 저장하려면)
Garbage Collection
이 바깥 클래스의 인스턴스 수거 불가 -> 메모리 누수 발생- 참조가 눈에 보이지 않아 개발이 어려움
- 이름이 없는 클래스
- 바깥 클래스의 멤버가 아니다.
- 멤버와 달리, 쓰이는 시점에 선언과 동시에 인스턴스가 생성
- 비 정적인 문맥에서 사용될 때에만 바깥 클래스의 인스턴스를 참조할 수 있다.
- static으로 선언된 메소드에는 static만 접근이 가능하기 때문
- 정적 문맥에서라도 상수(
static final
)변수 이외의 정적 멤버는 가질 수 없다.
- 선언 지점에서만 인스턴스 생성이 가능하며, 이름이 없기에 instanceof와 같은 검사가 불가하다.
- 인터페이스 구현 및 다른 클래스 상속 불가
public class AnonymousClassExample {
public static void main(String[] args) {
// Runnable 인터페이스의 익명 클래스 구현
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("익명 클래스를 사용한 스레드 실행");
}
};
Thread thread = new Thread(r);
thread.start();
}
}
→ 람다를 지원하기 전까지는 즉석에서 작은 함수 객체나 처리 객체를 만드는 데 익명 클래스를 주로 사용했다
→ 이제는 람다를 사용한다.
public class LambdaExample {
public static void main(String[] args) {
Runnable r = () -> System.out.println("람다를 사용한 스레드 실행");
Thread thread = new Thread(r);
thread.start();
}
}
- 정적 팩터리 메서드를 구현할 때에도 익명 클래스가 사용된다.
public class ServiceFactory {
// 정적 팩터리 메서드: Service 인터페이스의 익명 구현체를 반환
public static Service createService() {
return new Service() {
@Override
public void execute() {
System.out.println("Service executed.");
}
};
}
}
public class Main {
public static void main(String[] args) {
Service service = ServiceFactory.createService();
service.execute();
}
}
- 멤버 클래스
- 이름이 있고, 반복해서 사용 가능
- 익명 클래스
- 비정적 문맥에서 사용될 때만 바깥 인스턴스 참조 가능
- 정적 멤버를 가질 수 없음
- 가독성을 위해 짧게 작성해야 함
- 멤버 클래스 사용 경우
- 메소드 밖에서도 사용해야 하거나 너무 긴 경우
- 바깥 인스턴스 참조 → 비정적으로 구성
- 바깥 인스턴스 참조 X → 정적으로 구성
- 익명 클래스 or 지역 클래스 사용 경우
- 한 메서드 안에서만 사용,
- 인스턴스 생성 시점이 단 한 곳,
- 해당 타입으로 쓰기 적합한 클래스나 인터페이스 이미 존재 → 익명 클래스
- 해당 타입으로 쓰기 적합한 클래스나 인터페이스 이미 존재 X → 지역 클래스