Chapter 4 클래스와 인터페이스
클래스와 인터페이스는 자바 언어의 심장과도 같다. 그러니까 열심히 하자.
item15 클래스와 멤버의 접근 권한을 최소화하라
어설프게 설계된 컴포넌트와 잘 설계된 컴포넌트의 가장 큰 차이는 바로 클래스 내부 데이터와 내부 구현 정보를 외부 컴포넌트롤부터 얼마나 잘 숨겼느냐다. 정보 은닉, 혹은 캡슐화라고 하는 이 개념은 소프트웨어 설계의 근간이 되는 원리다.
정보 은닉의 장점은 다음과 같다.
- 시스템의 개발 속도를 높인다.
- 시스템 관리 비용을 낮춘다.
- 정보 은닉 자체가 성능을 높여주지는 않지만, 성능 최적화에 도움을 준다.
- 완성된 시스템을 프로파일링해 최적화할 컴포넌트를 정한 다음 다른 컴포넌트에 영향을 주지 않고 해당 컴포넌트만 최적화할 수 있기 때문이다.
- 소프트웨어의 재사용성을 높인다.
- 큰 시스템을 제작하는 난이도를 낮춰준다.
접근 제한자를 제대로 활용하는 것이 정보 은닉의 핵심이다. 기본 원칙은 '모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다.' 달리 말하면, 소프트웨어가 올바로 동작하는 한 항상 가장 낮은 접근 수준을 부여해야 한다는 뜻이다.
package-private은 API가 아닌 내부 구현이 되어 언제든 수정할 수 있다. 즉, 클라이언트에 아무런 피해 없이 다음 리리스에서 수정, 교체, 제거할 수 있다. 반면, public으로 선언한다면 API가 되므로 하위 호환을 위해 영원히 관리해줘야만 한다.
클래스의 공개 API를 세심히 설계한 후, 그 외의 모든 멤버는 private으로 만들자. 그런 다음 오직 같은 패키지의 다른 클래스가 접근해야 하는 멤버에 한 하여 (private 제한자를 제거해) package-private으로 풀어주자. 권한을 풀어주는 일을 자주 하게 된다면 여러분 시스템에서 컴포넌트를 더 분해해야 하는 것은 아닌지 고민해보자.
ㅇㅇㅇㅇ
public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다.필드가 가변 객체를 참조하거나, final이 아닌 인스턴스 필드를 public으로 선언하면 그 필드에 담을 수 있는 값을 제한할 힘을 잃게 된다. 여기에 더해, 필드가 수정될 때 다른 작업을 할 수 없게 되므로 public 가변 필드를 갖는 클래스는 일벚ㄱ으로 스레드 안전하지 안다.
길이가 0이 아닌 배열은 모두 변경 가능하니 주의하자. 따라서 클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공해서는 안 된다. 이런 필드나 접근자를 제공한다면 클라이언트에서 그 배열의 내용을 수정할 수 있게 된다. 다음 코드를 보자.
public static final Thing[] VALUES = {...};
어떤 IDE가 생성하는 접근자는 private 배열 필드의 참조를 반환하여 이 같은 문제를 똑같이 일으키니 주의하자.
위의 문제를 해결하는 방법 중 하나는 다음과 같다.
private static final Thing[] PRIVATE_VALUES = {};
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
public 배열을 private으로 만들고 public 불변 리스트를 추가하는 방법이다.
두 번째 배열을 private으로 만들고 그 복사본을 반환하는 public 메서드를 추가하는 방법이다.
private static final Thing[] PRIVATE_VALUES = {};
public static final Thing[] values() {
return PRIVATE_VALUES.clone();
}
item16 public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
이따금 인스턴스 필드를 모아놓는 일 외에는 아무 목적도 없는 퇴보한 클래스를 작성하려 할 때가 있다.
class Point {
public double x;
public double y;
}
이런 클래스는 데이터 필드에 직접 접근할 수 있으니 캡슐화의 이점을 제공하지 못한다. API를 수정하지 않고는 내부 표현을 바꿀 수 없고, 불변식을 보장할 수 없으며, 외부에서 필드에 접근할 때 부수 작업을 수행할 수도 없다. 철저한 객체 지향 프로그래머는 이런 클래스를 상당히 싫어해서 필들을 모두 private으로 바꾸고 publi 접근자를 추가한다.
class Point {
private double x;
private double y;
public double getX() {
return x;
};
public double getY() {
return y;
}
public void setX(double x) {
this.x = x;
}
public void setY(double y) {
this.y = y;
}
}
하지만 package-private 클래스 혹은 private 중첩 클래스라면 데이터 필드를 노출한다 해도 하등의 문제가 없다. 크 클래스가 표현하려는 추상 개념만 올바르게 표현해주면 된다.
자바 플랫폼 라이브러리에도 public 클래스의 필드를 직접 노출하지 말라는 규칙을 어기는 사례가 종종 있다. 대표적인 예가 java.awt.package 패키지의 Point와 Dimension클래스다. 이 클래스들을 흉내 내지 말고, 타산지석으로 삼길 바란다.
흠... 정말로 public으로 선언되어있군요...
item17 변경 가능서을 최소화하라
불변 클래스란 그 인스턴스 내부의 값을 수정할 수 없는 클래스다.
클래스를 불면으로 만들려면 다음 다섯 가지 규칙을 따르면 된다.
- 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
- 클래스를 확장할 수 없도록 한다.
- 모든 필드를 final로 선언한다.
- 모든 필드를 private으로 선언한다.
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
아래의 불변 객체를 보자
public final class Complex {
private final double re;
private final double im;
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
public Complex plus(Complex c) {
return new Complex(re + c.re, im + c.im);
}
}
위의 plus 메서드가 인스턴스 자신은 수정하지 않고 새로운 Complex 인스턴스를 만들어 반환하는 모습에 주목하자. 이처럼 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴을 함수형 프로그래밍이라 한다. 메서드 이름으로 동사 대신 plus 같은 전치사를 사용하는 것도 해당 메서드가 객체의 값을 변경하지 않는다는 사실을 강조하려는 의도이다.
함수형 프로그래밍에 익숙하지 않다면 조금 부자연스러워 보일 수도 있지만, 이 방식으로 프로그래밍하면 코드에서 불변이 되는 영역의 비율이 높아지는 장점을 누릴 수 있다. 불변 객체는 단순하다. 불변 객체는 생성된 시점의 상태를 파괴될 때 까지 그대로 간직한다.
불변객체의 단점은 다음과 같다.
첫 번째로는 값이 다르면 반드시 독립된 객체로 만들어야한다는 것이다. 값의 가짓수가 많다면 이들을 모두 만드는 데 큰 비용을 치러야 한다. BigInteger에서 비트 하나를 바꾸는 경우가 대표적인 예이다. 해당 객체는 원본과 단지 한 비트만 다른 백만 비트짜리 인스턴스를 새로 생성한다. 이런 연산은 크기의 비례해 시간과 공간을 잡아먹는다. 이런 문제의 해결 방법은 흔히 쓰일 다단계 연산들을 예측하여 기본 기능으로 제공하거나 package-privatye의 가변 동반 클래스를 사용하는 것이다.
불변의 법칙 중 하나인 '상속을 불가능 하게 하라'를 만족하려면 final클래스로 선언해도 되지만 모든 생성자를 private 혹은 package-private으로 만들고 public 정적 팩터리를 제공하는 방법이다.
public class Complex {
private final double re;
private final double im;
private Complex(double re, double im) {
this.re = re;
this.im = im;
}
public static Complex valueOf(double re, double im) {
return new Complex(re, im);
}
}
BigInteger와 BigDecimal을 설계할 당시엔 불변 객체가 사실상 final이어야 한다는 생각이 널리 퍼지지 않았다. 그래서 이 두 클래스의 메서드들은 모두 재정의할 수 있게 설계되었고, 안타깝게도 하위 호환성이 발목을 잡아 지금까지도 이 문제를 고치지 못했다. 위의 객체들을 인수로 받을 때 해당 인수가 신뢰할 수 없는 학위 클래스의 인스턴스라고 확인되면, 방어적 복사를 사용해야한다.
정리하자면 다음과 같다.
getter가 있다고 무조건 setter를 만들지는 말자. 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
모든 클래스를 불변으로 만들 수는 없다. 이럴 때는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자. 객체가 가질 수 있는 상태의 수를 줄이면 그 객체를 예측하기 쉬워지고 오류가 생길 가능성이 줄어든다. 다른 합당한 이유가 없다면 모든 필드는 private final이어야한다.
생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다. 확실한 이유가 없다면 생성자와 정적 팩터리 외에는 그 어떤 초기화 메서드도 public으로 제공해서는 안 된다.
CountDownLatch 클래스가 이상의 원칙을 잘 방중한다. 비록 가변 클래스지만 가질 수 있는 상태의 수가 많지 않다. 인스턴스를 생성해 한 번 사용하고 그걸로 끝이다.
item18 상속보다는 컴포지션을 사용하라
상속은 코드를 재사용하는 강력한 수단이지만, 항상 최선은 아니다. 잘못 사용하면 오류를 내기 쉬운 소프트웨어를 만들게 된다. 상위 클래스와 하위 클래스를 모두 같은 프로그래머가 통제하는 패키지 않에서라면 상속도 안전한 방법이지만 일반적인 구체 클래스를 패키지 경계를 넘어, 즉 다른 패키지의 구체 클래스를 상속하는 일은 위함하다. 여기서 말하는 상속이란, 클래스가 다른 클래스를 확장하는 구현 상속을 말한다.
메서드 호출과 달리 상속은 캡슐화를 깨뜨린다. 이는 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있음을 말한다. 상위 클래스는 릴리스마다 내부 구현이 달라질 수 있으며, 그 여파로 코드 한 줄 건드리지 않은 하위 클래스가 오동작할 수 있다는 말이다.
아래의 예시를 보자
import java.util.HashSet;
public class InstrumentedHashSet extends HashSet<E> {
private int addCount = 0;
public InstrumentedHashSet () {
}
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
public void setAddCount(int addCount) {
this.addCount = addCount;
}
}
위의 코드의 경우 addAll() 메서드에 3개의 크기의 배열을 넣었을 때 addCount의 값은 3이 아니라 6이 된다. 이유는 HashMap의 addAll() 메서드는 add() 메서드를 사용해서 구현되었는데, 이때의 add() 메서드는 Overried된 InsttrumentedHashSet의 add() 메서드이기 때문이다.
addAll메서드를 다른식으로 재정의할 수도 있다. 하지만 여전히 상위 클래스의 메서드 동작을 다시 구현하는 방식은 어렵고, 시간도 더 들고, 자칫 오류를 내거나 성능을 떨어뜨릴 수도 있다.
하위 클래스가 깨지기 쉬운 이유는 더 있다. 다음 릴리즈에서 상위 클래스에 새로운 메서드를 추가한다면 어떨까? 보안 때문에 컬렉션에 추가된 모든 원소가 특정 조건을 만족해야만 하는 프로그램을 생각해보자. 그 컬렉션을 상속하여 원소를 추가하는 모든 메서드를 재정의해 필요한 조건을 먼저 검사하게끔 하면 될 것 같다. 하지만 이 방식이 통하는 것은 상위 클래스에 또 다른 원소 추가 메서드가 만들어지기 전까지다. 다음 릴리스에서 우려한 일이 생기면, 하위 클래스에서 재정의하지 못한 그 새로운 메서드를 사용해 '허용되지 않은' 원소를 추가할 수 있게 된다. 실제로도 컬렉션 프레임워크 이전부터 존재하던 Hashtable과 Vector를 컬렉션 프레임워크에 포함시키자 이와 관련한 보안 구멍들을 수정해야 하는 사태가 벌어졌다.
위의 문제를 해결하는 방법은 기존 클래스를 확장하는 대신, 새로운 클래스를 만들과 private 필드로 기존 클래스이 인스턴스를 참조하게 하는 것이다. 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻에서 이러한 설계를 컴포지션이라 한다. 새 클래스의 인스턴스 메서드들은 기존 크래스의 대응하는 메서드를 호출해 그 결과를 반환한다. 이 방식을 전달이라 하며, 새 클래스의 메서드들을 전달 메서드라 부른다. 그 결과 새로운 클래스는 기존 클래스의 내부 구현 방식의 영향에서 벗어나며, 심지어 기존 클래스에 새로운 메서드가 추가되더라도 전혀 영향받지 않는다.
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) {this.s = s;};
public void clear() { s.clear(); }
public boolean contains(Object o) { return s.contains(o); }
public boolean isEmpty() { return s.isEmpty(); }
public int size() { return s.size(); }
public Iterator<E> iterator() {
return s.iterator();
}
public Object[] toArray() {
return new Object[0];
}
public <T> T[] toArray(T[] a) {
return s.toArray(a);
}
public boolean add(E e) {
return s.add(e);
}
public boolean remove(Object o) {
return s.remove(o);
}
public boolean containsAll(Collection<?> c) {
return s.containsAll(c);
}
public boolean addAll(Collection<? extends E> c) {
return s.addAll(c);
}
public boolean retainAll(Collection<?> c) {
return s.retainAll(c);
}
public boolean removeAll(Collection<?> c) {
return s.removeAll(c);
}
@Override
public boolean equals(Object o) {
return s.equals(o);
}
@Override
public int hashCode() {
return s.hashCode();
}
@Override
public String toString() {
return s.toString();
}
}
import java.util.Collection;
import java.util.Set;
public class InstrumentedSet<E> extends ForwardingSet<E>{
private int addCount = 0;
public InstrumentedSet(Set<E> s) {
super(s);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
}
하나는 집합 클래스이고, 다른 하나는 전달 메서드만으로 이뤄진 재사용 가능한 전달 클래스다.
임의의 Set에 계측 기능을 덧씌워 새로운 Set으로 만드는 것이 이 클래스의 핵심이다. 상속 방식은 구체 클래스 각가을 따로 확장해야 하며, 지원하고 싶은 상위 클래스의 생성자 각각에 대응하는 생성자를 별도로 정의해줘야 한다. 하지만 위와 같은 컴포지션 방식은 한 번만 구현해두면 어떠한 Set 구현체라도 계측할 수 있으며, 기존 생성자들과도 함께 사용할 수 있다.
다른 Set 인스턴스를 감싸고 있다는 뜻에서 InstrumentedSet 같은 클래스를 래퍼 클래스라 하며, 다른 Set에 계측 기능을 덧씌운다는 뜻에서 데코레이션 패턴 이라고 한다.
컴포지션 대신 상속을 사용하기로 결정하기 전에 마지막으로 자문해야하는 질문은 다음과 같다.
- 확장하려는 클래스의 API에 아무런 결함이 없는가?
- 결함이 있다면, 이 결함이 여러분 클래스의 API까지 전파돼도 괜찮은가?
item19 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라
메서드를 재정의하면 어떤 일이 일어나는지를 정확히 정리하여 문서로 남겨야 한다. 달리 말하면, 상속용 클래스는 재정의 할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야한다. 클래스의 API로 공개된 메서드에서 클래스 자신의 또 다른 메서드를 호출할 수도 있다. 그런데 마침 호출되는 메서드가 재정의 가능 메서드라면 그 사실을 호출하는 메서드의 API 설명에 적시해야 한다.
API 문서의 메서드 설명 끝에서 종종 "Implementation Requirements"로 시작하는 절을 볼 수 있는데, 그 메서드의 내부 동작 방식을 설명하는 곳이다. 이 절은 메서드 주석에 @implSpec 태그를 붙여주면 자바독 도구가 생성해준다. 다음은 AbstractCollection에서 발췌한 예다.
/**
* {@inheritDoc}
*
* @implSpec
* This implementation iterates over the collection looking for the
* specified element. If it finds the element, it removes the element
* from the collection using the iterator's remove method.
*
* <p>Note that this implementation throws an
* {@code UnsupportedOperationException} if the iterator returned by this
* collection's iterator method does not implement the {@code remove}
* method and this collection contains the specified object.
*
* @throws UnsupportedOperationException {@inheritDoc}
* @throws ClassCastException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
이 설명에 따르면 iterator 메서드를 재정의하면 remove 메서드의 동작에 영향을 줌을 확실히 알 수 있다. iterator 메서드로 얻은 반복자의 동작이 remove 메서드의 동작에 주는 영향도 정확히 설명했다. 아이템 18에서는 HashSet을 상속하여 add를 재정의한 것이 addAll에까지 영향을 준다는 사실을 알 수 없었는데 아주 대조적이다.
효율적인 하위 클래스를 큰 어려움 없이 만들 수 있게 하려면 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있다. 드물게는 protected 필드로 공개해야 할 수도 있다.
/**
* Removes from this list all of the elements whose index is between
* {@code fromIndex}, inclusive, and {@code toIndex}, exclusive.
* Shifts any succeeding elements to the left (reduces their index).
* This call shortens the list by {@code (toIndex - fromIndex)} elements.
* (If {@code toIndex==fromIndex}, this operation has no effect.)
*
* <p>This method is called by the {@code clear} operation on this list
* and its subLists. Overriding this method to take advantage of
* the internals of the list implementation can <i>substantially</i>
* improve the performance of the {@code clear} operation on this list
* and its subLists.
*
* @implSpec
* This implementation gets a list iterator positioned before
* {@code fromIndex}, and repeatedly calls {@code ListIterator.next}
* followed by {@code ListIterator.remove} until the entire range has
* been removed. <b>Note: if {@code ListIterator.remove} requires linear
* time, this implementation requires quadratic time.</b>
*
* @param fromIndex index of first element to be removed
* @param toIndex index after last element to be removed
*/
protected void removeRange(int fromIndex, int toIndex) {
ListIterator<E> it = listIterator(fromIndex);
for (int i=0, n=toIndex-fromIndex; i<n; i++) {
it.next();
it.remove();
}
removeRange 메서드가 없다면 하위 클래스에서 clear 메서드를 호출하면 제곱에 비례해 성능이 느려지거나 부분 리스트의 메커니즘을 밑바닥부터 새로 구현해야 했을 것이다. 만만치 않은 일이다.
상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 '유일'하다.
상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다.
public final class Sub extends Super{
private final Instant instant;
Sub() {
instant = Instant.now();
}
@Override
public void overrideMe(){
System.out.println(instant);
}
}
위의 프로그램에서는 상위 클래스의 생성자가 하위 클래스이 생성자가 인스턴스를 초기화하기도 전에 overrideMe를 호출하여 NULL을 출력한다.
item20 추상 클래스보다는 인터페이스를 우선하라
추상 클래스, 인터페이스의 가장 큰 차이는 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다는 점이다. 자바는 단일 상속만 지원하니, 추상 클래스 방식은 새로운 타입을 정의하는데 커다란 제약을 안게 되는 셈이다.
인터페이스는 믹스인 정의에 안성맞춤이다. 믹스인이란 클래스가 구현할 수 있는 타입으로, 믹스인을 구현한 클래스에 원래의 '주된 타입' 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다. 예컨대 Comparable은 자신을 구현한 클래스의 인스턴스끼리는 순서를 정할 수 있다고 선언하는 믹스인 인터페이스다.
item21 인터페이스는 구현하는 쪽을 생각해 설계하라
자바 8에서는 기존 인터페이스에 메서드를 추가할 수 있도록 디폴트 메서드를 추가했지만 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어려운 법이다.
아래의 Collection 인터페이시에 추가된 디폴트 메서드를 보자.
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
위의 함수는 Predicate가 true를 반환하는 모든 원소를 제거한다.
그러나 SynchronizedCollection 클래스와 같은 경우 자바 8과 함께 사용한다면 다시 말해 removeIf의 디폴트 구현을 물려받게 된다면 정상적으로 작동하지 못한다.
자바 플랫폼 라이브러리에서도 이런 문제를 예방하기 위해 일련의 조치를 취했다. 예를 들어 구현한 인터페이스의 디폴트 메서드를 재정의하고, 다른 메서드에서는 디폴트 메서드를 호출하기 전에 필요한 작업을 수행하도록 했다.
이 아이템의 핵심은 인터페이스를 설계할 때는 디포틀 메서드라는 도구가 생겼어도 여전히 세심한 주의를 기울여야 한다는 것이다.
item22 인터페이스는 타입을 정의하는 용도로만 사용하라
클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에게 이야기해주는 것이다. 인터페이스는 오직 이 용도로만 사용해야 한다.
item23 태그 달린 클래스보다는 클래스 계층구조를 활용하라
두 가지 이상의 의미를 표현할 수 있으며, 그중 현재 표현하는 의미를 태그 값으로 알려주는 클래스를 본 적이 있을 것이다. 다음 코드를 보자.
public class Figure {
enum Shape {RECTANGLE, CIRCLE};
final Shape shape;
}
위의 코드처럼 태그를 사용하는 클래스에는 단점이 한가득이다.
- 열거 타입 선언, 태그 필드, switch와 같이 쓸데없는 코드가 많다.
- 가독성이 나빠진다.
- 메모리를 많이 사용한다.
- 사용하지 않는 필드를 초기화하는 불필요한 코드가 늘어난다.
- 새로운 태그를 추가할 때 마다 코드가 늘어난다.
태그 달린 클래스는 클래스 계층구조를 어설프게 흉내낸 아류일 뿐이다. (ㄷㄷ) 아래는 클래스 계층구조를 사용하여 만든 객체이다.
abstract class Figure {
abstract double area();
}
class Circle extends Figure {
final double radius;
Circle(double radius) {this.radius = radius; };
@Override
double area() {
return Math.PI * (radius * radius);
}
}
class Rectangle extends Figure {
final double length;
final double width;
Rectangle (double length, double width){
this.length = length;
this.width = width;
}
@Override
double area() {
return length * width;
}
}
item24 멤버 클래스는 되도록 static으로 만들라
중첩 클래스란 다른 클래스 안에 정의된 클래스를 말한다. 중첩 클래스는 자신을 감싼 바깥 클래스에서만 쓰여야 하며, 그 외의 쓰임새가 있다면 톱레벨 클래스로 만들어야 한다. 다음은 중첩 클래스의 종류이다.
- 정적 멤버 클래스
- 멤버 클래스
- 익명 클래스
- 지역 클래스
정적 멤버 클래스는 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할 수 있다는 점만 제외하고는 일반 클래스와 똑같다.
정적 멤버 클래스와 비정적 멤버 클래스이 구문상 차이는 단지 static이 붙어 있고 없고 뿐이지만, 의미상 차이는 의외로 꽤 크다. 비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다. 그래서 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this를 사용해 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있다.
item24 톱 레벨 클래스는 한 파일에 하나만 담으라
소스 파일 하나에 톱레벨 클래스를 여러 개 선언하게 된다면 한 클래스를 여러 가지로 정의할 수 있으며, 그중 어느 것을 사용할지는 어느 소스 파일을 먼저 컴파일하냐에 따라 달라지기에, 큰 위험이 있다. 아래의 예시를 보자
public class Main {
public static void main(String[] args) {
System.out.println(Utensil.NAME + Dessert.NAME);
}
}
class Utensil {
static final String NAME = "pan";
}
class Dessert {
static final String NAME = "cake";
}
위의 코드를 실행하면 문제없이 pancake이 출력된다. 그러나 같은 객체를 담은 다른 파일을 만들었다고 가정해보자.
class Utensil {
static final String NAME = "pot";
}
class Dessert {
static final String NAME = "pie";
}
이렇게 된다면, 컴파일 방법에 따라 코드의 출력이 달라진다.
'programming > Effective Java' 카테고리의 다른 글
[Effective Java] Chapter 3 (1) | 2025.01.25 |
---|---|
[Effective Java] Chapter 2 (3) | 2025.01.18 |