아이템 15. 클래스와 멤버의 접근 권한을 최소화하라

잘 설계된 컴포넌트는 모든 내부 구현을 완벽히 숨겨, 구현과 API를 깔끔히 분리한다.

프로그램 요소의 접근성은 가능한 한 최소한으로 하라.

public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안 된다. public static final 필드가 참조하는 객체가 불변인지 확인하자.

public 가변 필드를 갖는 클래스는 일반적으로 스레드 안전하지 않다.

클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공해서는 안 된다. (내용을 수정할 수 있게 된다)

// 보안 허점이 숨어 있다.
public static final Thing[] VALUES = { ... };
해결책 1. public 불변 리스트 추가
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
해결책 2. 복사본을 반환하는 public 메서드 추가
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> values() {
  return PRIVATE_VALUES.clone();
}

java 9 에서는 모듈 시스템이라는 개념이 도입되었다.

  • 모듈은 패키지들의 묶음
  • 모듈은 자신에 속하는 패키지 중 공개(export)할 것들을 (관례상 module-info.java 파일에) 선언한다.
  • protected 혹은 public 멤버라도 해당 패키지를 공개하지 않았다면 모듈 외부에서는 접근할 수 없다.
  • 모듈 jar 파일을 자신의 모듈 경로가 아닌 애플리케이션의 classpath에 두면 그 모듈 안의 모든 패키지는 마치 모듈이 없는 것처럼 행동한다.(공개 여부와 상관없이 모듈 밖에서 접근할 수 있게 된다)

아이템 16. public 클래스에서는 public 필드가 아닌 접근자 메소드를 사용하라

class Point {
  public double x;
  public double y;
}

이런 클래스는 캡슐화의 이점을 제공하지 못한다.

API를 수정하지 않고는 내부 표현을 바꿀 수 없고, 불변식을 보장할 수 없다.

외부에서 필드에 접근할 때 부수 작업을 수행할 수도 없다.

패키지 바깥에서 접근할 수 있는 클래스라면 접근자를 제공하자.

package-private 클래스 혹은 private 중첩 클래스라면 데이터 필드를 노출한다 해도 하등의 문제가 없다.

  • 이 방식은 클래스 선언 면에서나 이를 사용하는 클라이언트 코드 면에서나 접근자 방식보다 훨씬 깔끔하다.
  • 클라이언트 코드가 이 클래스 내부 표현에 묶이기는 하나, 클라이언트도 어차피 이 클래스를 포함하는 패키지 안에서만 동작하는 코드일 뿐이다.
  • 패키지 바깥 코드는 전혀 손대지 않고도 데이터표현 방식을 바꿀 수 있다.

불편 필드라면 노출해도 덜 위험하지만 완전히 안심할 수는 없다. 하지만 package-private 클래스나 private 중첩 클래스에서는 종종 (불변이든 가변이든) 필드를 노출하는 편이 나을 때도 있다.

아이템 17. 변경 가능성을 최소화하라

클래스를 불변으로 만들기 위한 다섯가지 규칙

객체의 상태를 변경하는 메서드를 제공하지 않는다.

클래스를 확장할 수 없도록 한다.

  • 상속을 막는 대표적인 방법은 클래스를 final로 선언하는 것

모든 필드를 final로 선언한다.

모든 필드를 private으로 선언한다.

자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.

  • 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 그 객체의 참조를 얻을 수 없도록 해야 한다.
  • 이런 필드는 절대 클라이언트가 제공한 객체 참조를 가리키게 해서는 안 되며, 접근자 메서드가 그 필드를 그대로 반환해서도 안 된다.
  • 생성자, 접근자, readObject 메서드(item 88) 모두에서 방어적 복사를 수행하라.

피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴을 함수형 프로그래밍이라 한다.

절자척 혹은 명령형 프로그래밍에서는 메서드에서 피연산자인 자신을 수정해 자신의 상태가 변하게 된다.

메서드 이름으로 (add 같은) 동사 대신 (plus 같은) 전치사를 사용한다. 이는 해당 메서드가 객체의 값을 변경하지 않는다는 사실을 강조하려는 의도다.

이 방식으로 프로그래밍하면 코드에서 불변이 되는 영역의 비율이 높아지는 장점을 누릴 수 있다.

불변 객체는 단순하다.

반면 가변 객체는 임의의 복잡한 상태에 놓일 수 있다.

불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요 없다.

불변 객체에 대해서는 그 어떤 스레드도 다른 스레드에 영향을 줄 수 없으니 불변객체는 안심하고 공유할 수 있다.

따라서 불변 클래스라면 한번 만든 인스턴스를 최대한 재활용하기를 권한다.

  • 정적 팩터리를 제공하자. (item 1)
  • 메모리 사용량과 가비지 컬렉션 비용이 줄어든다.
  • 새로운 클래스를 설계할 때 public 생성자 대신 정적 팩터리를 만들어두면, 클라이언트를 수정하지 않고도 필요에 따라 캐시 기능을 나중에 덧붙일 수 있다.

불변 객체는 자유롭게 공유할 수 있음은 물론, 불변 객체끼리는 내부 데이터를 공유할 수 있다.

객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다.

불변 객체는 그 자체로 실패 원자성(item 76)을 제공한다.

단점. 값이 다르면 반드시 독립된 객체로 만들어야 한다.

  • 원하는 객체를 완성하기까지의 단계가 많고, 그 중간 단계에서 만들어진 객체들이 모두 버려진다면 성능 문제가 더 불거진다.
  • 해결책. 다단계 연산들을 예측하여 기본 기능으로 제공하는 방법

모든 생성자를 priate 혹은 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의 인스턴스를 인수로 받는다면 주의해야 한다. 인수로 받은 객체가 진짜 BigInteger인지 반드시 확인해야 한다.

다시 말해 신뢰할 수 없는 하위 클래스의 인스턴스라고 확인되면, 이 인수들은 가변이라 가정하고 방어적으로 복사해 사용해야 한다.(item 50)


getter가 있다고 해서 무조건 setter를 만들지는 말자.

클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.

성능 때문에 어쩔 수 없다면(item 67) 불변 클래스와 쌍을 이루는 가변 동반 클래스를 public 클래스로 제공하도록 하자.

한편, 모든 클래스를 불변으로 만들 수는 없다.

불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자.

  • 해당 객체의 예측이 쉬워지고 오류가 생길 가능성이 줄어듬

다른 합당한 이유가 없다면 모든 필드는 private final 이어야 한다.

생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.

아이템 18. 상속보다는 컴포지션을 사용하라

상속은 잘못 사용하면 오류를 내기 쉬운 소프트웨어를 만들게 된다.

  • 상위 클래스와 하위 클래스를 모두 같은 프로그래머가 통제하는 패키지 안에서라면 상속도 안전한 방법이다.
  • 확장할 목적으로 설계되었고 문서화도 잘 된 클래스(item 19)도 마찬가지로 안전하다.
  • 하지만 일반적인 구체 클래스를 패키지 경계를 넘어, 즉 다른 패키지의 구체 클래스를 상속하는 일은 위험하다.

메서드 호출과 달리 상속은 캡슐화를 깨뜨린다.

  • 상위 클래스를 릴리스마다 내부 구현이 달라질 수 있으며, 그 여파로 코드 한 줄 건드리지 않은 하위 클래스가 오동작할 수 있다.

메서드 재정의로 인한 문제가 발생할 수 있다.

그렇다면 클래스를 확장하더라도 메서드를 재정의하는 대신 새로운 메서드를 추가하면 괜찮을까?

  • 이 방식이 훨씬 안전한 것은 맞지만, 위험이 전혀 없는 것은 아니다.
  • 다음 릴리스에서 상위 클래스에 새 메서드가 추가됐는데, 운 없게도 하필 내가 하위 클래스에 추가한 메서드와 시그니처가 같고 반환 타입은 다르다면 컴파일 조차 되지 않는다.
  • 상위 클래스의 메서드가 요구하는 규약을 만족하지 못할 가능성이 크다.

다행이 이상의 문제를 모두 피해 가는 묘안이 있다.

기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하자. 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻에서 이러한 설계를 컴포지션이라 한다.

새 클래스의 인스턴스 메서드들은 (private 필드를 참조하는) 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환한다.

새로운 클래스는 기존 클래스의 내부 구현 방식의 영향에서 벗어나며, 심지어 기존 클래스에 새로운 메서드가 추가되더라도 전혀 영향받지 않는다.

// 래퍼 클래스 - 상속 대신 컴포지션을 사용했다.
public class MySet<E> extends ForwardingSet<E>  {
    private int addCount = 0;

    public MySet(Set<E> set) {
        super(set);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> collection) {
        addCount = addCount + collection.size();
        return super.addAll(collection);
    }

    public int getAddCount() {
        return addCount;
    }
}
// 재사용할 수 있는 전달 클래스
public class ForwardingSet<E> implements Set<E> {
    private final Set<E> set;
    public ForwardingSet(Set<E> set) { this.set = set; }
    public void clear() { set.clear(); }
    public boolean isEmpty() { return set.isEmpbty(); }
    public boolean add(E e) { return set.add(e); }
    public boolean addAll(Collection<? extends E> c) { return set.addAll(c); }
    ... 
}

InstrumentedSet은 Set 인터페이스를 구현했고, Set의 인스턴스를 인수로 받는 생성자를 하나 제공한다. 임의의 Set에 계측 기능을 덧씌워 새로운 Set으로 만드는 것이 이 클래스의 핵심이다.

상속 방식은 구체 클래스 각각을 따로 확장해야 하며, 지원하고 싶은 상위 클래스의 생성자 각각에 대응하는 생성자를 별도로 정의해줘야 한다.

하지만 컴포지션 방식은 한 번만 구현해두면 어떠한 Set 구현체라도 계측할 수 있으며, 기존 생성자들과도 함께 사용할 수 있다.

Set<Instant> times = new InstrumentedSet<>(new TreeSet<>(cmp));
Set<E> s = new InstrumentedSet<>(new HashSet<>(INIT_CAPACITY));

InstrumnetedSet을 이용하면 대상 Set 인스턴스를 특정 조건하에서만 임시로 계측할 수 있다.

static void walk(Set<Dog> dogs) {
  InstrumentedSet<Dog> iDogs = new InstrumentedSet<>(dogs);
  ... // 이 메서드에서는 dogs 대신 iDogs를 사용한다.
}

다른 Set 인스턴스를 감싸고 있다는 뜻에서 InstrumentedSet 같은 클래스를 래퍼 클래스라 하며, 다른 Set에 계측 기능을 덧씌운다는 뜻에서 데코레이터 패턴이라고 한다.

컴포지션과 전달의 조합은 넓은 의미로 위임(deligation)이라고 부른다. 단, 엄밀히 따지면 래퍼 객체가 내부 객체에 자기 자신의 참조를 넘기는 경우만 위임에 해당한다.

래퍼 클래스는 콜백 프레임워크와는 어울리지 않는다.

콜백 프레임워크에서는 자기 자신의 참조를 다른 객체에 넘겨서 다음 호출(콜백) 때 사용하도록 한다. 내부 객체는 자신을 감싸고 있는 래퍼의 존재를 모르니 대신 자신(this)의 참조를 넘기고, 콜백 때는 래퍼가 아닌 내부 객체를 호출하게 된다.(SELF 문제)

재사용할 수 있는 전달 클래스를 인터페이스당 하나씩만 만들어두면 원하는 기능을 덧씌우는 전달 클래스들을 아주 손쉽게 구현할 수 있다.

상속은 반드시 하위 클래스가 상위 클래스의 '진짜' 하위 타입인 상황에서만 쓰여야 한다. 다르게 말하면, 클래스 B가 클래스 A와 is-a 관계일 때만 클래스 A를 상속해야 한다. 클래스 A를 상속하는 클래스 B를 작성하려 한다면 "B가 정말 A인가?"하고 자문해보자. 대답이 "아니다"라면 A를 private 인스턴스로 두고, A와는 다른 API를 제공해야 하는 상황이 대다수다. 즉, A는 B의 필수 구성요소가 아니라 구현하는 방법 중 하나일 뿐이다.

컴포지션을 써야 할 상황에서 상속을 사용하는 건 내부 구현을 불필요하게 노출하는 꼴이다.

  • 클라이언트가 노출된 내부에 직접 접근할 수 있어서 문제가 된다.

컴포지션 대신 상속을 사용하기로 결정하기 전에 마지막으로 자문해야 할 질문들

  • 확장하려는 클래스의 API에 아무런 결함이 없는가?
  • 결함이 있다면, 이 결함이 여러분 클래스의 API까지 전파돼도 괜찮은가?

컴포지션으로는 이런 결함을 숨기는 새로운 API를 설계할 수 있지만, 상속은 상위 클래스의 API를 그 결함까지도 그래도 승계한다.


하위 클래스의 패키지가 상위 클래스와 다르고, 상위 클래스가 확장을 고려해 설계되지 않았다면 문제가 될 수 있다.

상속의 취약점을 피하려면 상속 대신 컴포지션과 전달을 사용하자. 특히 래퍼 클래스로 구현할 적당한 인터페이스가 있다면 더욱 그렇다.

래퍼 클래스는 하위 클래스보다 견고하고 강력하다.

아이템 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라

상속용 클래스를 설계하기란 결코 만만치 않다.

상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.

상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다.

클래스 내부에서 스스로를 어떻게 사용하는지(자기사용 패턴) 모두 문서로 남겨야 하며, 일단 문서화한 것은 그 클래스가 쓰이는 한 반드시 지켜야 한다. 그러지 않으면 그 내부 구현 방식을 믿고 활용하던 하위 클래스를 오동작하게 만들 수 있다.

다른 이가 효율 좋은 하위 클래스를 만들 수 있도록 일부 메서드를 protected로 제공해야 할 수도 있다. 그러니 클래스를 확장해야 할 명확한 이유가 떠오르지 않으면 상속을 금지하는 편이 나을 것이다.

상속을 금지하려면 클래스를 final로 선언하거나 생성자 모두를 외부에서 접근할 수 없도록 만들면 된다.

아이템 20. 추상 클래스보다는 인터페이스를 우선하라

골격 구현 클래스

인터페이스로는 타입을 정의하고, 필요하면 디폴트 메서드 몇 개도 함께 제공한다. 그리고 골격 구현 클래스는 나머지 메서드들까지 구현한다. 이렇게 해두면 단순히 골격을 확장하는 것만으로 이 인터페이스를 구현하는 데 필요한 일이 대부분 완료된다. 바로 템플릿 메서드 패턴이다.

관례상 인터페이스 이름이 Interface라면 그 골격 구현 클래스의 이름은 AbstractInterface로 짓는다.

다음 코드는 완벽히 동작하는 List 구현체를 반환하는 정적 팩터리 메서드로, AbstractList 골격 구현으로 활용했다.

static List<Integer> intArrayAsList(int[] a){
    Objects.requiredNonNull(a);

    return new AbstractList<Integer>(){
        @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;
        }
    }
}

골격 구현 작성

가장 먼저 인터페이스를 잘 살펴 다른 메서드들의 구현에 사용되는 기반 메서드들을 선정한다. 이 기반 메서드들은 골격 구현에서는 추상 메서드가 될 것이다.

그 다음으로 기반 메서드들을 사용해 직접 구현할 수 있는 메서드를 모두 디폴트 메서드로 제공한다. (단, equals 와 hashCode 같은 Object메서드는 디폴트 메서드로 제공하면 안된다는 사실을 항상 유념하자)

만약 인터페이스의 메서드 모두가 기반 메서드와 디폴트 메서드가 된다면 골격 구현 클래스를 별도로 만들 이유는 없다.

기반 메서드나 디폴트 메서드로 만들지 못한 메서드가 남아 있다면, 이 인터페이스를 구현하는 골격 구현 클래스를 하나 만들어 남은 메서드들을 작성해 넣는다.

골격 구현 클래스에는 필요하면 public이 아닌 필드와 메서드를 추가 해도 된다.

간단한 예로 Map.Entry 인터페이스를 살펴보자. getKey, getValue는 확실히 기반 메서드이며, 선택적으로 setValue도 포함할 수 있다. 이 인터페이스는 equals 와 hashCode의 동작 방식도 정의해놨다. Object 메서드들은 디폴트 메서드로 제공해서는 안되므로, 해당 메서드들은 모두 골격 구현 클래스에 구현한다.

public abstract class AbstractMapEntry<K, V> implements Map.Entry<K,V>{

    // 변경 가능한 엔트리는 이 메서드를 반드시 재정의해야 한다.
    @Override public V setValue(V value){
        throw new UnsupportedOperationException();
    }

    // 생략.....
    // equals, hashCdoe, toString 구현
}

일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합하다.

복잡한 인터페이스라면 구현하는 수고를 덜어주는 골격 구현을 함께 제공하는 방법을 꼭 고려해보자.

골격 구현은 '가능한 한' 인터페이스의 디폴트 메서드로 제공하여 그 인터페이스를 구현한 모든 곳에서 활용하도록 하는 것이 좋다.

'가능한 한'이라고 한 이유는, 인터페이스에 걸려 있는 구현상의 제약 때문에 골격 구현을 추상 클래스로 제공하는 경우가 더 흔하기 때문이다.

아이템 21. 인터페이스는 구현하는 쪽을 생각해 설계하라

디폴트 메서드를 선언하면, 그 인터페이스를 구현한 후 디폴트 메서드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰이게 된다.

생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기랑 어려운 법이다.

SynchronizedCollection 인스턴스를 여러 스레드가 공유하는 환경에서 한 스레드가 Collection 인터페이스의 디폴트 메서드인 removeIf를 호출하면 ConcurrentModificationException이 발생하거나 다른 예기치 못한 결과로 이어질 수 있다.

자바 플랫폼 라이브러리에서도 이런 문제를 예방하기 위해 일련의 조치를 취했다.

  • 구현한 인터페이스의 디폴트 메서드를 재정의
  • 다른 메서드에서는 디폴트 메서드를 호출하기 전에 필요한 작업을 수행

디폴트 메서드는 (컴파일에 성공하더라도) 기존 구현체에 런타임 오류를 일으킬 수 있다.

기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니면 피해야 한다.

반면, 새로운 인터페이스를 만드는 경우라면 표준적인 메서드 구현을 제공하는 데 아주 유용한 수단이며, 그 인터페이스를 더 쉽게 구현해 활용할 수 있게끔 해준다.(item 20)

인터페이스를 릴리스한 후라도 결함을 수정하는 게 가능한 경우도 있겠지만, 절대 그 가능성에 기대서는 안 된다.

아이템 22. 인터페이스는 타입을 정의하는 용도로만 사용하라

인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다. 인터페이스는 오직 이 용도로만 사용해야 한다.

인터페이스를 상수 공개용 수단으로 사용하지 말자.

// 상수 인터페이스 안티패턴 - 사용금지 !!
public interface PhysicalConstants {
  static final double AVOGADROS_NUMBER = 6.022
}

상수 인터페이스 안티패턴은 인터페이스를 잘못 사용한 예다. 클래스 내부에서 사용하는 상수는 외부 인터페이스가 아니라 내부 구현에 해당한다.

final이 아닌 클래스가 상수 인터페이스를 구현한다면 모든 하위 클래스의 이름공간이 그 인터페이스가 정의한 상수들로 오염되어 버린다.

특정 클래스나 인터페이스와 강하게 연관된 상수일 경우

  • 그 클래스나 인터페이스 자체에 추가해야 한다.
  • enum 타입으로 나타내기 적합한 상수라면 enum 타입으로 만들어 공개하면 된다.(item 34)
  • 그것도 아니라면, 인스턴스화할 수 없는 유틸리티 클래스(item 4)에 담아 공개하자.

유틸리티 클래스의 상수를 빈번히 사용한다면 static import를 하자.

아이템 23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라

// 태그 달린 클래스 - 클래스 계층구조보다 훨씬 나쁘다 !!
class Figure {
    enum Shape { RECTANGLE, CIRCLE };
  // 태그 필드 - 현재 모양을 나타낸다.
    final Shape shape; 

    // 다음 필드들은 모양이 사각형(RECTANGLE)일 때만 쓰인다.
    double length;
    double width;

    // 다음 필드느 모양이 원(CIRCLE)일 때만 쓰인다.
    double radius;

    // 원용 생성자
    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

    // 사각형용 생성자
    Figure(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }

    double area() {
        switch(shape) {
            case RECTANGLE:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default:
                throw new AssertionError(shape);
        }
    }
}

태그 달린 클래스의 단점

  • 장황하고, 오류를 내기 쉽고, 비효율적이다.
  • 클래스 계층구조를 어설프게 흉내낸 아류일 뿐이다.

위의 태그 달린 클래스를 클래스 계층구조로 바꿔보자.

가장 먼저 계층구조의 root가 될 추상 클래스를 정의하고, 태그 값에 따라 동작이 달라지는 메서드들을 root 클래스의 추상 메서드로 선언한다.

그런 다음 태그 값에 상관없이 동작이 일정한 메서드들을 root 클래스에 일반 메서드로 추가한다.

모든 하위 클래스에서 공통으로 사용하는 데이터 필드들도 전부 root 클래스로 올린다.

root 클래스를 확장한 구체 클래스를 의미별로 하나씩 정의한다.

// 태그 달린 클래스를 클래스 계층구조로 변환
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; }
}

클래스 계층구조는 간결하고 명확하며, 쓸데없는 코드도 모두 사라졌다.

살아 남은 필드들은 모두 final이다.

각 클래스의 생성자가 모든 필드를 남김없이 초기화하고 추상 메서드를 모두 구현했는지 컴파일러가 확인해준다.

또한, 타입 사이의 자연스러운 계층 관계를 반영할 수 있어서 유연성은 물론 컴파일타임 타입 검사 능력을 높여준다는 장점도 있다.


태그 달린 클래스를 써야 하는 상황은 거의 없다. 새로운 클래스를 작성하는 데 태그 필드가 등장한다면 태그를 없애고 계층구조로 대체하는 방법을 생각해보자.

아이템 24. 멤버 클래스는 되도록 static으로 만들라

정적 멤버 클래스 && 비정적 멤버 클래스

정적 멤버 클래스는 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할 수 있다는 점만 제외하고는 일반 클래스와 똑같다.

정적 멤버 클래스는 흔히 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스로 쓰인다.

Operation 열거 타입은 Calculator 클래스의 public 정적 멤버 클래스가 되어야 한다.

비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다. 그래서 정규화된 this를 사용해 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있다.

따라서 개념상 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들어야 한다.

비정적 멤버 클래스는 어댑터를 정의할 때 자주 쓰인다. 즉, 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 사용하는 것이다.

// 비정적 멤버 클래스의 흔한 쓰임 - 자신의 반복자 구현
public class MySet<E> extends AbstractSet<E> {
  ... // 생략

  @Override public Iterator<E> iterator() {
    return new MyIterator();
  }

  private class MyIterator implements Iterator<E> {
    ...
  }
}

멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 부여서 정적 멤버 클래스로 만들자.

  • 숨은 외부 참조 시에 시간과 공간이 소비됨.
  • 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수가 생길 수 있다.(item 7)

익명 클래스

멤버와 달리, 쓰이는 시점에 선언과 동시에 인스턴스가 만들어진다.

오직 비정적인 문맥에서 사용될 때만 바깥 클래스의 인스턴스를 참조할 수 있다.

선언한 지점에서만 인스턴스를 만들 수 있다.

여러 인터페이스를 구현할 수 없다.

짧지 않으면 가독성이 떨어진다.

람다를 지원하기 전에는 즉석에서 작은 함수 객체나 처리 객체를 만드는 데 익명 클래스를 주로 사용했다.

또 다른 쓰임은 정적 팩터리 메서드를 구현할 때다.(코드 20-1)

지역 클래스

네 가지 중첩 클래스 중 가장 드물게 사용된다.

지역변수를 선언할 수 있는 곳이면 실질적으로 어디서든 선언할 수 있고, 유효 범위도 지역변수와 같다.

멤버 클래스처럼 이름이 있고 반복해서 사용할 수 있고, 익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있으며, 정적 멤버는 가질 수 없으며, 가독성을 위해 짧게 작성해야 한다.


중첩 클래스에는 네 가지가 있으며, 각각의 쓰임이 다르다.

메서드 밖에서도 사용해야 하거나 메서드 안에 정의하기엔 너무 길다면 멤버 클래스로 만든다.

멤버 클래스의 인스턴스 각각이 바깥 인스턴스를 참조한다면 비정적으로, 그렇지 않으면 정적으로 만들자.

중첩 클래스가 한 메서드 안에서만 쓰이면서 그 인스턴스를 생성하는 지점이 단 한 곳이고 해당 타입으로 쓰기에 적합한 클래스나 인터페이스가 이미 있다면 익명 클래스로 만들고, 그렇지 않으면 지역 클래스로 만들자.

item 25. 톱레벨 클래스는 한 파일에 하나만 담으라

소스 파일 하나에는 반드시 톱레벨 클래스(혹은 톱레벨 인터페이스)를 하나만 담자.

이 규칙만 따른다면 컴파일러가 한 클래스에 대한 정의를 여러 개 만들어내는 일은 사라진다.

소스 파일을 어떤 순서로 컴파일하든 바이너리 파일이나 프로그램의 동작이 달라지는 일은 결코 일어나지 않을 것이다.

+ 따끈한 최근 게시물