결론
중첩 클래스에는 네 가지가 있으며, 각각의 쓰임이 다릅니다.
메서드 밖에서도 사용해야 하거나, 안에 정의하기에 너무 길다면 멤버 클래스로 만듭니다.
멤버 클래스의 인스턴스 각각이 바깥 클래스를 참조한다면 비정적으로, 그렇지 않으면 정적으로 만듭시다.
중첩 클래스가 한 메서드 안에서만 쓰이면서 그 인스턴스를 생성하는 지점이 단 한곳이고 해당 타입으로 쓰기에 적합한 클래스나 인터페이스가 이미 있다면 익명 클래스로 만들고, 그렇지 않으면 지역 클래스로 만듭시다.
설명
중첩 클래스(nested class)란 다른 클래스 안에 정의된 클래스를 말합니다. 중첩 클래스는 자신을 감싼 바깥 클래스에서만 쓰여야 합니다.
중첩 클래스의 종류
- 정적 멤버 클래스
- (비정적) 멤버 클래스
- 익명 클래스
- 지역 클래스
이번 아이템에서는 각각의 중첩 클래스를 언제, 그리고 왜 사용해야 하는지 이야기합니다.
1. 정적 멤버 클래스
특징
public class Outer {
private static int outerStaticField; //접근 가능
private int outerInstanceField; //접근 불가
public static class StaticInner {
public void printOuterStaticField() {
System.out.println(outerStaticField);
}
}
}
- 정적 멤버 클래스는 외부 클래스의 인스턴스와 독립적으로 존재할 수 있습니다.
따라서, 정적 멤버 클래스의 인스턴스를 생성하려면 외부 클래스의 인스턴스가 필요하지 않습니다. - 외부 클래스의 정적 멤버에 직접 접근할 수 있습니다. 하지만, 일반 멤버에는 직접 접근할 수 없습니다.
접근하기 위해서는 정적 멤버 클래스가 클래스의 인스턴스를 받아야 합니다. - 정적 멤버 클래스는 외부 클래스와 독립된 이름 공간을 가지므로, 클래스 이름 충돌을 피할 수 있습니다.
- 정적 멤버 클래스는 다른 정적 멤버와 똑같은 접근 규칙을 적용받습니다.
예를 들면, private으로 선언하면 바깥 클래스에서만 접근할 수 있습니다.
활용
정적 멤버 클래스는 흔히 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스로 쓰입니다.
예) 계산기가 지원하는 연산 종류를 정의하는 열거 타입
Operation 열거 타입은 Calculator 클래스의 public 정적 멤버 클래스가 되어야 합니다.
그러면 calculator의 클라이언트에서 Calculator.Operations.PLUS 나 Calculator.Operations.MINUS 같은 형태로 원하는 연산을 참조할수 있습니다.
2. (비정적) 멤버 클래스
특징
public class Outer {
private int outerField;
public class Inner {
public void printOuterField() {
System.out.println(outerField);
}
}
}
- 비정적 멤버 클래스는 바깥 클래스와 밀접한 관계가 있습니다.
그래서 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this(클래스명.this)를 사용해 바깥 인스턴스의 메서드를 호출하거나, 바깥 인스턴스의 참조를 가져올 수 있습니다. - 비정적 멤버 클래스는 바깥 인스턴스 없이는 생성할 수 없습니다.
- 비정적 멤버 클래스의 인스턴스와 바깥 인스턴스 사이의 관계는 멤버 클래스가 인스턴스화될 때 확립되며, 더 이상 변경할 수 없습니다.
이 관계는 바깥 클래스의 인스턴스 메서드에서 비정적 멤버 클래스의 생성자를 호출할 때 자동으로 만들어지는 게 보통이지만, 드물게는 직접 바깥 인스턴스의 클래스.new MemberClass(args)를 호출해 수동으로 만들기도 합니다.
중요!
개념상 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있거나 바깥 클래스를 참조하지 않는다면, 정적 멤버 클래스로 만들어야 합니다.
바깥 클래스와 중첩 클래스와의 관계 정보는 비정적 멤버 클래스의 인스턴스 안에 만들어져 메모리 공간을 차지하며, 생성 시간도 더 걸리기 때문입니다.
활용
비정적 멤버 클래스는 어댑터를 정의할 때 자주 쓰입니다.
즉, 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 사용하는 것입니다.
예를 들어, Map 인터페이스의 구현체들은 보통 자신의 컬렉션 뷰를 구현할 때 비정적 멤버 클래스를 사용합니다.
비슷하게 Set과 List 같은 다른 컬렉션 인터페이스 구현들도 자신의 반복자를 구현할 때 비정적 멤버 클래스를 주로 사용합니다.
비정적 멤버 클래스의 흔한 쓰임 - 자신의 반복자 구현
public class MySet<E> extends AbstractSet<E> {
...
@Override public Iterator<E> iterator() {
return new MyIterator();
}
//비정적 멤버 클래스
private class MyIterator implements Iterator<E> {
...
}
}
Static vs Non-Static
멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면, 무조건 static을 붙여 정적 멤버 클래스로 만듭시다.
static을 생략하면 바깥 인스턴스로의 숨은 외부 참조를 갖게 됩니다.
앞서도 얘기했듯 이 참조를 저장하려면 시간과 공간이 소비됩니다.
더 심각한 문제는 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수가 생길 수 있다는 점입니다.
참조가 눈에 보이지 않으니 문제의 원인을 찾기 어려워 때때로 심각한 상황을 초래하기도 합니다.
1-1. Private 정적 멤버 클래스
private 정적 멤버 클래스는 흔히 바깥 클래스가 표현하는 객체의 한 부분(구성요소)을 나타낼 때 씁니다.
아래의 예시는 "주소"를 나타내는 Address 클래스를 바깥 Person 클래스의 한 부분으로 표현하는 코드입니다.
이때 Address 클래스는 Person 클래스의 내부에서 사용되며, Person 클래스의 외부에서는 접근할 수 없습니다.
이로써 Address 클래스는 Person 클래스의 구성 부분으로서 활용됩니다.
public class Person {
private String name;
private int age;
private Address address;
public Person(String name, int age, String street, String city) {
this.name = name;
this.age = age;
this.address = new Address(street, city);
}
private static class Address {
private String street;
private String city;
public Address(String street, String city) {
this.street = street;
this.city = city;
}
public String getFullAddress() {
return street + ", " + city;
}
}
public String getDetails() {
return "Name: " + name + ", Age: " + age + ", Address: " + address.getFullAddress();
}
public static void main(String[] args) {
Person person = new Person("John Doe", 30, "123 Main St", "Cityville");
System.out.println(person.getDetails());
}
}
Static?
Address 클래스는 Person 객체를 사용하지 않습니다.
따라서 Address를 비정적 멤버 클래스로 표현하는 것은 낭비이고, private 정적 멤버 클래스가 가장 알맞습니다.
선언할 때 static을 빠트려도 여전히 동작하겟지만, 모든 Address 가 바깥 Person 의 참조를 갖게 되어 공간과 시간을 낭비할 것입니다.
멤버 클래스가 공개된 클래스의 public이나 protected 멤버라면 정적이냐 아니냐는 두 배로 중요해집니다.
멤버 클래스 역시 공개 API가 되니, 혹시라도 향후 릴리스를 붙이면 하위 호완성이 깨집니다.
3. 익명 클래스
특징
public class AnonymousClassExample {
public static void main(String[] args) {
// ActionListener 인터페이스의 익명 클래스 구현
JButton button = new JButton("Click me!");
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Button clicked from an anonymous class!");
}
});
}
}
- 익명 클래스는 바깥 클래스의 멤버가 아닙니다.
멤버와 달리, 쓰이는 시점에 선언과 동시에 인스턴스가 만들어집니다. - 오직 비정적인 문맥에서 사용될 때만 바깥 클래스의 인스턴스를 참조할 수 있습니다. 정적 문맥에서라도 상수 변수 이외의 정적 멤버는 가질 수 없습니다.
- 익명 클래스는 응용하는 데 제약이 많은 편입니다.
- 선언한 지점에서만 인스턴스를 만들 수 있고, instanceof 검사나 클래스의 이름이 필요한 작업은 수행할 수 없습니다.
- 여러 인터페이스를 구현할 수 없고, 인터페이스를 구현하는 동시에 다른 클래스를 상속할 수도 없습니다.
- 익명 클래스를 사용하는 클라이언트는 그 익명 클래스가 상위 타입에서 상속한 멤버 이외에 호출할 수 없습니다.
- 익명 클래스는 표현식 중간에 등장하므로, 10줄 이하로 짧지 않으면 가독성이 떨어집니다.
활용
자바가 람다를 지원하기 전에는 즉석에서 작은 함수 객체나 처리 객체를 만드는 데 익명 클래스를 주로 사용했습니다.
물론 이제는 람다에게 그 자리를 물려줬습니다.
익명 클래스의 또 다른 주 쓰임은 정적 팩터리 메서드를 구현할 때입니다.
골격 구현을 활용해 List 구현체를 반환하는 정적 팩터리 메서드
static List<Integer> intArrayAsList(int[] a) {
Objects.requireNonNull(a);
return new AbstractList<>() {
@Override public Integer 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;
}
};
}
4. 지역 클래스
특징
public class Outer {
public void methodWithLocalClass() {
int localVar = 10;
class LocalInner {
public void printLocalVar() {
System.out.println(localVar);
}
}
LocalInner localInner = new LocalInner();
localInner.printLocalVar();
}
}
- 지역 클래스는 메서드나 블록 내부에서 정의되는 클래스입니다.
- 이 클래스는 외부에서는 접근할 수 없으며, 정의된 메서드나 블록 내부에서만 사용됩니다.
- 지역 클래스는 주로 특정한 메서드에서만 필요한 클래스를 정의할 때 사용됩니다.
- 지역 클래스는 네 가지 중첩 클래스 중 가장 드물에 사용됩니다.
- 다른 세 중첩 클래스와의 공통점도 하나씩 가지고 있습니다.
- 멤버 클래스처럼 이름이 있고, 반복해서 사용할 수 있습니다.
- 익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있으며, 정적 멤버는 가질 수 없습니다.
- 가독성을 위해 짧게 작성해야 합니다.
참고) 정적인 문맥, 비정적인 문맥
비정적인 문맥 (Non-Static Context)
내부 클래스는 주로 외부 클래스의 멤버로써 정의됩니다. 내부 클래스가 비정적인 문맥에서 사용되는 경우, 이는 내부 클래스가 외부 클래스의 인스턴스 필드와 메서드에 접근할 수 있음을 의미합니다.
내부 클래스가 비정적인 문맥에서 사용되면 내부 클래스의 인스턴스는 항상 외부 클래스의 인스턴스와 연결되어 있습니다.
정적 문맥(Static Context)
반면, 정적 문맥에서 내부 클래스를 사용하는 경우에는 내부 클래스의 인스턴스와 외부 클래스의 인스턴스 간의 연관성이 없습니다.
이때 내부 클래스는 외부 클래스의 정적 필드나 정적 메서드와 연관이 있습니다.
public class Outer {
private int outerField;
public void outerMethod() {
// 비정적인 문맥에서 내부 클래스 사용
Inner inner = new Inner();
inner.innerMethod();
}
public static void staticMethod() {
// 정적 문맥에서 내부 클래스 사용
StaticInner staticInner = new StaticInner();
staticInner.staticInnerMethod();
}
// 비정적 내부 클래스
public class Inner {
public void innerMethod() {
System.out.println("Inner method, outerField: " + outerField);
}
}
// 정적 내부 클래스
public static class StaticInner {
public void staticInnerMethod() {
System.out.println("StaticInner method");
}
}
}
Inner 클래스는 외부 클래스의 인스턴스 필드에 접근하고, StaticInner 클래스는 정적 메서드인 staticMethod 내부에서 사용됩니다.
이처럼 내부 클래스가 외부 클래스의 인스턴스와 관련이 있거나 없는 경우에 따라 "비정적인 문맥에서 사용된다" 혹은 "정적 문맥에서 사용된다"라는 표현을 사용합니다.
출처
https://www.yes24.com/Product/Goods/65551284
'책 > effective java' 카테고리의 다른 글
[effective java] 제 5장. 제네릭 (들어가기 전에, 용어 정리) (0) | 2023.08.28 |
---|---|
[effective java] item 25. 톱 레벨 클래스는 한 파일에 하나만 담으라 (0) | 2023.08.28 |
[effective java] item 23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라 (0) | 2023.08.21 |
[effective java] item 22. 인터페이스는 타입을 정의하는 용도로만 사용하라 (0) | 2023.08.21 |
[effective java] item 21. 인터페이스는 구현하는 쪽을 생각해 설계하라 (0) | 2023.08.21 |