📌 12장 : 직렬화

아이템 85. 자바 직렬화의 대안을 찾으라

  • 1997년, 자바에 직렬화 도입
  • 직렬화의 근본적인 문제
    • 공격 범위가 넓고, 지속적으로 넓어져 방어가 어렵다.
    • ObjectInputStream의 readObject 메서드를 호출하면 객체 그래프가 역직렬화 되기 떄문
  • 가젯(gadget)
    • 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메서드
  • 역직렬화 폭탄(deserialization bomb)
    • 역직렬화에 시간이 오래 걸리는 짧은 스트림을 역직렬화하다 서비스 거부 공격에 쉽게 노출될 수 있다.
  • 직렬화 문제 회피 방법
    • 아무것도 역직렬화하지 않는다.
    • 객체와 바이트 시퀀스를 변환해주는 다른 메커니즘을 사용한다.
    • 크로스-플랫폼 구조화된 데이터 표현(cross-platform structured-data representation)
  • 크로스-플랫폼 구조화된 데이터 표현
    • 자바 직렬화보다 간단하다.
    • 임의 객체 그래프를 자동으로 직렬화/역직렬화 하지 않는다.
    • 속성-값 쌍의 집합으로 구성된 간단하고 구조화된 데이터 객체를 사용한다.
    • ex. JSON, 프로토콜 버퍼(protocol buffers)
      • JSON : 텍스트 기반이라 사람이 읽을 수 있다.
      • 프로토콜 버퍼 : 이진 표현이라 효율이 좋다.
      • JSON은 데이터를 표현하는 데 사용, 프로토콜 버퍼는 문서를 위한 스키마(타입)을 제공한다.
      • 프로토콜 버퍼는 사람이 읽을 수 있는 텍스트 표현(pbtxt)도 지원한다.

아이템 86. Serializable 을 구현할지는 신중히 결정하라

  • 클래스의 인스턴스를 직렬화 하기 위해서는 클래스 선언에 implements Serializable 붙이기
  • 선언은 쉬워보이지만 Serializable 을 구현하면 릴리즈한 뒤에 수정이 어렵다.
    • Serializable 구현 후, 직렬화된 바이트 스트림 인코딩은 공개 API가 된다.
  • Serializable 구현은 버그와 보안 구멍이 생길 위험이 높아진다.
    • 객체는 생성자를 사용해 만드는데, 직렬화는 기본 메커니즘을 우회하는 객체 생성 기법
    • 역직렬화를 사용하면 불변식 깨짐과 허가되지 않은 접근에 노출된다.
  • Serializable 구현은 신버전 릴리즈할 때 테스트할 것이 늘어난다.
    • 직렬화 수정 후, 구버전으로 역직렬화 할 수 있는지 반대도 가능한지 확인해야 한다.
  • 상속용으로 설계된 클래스는 대부분 Serializable을 구현하면 안된다. 인스턴스도 대부분 안된다.
  • 내부 클래스는 직렬화를 구현하면 안된다.

아이템 87. 커스텀 직렬화 형태를 고려해보라

  • 수정이 어렵기 떄문에 고민한 후에 괜찮다고 판단될 때만 기본 직렬화 형태 사용하기
  • 객체의 물리적 표현과 논리적 내용이 같으면 기본 직렬화 형태도 무방하다.
  • 차이가 클 때 사용시 문제점
    • 공개 API가 현재의 내부 표현 방식에 영구히 묶인다.
    • 너무 많은 공간을 차지할 수 있다.
    • 시간이 너무 많이 걸릴 수 있다.
    • 스택 오버플로를 일으킬 수 있다.

아이템 88. readObject 메서드는 방어적으로 작성하라

  • readObject는 어떤 바이트 스트림이 넘어와도 유효한 인스턴스를 만들어야 한다.
  • 안전한 readObject 메서드를 작성하는 방법
    • private 이어야 하는 객체 참조 필드는 각 필드가 가리키는 객체를 방어적으로 복사해야 한다.
    • 모든 불변식을 검사해서 어긋나는 게 발견되면, InvalidObjectException 을 던진다.
    • 역직렬화 후 객체 그래프 전체의 유효성을 검사해아 하면, ObjectInputValidation 인터페이스를 사용해라
    • 재정의할 수 있는 메서드를 호출하지 않는다.

아이템 89. 인스턴스 수를 통제해야 한다면 readResolve 보다는 열거 타입을 사용하라

  • readResolve 를 인스턴스 통제 목적으로 사용하려면 객체 참조 타입 인스턴스 필드는 모두 transient 으로 선언해야 한다.
    • 그렇게 하지 않으면, readResolve 메서드가 수행되기 전에 역직렬화된 객체의 참조를 공격할 수도 있다.
  • 직렬화 가능한 인스턴스 통제 클래스를 열거 타입을 이용해 구현하면 선언한 상수 외의 다른 객체는 존재하지 않는 것을 보장해준다.
  • 열거 타입 싱글턴 예시
public enum Elvis { 
    INSTANCE;
    private String[] favoriteSongs =
        { "Hound Dog", "Heartbreak Hotel" };
    public void printFavorites() {
        System. out.println(Arrays.toString (favoriteSongs));
    } 
}

아이템 90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라

  • Serializable 으로 구현하면, 생성자 이외의 방법으로 인스턴스를 생성
    • 버그와 보안 문제가 일어날 수 있다.
    • 직렬화 프록시 패턴(serialization proxy pattern) 으로 해결
  • 직렬화 프록시 패턴
    • 바깥 클래스의 논리적 상태를 정밀하게 표현하는 중첩 클래스를 설계해서 private static 으로 선언한다.
    • 중첩 클래스의 생성자는 1개
    • 바깥 클래스를 매개변수로 받아야 한다.
    • 생성자는 단순히 인수로 넘어온 인스턴스 데이터를 복사
    • 일관성 검사, 방어적 복사도 필요 없다.
    • 바깥 클래스, 직렬화 프록시 모두 Serializable 를 구현한다고 선언해야 한다.
  • 직렬화한 Period 클래스 예시
private static class SerializationProxy implements Serializable { 
    private final Date start;
    private final Date end;
    
    Serializationproxy(Period p) { 
        this.start = p.start; 
        this.end = p.end;
    }
    private static final long serialVersionUID = 
            234098243823485285L; // 아무 값이나 상관없다. (아이템 87)
}

// 바깥 클래스에 추가 : 직렬화 프록시 패턴용 writeReplace 메서드
// 직렬화가 이뤄지기 전에 바깥 클래스 인스턴스를 직렬화 프록시로 변환
private Object writeReplace() {
    return new SerializationProxy(this); 
}

// 직렬화 프록시 패턴용 readObject 메서드
private void readObject(ObjectlnputStream stream) 
        throws InvalidObjectException {
    throw new InvalidObjectException("프록시가 필요합니다.");
}

// Period.SerializationProxy용 readResolve 메서드
// 역직렬화 시에 직렬화 시스템이 직렬화 프록시를 다시 바깥 클래스의 인스턴스로 변환
private Object readResolve() {
  return new Period(start, end); // public 생성자를 사용한다.
}

📌 11장 : 동시성

아이템 78. 공유 중인 가변 데이터는 동기화해 사용하라

  • synchronized
    • 해당 메서드나 블록을 한 번에 한 스레드씩 수행하도록 보장
    • 일반적인 동기화
      • 한 객체가 일관된 상태를 가지고 생성, 객체에 접근하는 메서드는 그 객체에 lock 을 건다.
      • lock 을 건 메서드는 객체의 상태를 확인하고 필요하면 수정한다.
    • 동기화의 중요한 기능
      • 일관성이 깨진 상태를 볼 수 없게 해주고
      • 동기화된 메서드나 블록에 들어간 스레드가 같은 락의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다.
  • 동기화는 배타적 실행뿐만 아니라, 스레드 사이의 안정적인 통신에 필요하다.
  • 다른 스레드를 멈추는 올바른 방법
    • 첫 번째 스레드는 자신의 boolean 필드를 폴링하면서 값이 true가 되면 멈춘다.
    • 필드를 false로 초기화하고, 다른 스레드에서 이 스레드를 멈추려고 할 때 true 로 변경
  • 적절한 스레드 종료 방법
public class StopThread {
    private static boolean stopRequested;
    
    private static synchronized void requeststop() { 
        stopRequested = true;
    }
    
    private static synchronized boolean stopRequested() { 
        return stopRequested;
    }
    
    public static void main(String[] args) 
        throws InterruptedException {
            Thread backgroundThread = new Thread(() -> { 
                int i = 0;
                while (!stopRequested()) 
                    i++;
            }); 
            backgroundThread.start();
            
            TimeUnit.SECONDS.sleep(1);
            requestStop();
    } 
}
  • 쓰기 메서드(requestStop), 읽기 메서드(stopRequested) 모두 동기화 해야 한다.
  • 둘 다 동기화되지 않으면, 동작을 보장하지 않는다.
  • volatile
    • 배타적 수행과는 상관없이 항상 가장 최근에 기록된 값을 읽게 해준다.
    • 반복문에서 동기화 하는 비용을 더 빠르게 하는 방법
private static volatile boolean stopRequested;
  • 주의해야 하는 상황
    • 증가 연산자는 코드상으로 하나지만, 실제로 nextSerialNumber 필드에 두 번 접근한다.
    • 먼저 값을 읽고, 증가한 새로운 값을 저장한다.
    • 두 번째 스레드가 이 두 접근 사이를 들어와 값을 읽게 되면 첫 번째 스레드와 같은 값을 받게 된다.
    • 안전 실패(safety failure) : 프로그램이 잘못된 결과를 계산하는 오류
    • 해결 방법 : generateSerialNumber 메서드에 synchronized 한정자를 붙인다.
      • 동시 호출을 해도 이전 호출이 변경한 값을 읽게 해준다.
      • synchronized을 사용하면 nextSeriaINumber 필드에서 volatile 를 제거해주어야 한다.
private static volatile int nextSeriaINumber = 0;

public static int generateSerialNumber() { 
        return nextSerialNumber++;
}
  • 위의 문제들을 피하는 가장 좋은 방법은 처음부터 가변 데이터를 공유하지 않도록 한다.
  • 가변 데이터는 단일 스레드에서만 사용하도록 한다.

아이템 79. 과도한 동기화는 피하라

  • 과도한 동기화는 성능이 저하되고, 교착상태에 빠질 수 있다.
  • 응답 불가와 안전 실패를 피하기 위해서 동기호 메서드나 동기화 블록 안에서 제어를 클라이언트에 양도하지 않기
    • 동기화된 영역 안에 재정의할 수 있는 메서드를 호출하면 안된다.
    • 클라이언트가 넘겨준 함수 객체를 호출하면 안된다.
  • 외계인 메서드 호출을 동기화 블록 바깥으로 옮겨준다.
private void notifyElementAdded(E element) { 
    List<SetObserver<E>> snapshot = null; 
    synchronized(observers) {
      snapshot = new ArrayList<>(observers);
    }
    for (SetObserver<E> observer : snapshot)
        observer.added(this, element);
}
  • 열린 호출(open call)
    • 동기화 영역 바깥에서 호출되는 외계인 메서드
    • 외계인 메서드는 언제까지 실행될지 알 수 없는데, 동기화 영역 안에서 호출되면 다른 스레드는 사용 대기를 해야 한다.
    • 열린 호출은 실패 방지 효과 이외에 동시성 효율을 개선해준다.
  • 더 좋은 방법은 자바의 동시성 컬렉션 라이브러리 CopyOnWriteArrayList가 있다.
    • ArrayList를 구현한 클래스
    • 내부의 배열이 수정되지 않아 순회할 때 락이 필요 없어 속도가 매우 빠르다.
    • 순회만 일어나는 관찰자 리스트 용도로 좋다.
private final List<SetObserver<E>> observers = 
        new CopyOnWriteArrayList<>();
public void addObserver(SetObserver<E> observer) { 
    observers.add(observer);
}
public boolean removeObserver(SetObserver<E> observer) { 
    return observers.remove(observer);
}
private void notifyElementAdded(E element) { 
    for (SetObserver<E> observer : observers)
      observer.added(this, element);
}
  • 기본 규칙
    • 동기화 영역에서 가능한 일을 적게 하기
  • 멀티코어가 일반화 된 오늘날에, 과도한 동기화는 락을 얻는 데 드는 CPU 비용이 아니다.
  • 경쟁하는 낭비 시간, 모든 코어가 메모리를 일관되게 보기 위한 지연 시간

아이템 80. 스레드보다는 실행자, 태스크, 스트림을 애용하라

  • java.util.concurrent
    • 실행자 프레임워크라고 하는 인터페이스 기반의 유연한 태스크 실행 기능을 담고 있다.
    • 간단한 작업 큐 사용 가능
ExecutorService exec = Executors.newSingleThreadExecutor();
exec.execute(runnable); // 실행자에 실행할 태스크를 넘기는 법
exec.shutdown(); // 실행자를 종료시키는 법
  • 큐를 2개 이상의 스레드가 처리하게 하려면, 다른 정적 팩토리로 다른 종류의 실행자 서비스(스레드 풀) 생성하기
  • 스레드 풀 스레드 개수는 고정할 수 있고, 늘어나거나 줄어들게 할 수도 있다.
  • 주로 사용하는 실행자는 java.util.concurrent.Executors 에서 사용
  • ThreadPoolExecutor 클래스를 직접 사용해도 된다.
  • 실행자 프레임워크에서는 작업 단위와 실행 메커니즘이 분리된다.
  • 태스크 : 작업 단위를 나타내는 개념
  • 태스크 종류 : Runnable, Callable
    • Callable은 Runnable과 비슷하지만 값을 반환하고 임의의 예외를 던질 수 있다.
  • 실행자 서비스 : 태스크를 수행하는 일반적인 메커니즘
  • 자바 7부터 실행자 프레임워크는 포크-조인(fork-join) 태스크를 지원한다.
  • fork-join 태스크는 fork-join 풀이라는 특별한 실행자 서비스를 실행한다.
    • fork-join 태스크의 인스턴스는 작은 하위 태스크로 나뉠 수 있다.
    • fork-join 풀을 구성하는 스레드들이 태스크들을 처리한다.
    • 일을 먼저 끝낸 스레드는 다른 스레드의 남은 태스크를 가져와서 대신 처리할 수도 있다.
    • 모든 스레드가 움직여 CPU를 최대한 활용한다.
    • 높은 처리량, 낮은 지연시간

아이템 81. wait와 notify보다는 동시성 유틸리티를 애용하라

  • 자바 5부터 고수준 동시성 유틸리티가 wait, notify 대신 일을 처리해준다.
  • java.util.concurrent 의 고수준 유틸리티
    • 실행자 프레임워크
    • 동시성 컬렉션(concurrent collection)
    • 동기화 장치(synchronizer)
  • 동시성 컬렉션
    • List, Queue, Map과 같은 표준 컬렉션 인터페이스에 동시성을 추가한 고성능 컬렉션
    • 동기화를 각자 내부에서 수행해 높은 동시성을 갖춤
    • 동시성 컬렉션에서 동시성을 무력화할 수 없다. 외부에서 락을 추가로 사용하면 속도가 느려진다.
  • ConcurrentMap 으로 구현한 동시성 정규화 맵
public static String intern(String s) { 
    String result = map.get(s);
    if (result = null) {
      result = map.putIfAbsent(s, s); 
      if (result = null)
        result = s;
    }
    return result; 
}
  • Collections.synchronizedMap 보다 ConcurrentHashMap 의 성능이 더 좋다.
    • 동기화된 맵 -> 동시성 맵
  • wait, notify 보다 동시성 유틸리티를 사용하는 것이 더 좋지만
  • 레거시 코드를 유지보수해야 하는 경우, wait은 while문 안에서 호출해야 한다.
  • 일반적인 상황에서 notify보다 notifyAll을 사용해야 한다.
    • 응답 불가 상태에 빠질 수 있기 때문에

아이템 82. 스레드 안전성 수준을 문서화하라

  • 메서드 선언에서 synchronized 은 API에 속하지 않는다.
  • 멀티 스레드 환경에서 API를 안전하게 사용하기 위해서 스레드 안전성 수준을 정확히 명시해야 한다.
  • Collections.synchronizedMap API 문서
// synchronizedMap이 반환한 맵의 컬렉션 뷰를순회하려면 반드시 그 맵을 락으로 사용해 수동으로 동기화하라.

Map<K, V> m = Collections. synchronizedMap(new HashMapo()); 
Set<K> s = m.keySet(); // 동기화 블록 밖에 있어도 된다.
        ...
synchronized(m) { // s가 아닌 m을 사용해 동기화해야 한다! 
  for (K key : s)
    key.f();
}
// 이대로 따르지 않으면 동작을 예측할 수 없다.

아이템 83. 지연 초기화는 신중히 사용하라

  • 지연 초기화(lazy initialization)
    • 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법
    • 정적 필드, 인스턴스 필드에서 사용
    • 주로 최적화에 사용, 클래스와 인스턴스 초기화에 발생하는 위험 순환 문제 해결
    • 클래스, 인스턴스 생성의 초기화 비용은 줄지만, 필드 접근 비용이 커진다.
    • 지연 초기화가 성능을 느리게 할 수도 있다.
  • 멀티 스레드 환경에서는 지연 초기화 필드를 2개 이상의 스레드가 공유하면 반드시 동기화 해야 하기 때문에 구현이 어렵다.
  • 대부분의 상황에서 일반적인 초기화가 지연 초기보다 낫다.
  • 인스턴스 필드 초기화하는 일반적인 방법
private final FieldType field = computeFieldValue();
// 지연 초기화가 초기화 순환성(initialization circularity)을 깨뜨릴 것 같을 때 synchronized 접근자 사용하기

private FieldType field;
private synchronized FieldType getFieId() { 
    if (field = null)
        field = computeFieldValue(); 
    return field;
}
  • 정적 필드를 지연 초기화해야 할 때, 지연 초기화 홀더 클래스(lazy initialization holder class) 관용구 사용
private static class FieldHolder {
static final FieldType field = compiiteFieldValue();
}
private static FieldType getFieId() { return FieldHolder.field; }
  • 인스턴스 필드를 지연 초기화해야 할 때, 이중검사(double-check) 관용구 사용 
private volatile FieldType field;

private FieldType getFieId() {
        FieldType result = field;
        if (result != null) { // 첫 번째 검사 (락 사용 안 함)
            return result;
            
        synchronized(this) {
          if (field = null) // 두 번째 검사 (락 사용)
            field = computeFieldValue(); 
          return field;
        }
}

아이템 84. 프로그램의 동작을 스레드 스케줄러에 기대지 말라

  • 여러 스레드가 실행 중일 때, 운영체제의 스레드 스케줄러는 스레드 실행 순서와 시간을 정한다.
  • 구체적인 스케줄링 정책은 운영체제마다 다르다.
  • 정확성이나 성능이 스레드 스케줄러에 따라 달라지는 프로그램은 다른 플랫폼에 이식하기 어렵다.
  • 견고하고 빠르고 이식성 좋은 프로그램 작성 방법
    • 실행 가능 스레드의 평균 개수를 프로세서 수보다 크게 많아지지 않도록 해야한다.
  • 실행 가능 스레드 수를 적게 유지하는 주요 기법
    • 각 스레드가 유용한 작업 후, 다음 일이 생기기 전까지 대기하기
    • 당장 처리할 작업이 없으면 실행 X
    • ex. 실행자 프레임워크 : 스레드 풀 크기를 적절히 설정하고 작업은 짧게 유지한다.

📌 10장 : 예외

아이템 69. 예외는 진짜 예외 상황에만 사용하라

  • 잘못된 예외상황 : 무한루프를 돌다가 배열에 끝에 도달해서 ArrayIndexOutOfBoundException 발생했을 때 종료
try {
     int i = 0;
     while(true)
        range[i++].climb();
     } catch(ArrayIndexOutOfBoundsException e){
}
  • JVM은 배열에 접근할 때마다 경계가 넘는지 검사한다.
  • 위의 코드에서 같은 일(경계가 넘는지 확인)이 반복된다.
  • 예외를 사용 vs 표준 관용구 : 예외를 사용한 것이 속도가 더 느리다.
  • 예외는 예외 상황에서만 사용하고, 일상적인 제어 흐름용에서는 쓰지 않는다.

아이템 70. 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라

  • 자바 문제 상황을 알리는 타입(throwable)
    • 검사 예외, 런타임 예외, 에러
    • 호출하는 쪽에서 복구해야 하는 상황이면 검사 예외를 사용
    • 검사 예외일 때 복구에 필요한 정보를 알려주는 메서드를 제공해야 한다.
  • 비검사 throwable
    • 런타임 예외, 에러
    • 프로그래밍 오류를 나타낼 때 : 런타임 예외
    • JVM이 자원 부족, 불변식 깨짐 등 더 이상 수행을 할 수 없는 상황일 때 : 에러 사용
    • Error 클래스를 상속해서 하위 클래스를 만들지 않기
      • 비검사 throwable은 모두 RuntimeException 의 하위 클래스여야 한다.

아이템 71. 필요 없는 검사 예외 사용은 피하라

  • 검사 예외는 발생한 문제를 프로그래머가 처리해서 안전성을 높여준다.
  • 과하게 사용하면 쓰기 불편한 API가 될 수 있다.
  • 검사 예외를 회피하는 가장 쉬운 방법은 적절한 결과 타입을 담은 옵셔널을 반환하는 방법이다.
    • 검사 예외를 던지는 대신에 빈 옵셔널 반환하기
    • 단점 : 예외 발생 원인의 부가 정보를 담을 수 없다.
  • 예외를 사용하면 구체적인 예외 타입, 그 타입이 제공하는 메서드를 활용해 부가 정보를 제공할 수 있다.
  • 옵셔널만으로 상황을 처리해서 충분한 정보를 제공할 수 없을 때만 검사 예외를 사용한다.

아이템 72. 표준 예외를 사용하라

  • 표준 예외를 사용하면 사용이 쉽고, 예외 클래스가 적을수록 메모리 사용량이 줄고 클래스 적재 시간도 적게 걸린다.
  • 가장 많이 사용하는 예외 : IllegalArgumentException
    • 호출자가 인수로 부적절한 값을 넘길 때 던지는 예외
    • 반복 횟수를 지정하는 매개변수에 음수를 건낼 때 사용할 수 있다.
  • 자주 사용되는 예외들 
  • Exception, RuntimeException, Throwable, Error은 직접 재사용하지 말아야 한다.
  • 이 예외들은 다른 예외들의 상위 클래스이기 때문에, 여러 성격의 예외들을 포괄해서 안정적으로 테스트할 수 없다.

아이템 73. 추상화 수준에 맞는 예외를 던지라

  • 예외 번역(exception translation)
    • 메서드가 저수준 예외를 처리하지 않았을 때, 관련 없는 예외가 나오는 문제가 생길 수 있다.
    • 문제를 해결하기 위해서, 상위 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔서 던져야 한다.
try {
... // 저수준 추상화를 이용한다.
} catch (LowerLevelException e) {
    // 추상화 수준에 맞게 번역한다.
    throw new HigherLevelException(...);
}
  • 예외 연쇄(exception chaining)
    • 예외를 번역할 때, 저수준 예외가 디버깅에 도움이 될 때 예외 연쇄를 사용하는 것이 좋다.
    • 문제의 근본 원인(cause)인 저수준 예외를 고수준 예외에 실어 보내는 방식
    • 별도의 접근자 메서드(Throwable의 getCause 메서드)를 통해 저수준 예외를 꺼내서 볼 수 있다.
try {
... // 저수준 추상화를 이용한다.
} catch (LowerLevelException e) {
    // 저수준 예외를 고수준 예외에 실어 보낸다.
    throw new HigherLevelException(cause);
}
  • 고수준 예외의 생성자는 상위 클래스의 생성자에 원인을 보내 최종적으로 Throwable 생성자까지 보낼 수 있다.
class HigherLevelException extends Exception { 
    HigherLevelException(Throwable cause) {
        super(cause); 
    }
}

아이템 74. 메서드가 던지는 모든 예외를 문서화하라

  • 검사 예외는 항상 따로 선언하고, 각 예외가 발생하는 상황을 자바독 @throws 태그를 사용해서 문서화한다.
  • 공통 상위 클래스 하나로 뭉뚱그려 선언하지 않기
    • ex. Exception, Throwable을 던진다고 선언하지 않기
    • 예외 상황 : main은 오직 JVM만 호출하기 떄문에 Exception을 던지도록 선언해도 된다.
  • 메서드가 던질 수 있는 예외는 각각 @throws 태그로 문서화
  • 비검사 예외는 메서드 선언의 throws 목록에 넣지 않기

아이템 75. 예외의 상세 메시지에 실패 관련 정보를 담으라

  • 예외를 잡지 못해 프로그램이 실패하면 자바 시스템은 그 예외의 스택 추적(stack trace) 정보를 자동으로 출력한다.
  • 스택 추적 : 예외 객체의 toString 메서드를 호출해 얻는 문자열
/**
* IndexOutOfBoundsException을 생성한다. *
* @param lowerBound 인덱스의 최솟값
* @param upperBound 인덱스의 최댓값 + 1 * @param index 인덱스의 실젯값
*/
public IndexOutOfBoundsException(int lowerBound, int upperBound, int index){
        // 실패를 포착하는 상세 메시지를 생성한다.
        super(String.format(
        "최솟값: %d, 최댓값: %d, 인덱스: %d",
        lowerBound, upperBound, index));

        // 프로그램에서 이용할 수 있도록 실패 정보를 저장해둔다.
        this.lowerBound=lowerBound;
        this.upperBound=upperBound;
        this.index=index;
}

아이템 76. 가능한 한 실패 원자적으로 만드랄

  • 실패 원자적(failure-atomic) 특성
    • 호출된 메서드가 실패해도, 해당 객체는 메서드 호출 전 상태를 유지해야 한다.
  • 메서드를 실패 원자적으로 만드는 방법
    • 불변 객체로 설계 한다.
      • 불변 객체의 상태는 생성 시점에 고정되어 절대 변하지 않는다.
    • 가변 객체의 경우, 작업 수행에 앞서 매개변수 유효성을 검사한다.
      • 객체 내부 상태를 변경하기 전에 잠재적 예외 가능성을 걸러준다.
    • 객체의 임시 복사본에서 작업을 수행하고, 작업이 성공적으로 완료되면 원래 객체와 교체한다.
      • ex. 정렬 수행 전에 입력 리스트의 원소를 배열로 옮겨 담는다.
      • 배열을 사용하면 정렬 알고리즘의 반복문에서 원소들에 더 빠르게 접근 가능하고, 정렬이 실패해도 입력 리스트는 변하지 않는다.
    • 작업 도중 발생하는 실패를 가로채는 복구 코드를 작성해서 작업 전 상태로 되돌린다.
      • 주로 디스크 기반의 내구성(durability)을 보장해야 하는 자료구조에 쓰인다.

아이템 77. 예외를 무시하지 말라

  • 예외 상황을 무시하지 않도록 해야 한다.
  • 예외를 무시해야 하는 경우도 있다.
    • ex. FileInputStream 을 닫을 때
  • 예외를 무시하는 경우, catch 블록 안에 이유에 대해 주석으로 남기고, 예외 변수 이름을 ignored 으로 변경한다.
// catch 블록을 비워두면 예외가 무시된다. 
try {
        ...
        } catch(SomeException e){    
}
 
Future<Integer> f = exec.submit(planarMap::chromaticNumber);
int numColors = 4;
try {
    numColors = f.get(1L, TimeUnit.SECONDS);
} catch (TimeoutException | ExecutionException ignored) {
    // 기본값을 사용한다(색상 수를 최소화하면 좋지만, 필수는 아니다).
}

📌 9장 : 일반적인 프로그래밍 원칙

아이템 57. 지역변수의 범위를 최소화하라

  • 지역변수의 유효 범위를 최소로 줄이면
    • 코드 가독성 증가
    • 유지보수성 증가
    • 오류 가능성 감소
  • 지역변수 범위를 줄이는 방법
    • 가장 처음 쓰일 때 선언하기
    • 대부분 지역변수는 선언과 동시에 초기화한다.
    • 메서드를 작게 유지하고 한 가지 기능에 집중한다.

아이템 58. 전통적인 for 문보다는 for-each 문을 사용하라

  • for-each(enhanced for statement 향상된 for문)
    • 반복자와 인덱스 변수를 사용하지 않는다.
    • 코드가 깔끔하고 오류가 날 일이 없다.
    • 하나의 관용구로 컬렉션과 배열을 모두 처리할 수 있다.
  • for-each를 사용할 수 없는 상황
    • 파괴적인 필터링(destructive filtering)
      • 컬렉션을 순회하면서 선택된 원소를 제거해야 하는 경우, 반복자의 remove 메서드를 호출해야 한다.
      • 자바 8에서 Collection의 removeIf 메서드를 사용해 컬렉션을 명시적 순회하는 일을 피할 수 있다.
    • 변형(transforming)
      • 리스트나 배열을 순회하면서 일부 값이나 전체를 교체해야 하는 경우 리스트의 반복자나 배열의 인덱스를 사용해야 한다.
    • 병렬 반복(parallel iteration)
      • 여러 컬렉션을 병렬로 순회해야 하는 경우 각각의 반복자와 인덱스 변수를 사용해야 한다.

아이템 59. 라이브러리를 익히고 사용하라

  • 표준 라이브러리를 사용해서 크게 관련 없는 문제에 시간을 허비하지 않고, 기능 개발에 집중할 수 있다.
  • 자바 7 부터는 Random 사용을 하지 않는게 좋다. ThreadLocalRandom 으로 대체
    • 포크-조인 풀이나 병렬 스트림에서는 SplittableRandom 사용
  • 알아두면 좋은 API
    • java.lang
    • java.util
    • java.io
    • 이외 하위 패키지
    • java.util.concurrent 동시성 관련

아이템 60. 정확한 답이 필요하다면 float와 double은 피하라

  • flaot, dobule 타입은 과학과 공학 계산용으로 설계
  • 부동 소수점연산에 쓰이기 때문에 정확한 결과가 필요한 경우에는 사용하지 않는다.
  • System.out.printIn(1.03 - 0.42); 코드의 출력 결과
    • 0.6100000000000001 출력
  • 금융 계산과 같이 정확한 계산이 필요한 경우에는 BigDecimal, int, long을 사용해야 한다.
    • BigDecimal의 단점
      • 기본 타입보다 쓰기 불편하고 느리다.
      • int 혹은 long 타입을 사용할 수 있다.
  • 숫자를 9 자리 십진수로 표현할 수 있으면 int
  • 18 자리 십진수로 표현할 수 있으면 long
  • 18 자리가 넘어가면 BigDecimal

아이템 61. 박싱된 기본 타입보다는 기본 타입을 사용하라

  • 자바의 데이터 타입
    • 기본 타입 : int, double, boolean
    • 참조 타입 : String, List
  • 박싱된 기본 타입
    • 각각의 기본 타입에 대응하는 참조 타입
    • int : Integer
    • double : Double
    • boolean : Boolean
  • 기본타입과 박싱된 기본 타입의 차이
    • 기본 타입은 값만 가지고 있고, 박싱된 기본 타입은 값 + 식별성(identity)을 갖는다.
    • 기본 타입의 값은 언제나 유효하지만, 박싱된 기본 타입은 null을 가질 수 있다.
    • 기본 타입이 박싱된 기본 타입보다 시간과 메모리 사용면에서 더 효율적이다.
Comparator<Integer> naturalOrder =
    (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
  • 같은 값 비교시, 0을 출력해야 하지만 1을 출력하는 이유는?
    • 같은 객체를 비교하는 게 아니면 박싱된 기본 타입에 == 연산자를 사용하면 오류 발생
  • 실무에서는 기본 타입을 다루는 비교자가 필요할 때
    • Comparator, naturalOrder() 사용
Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> { 
    int i = iBoxed, j = jBoxed; // 오토박싱
    return i < j ? -1 : (i == j ? 0 : 1);
};
  • 기본 타입과 박싱된 기본 타입을 혼용한 연산에서 박싱된 기본 타입의 박싱은 자동으로 해제된다.
  • null 참조를 다시 언박싱 하게 되면 NullPointerException 이 발생한다.

아이템 62. 다른 타입이 적절하다면 문자열 사용을 피하라

  • 문자열이 다른 값 타입을 대신하기 적합하지 않는 경우
    • 열거 타입 : 상수를 열거할 때 문자열보다 열거 타입이 낫다.
    • 혼합 타입 : 각 요소를 개별로 접근하려면 문자열을 파싱해야 하기 때문에 속도가 느리고 오류가 생길 수 있다.
      • 이런 경우, private 정적 멤버 클래스로 새로 선언하는 것이 낫다.
    • 권한을 표현하는 경우

아이템 63. 문자열 연결은 느리니 주의하라

  • 문자열 연결 연산자 (+)
    • 문자열 연결 연산자를 사용한 문자열 n개를 잇는 시간은 n^2 에 비례한다.
    • 성능 개선을 위해 String 보다 StringBuilder 를 사용한다.

아이템 64. 객체는 인터페이스를 사용해 참조하라

  • 매개변수 타입으로 클래스보다 인터페이스가 더 적합하다.
  • 적합한 인터페이스가 있는 경우, 매개변수 말고도 반환 값, 변수, 필드를 전부 인터페이스 타입으로 선언해야 한다.
  • 객체의 실제 클래스를 사용해야 하는 경우는 생성자로 생성할 경우이다.
  • 예시 : Set 인터페이스를 구현한 LinkedHashSet 변수를 선언
// 좋은 예. 인터페이스를 타입으로 사용했다.
Set<Son> sonSet = new LinkedHashSet();

// 나쁜 예. 클래스를 타입으로 사용했다.
LinkedHashSet<Son> sonSet = new LinkedHashSet();

아이템 65. 리플렉션보다는 인터페이스를 사용하라

  • 리플렉션 기능(java.lang.refelct)을 사용하면 프로그램에서 임의의 클래스에 접근할 수 있다.
  • Class 객체가 주어지는 경우, 클래스의 생성자, 메서드, 필드에 해당하는 Constructor, Method, Field 인스턴스를 가져올 수 있다.
  • 인스턴스들로 클래스의 멤버 이름, 필드 타입, 메서드 시그니처를 가져올 수 있다.
  • 리플렉션 단점
    • 컴파일타임 타입 검사가 주는 이점을 누릴 수 없다.
    • 리플렉션을 이용하면 코드가 지저분해진다.
    • 성능이 저하된다.
  • 리플렉션을 써야 하는 복잡한 애플리케이션이 있지만, 단점 때문에 사용을 줄이고 있다.
  • 리플렉션은 아주 제한된 형태로 사용해야 단점을 피하고 이점을 취할 수 있다.
  • 컴파일 타임에는 알 수 없는 클래스를 사용해야하는 프로그램을 작성해야 할 때
    • 리플렉션은 인스턴스 생성에만 사용하고, 만든 인스턴스는 인터페이스나 상위 클래스로 참조해서 사용한다.

아이템 66. 네이티브 메서드는 신중히 사용하라

  • 자바 네이티브 인터페이스(Java Native Interface, JNI)
    • 자바 프로그램이 네이티브 메서드를 호출하는 기술
    • 네이티브 메서드 : 네이티브 프로그래밍 언어로 작성한 메서드
  • 네이티브 메서드의 쓰임새
    • 레지스트리 같은 플랫폼 특화 기능을 사용한다.
    • 네이티브 코드로 작성된 기존 라이브러리를 사용한다.
    • 성능 개선을 목적으로 성능에 결정적인 영향을 주는 영역만 따로 네이티브 언어로 작성한다.
  • 자바는 점점 하부 플랫폼의 기능을 흡수하고 있기 떄문에 네이티브 메서드 사용이 줄어들고 있다.
    • ex. 자바9에서 process API를 추가해 OS 프로세스에 접근하는 길을 열어줌
  • 네이티브 메서드로 성능 개선이 되는 일은 적기 때문에 되도록 사용하지 않는다.

아이템 67. 최적화는 신중히 하라

  • 빠른 프로그램보다 좋은 프로그램을 작성해야 한다.
  • 성능을 제한하는 설계는 피해야한다.
  • API를 설계할 때 성능에 주는 영향을 고려해라
  • 모든 변경 후에 성능을 측정하라

아이템 68. 일반적으로 통용되는 명명 규칙을 따르라

  • 임의의 타입 T
  • 컬렉션 원소의 타입 E
  • 맵의 키와 값에 K, V
  • 예외 X
  • 메서드 반환 타입 R

📌 8장 : 메서드

아이템 49. 매개변수가 유효한지 검사하라

  • 매개변수 검사를 제대로 하지 않았을 경우
    • 메서드가 수행되는 중간에 모호한 예외를 던지며 실패할 수 있다.
    • 메서드가 잘 수행되지만 잘못된 결과를 반환할 수 있다.
    • 메서드는 수행되지만 어떤 객체를 이상한 상태로 만들어 추후에 메서드와는 관련 없는 오류가 발생할 때
    • 위와 같이 매개변수 검사에 실패하면 실패 원자성(failure atomicity)을 어기게 된다.
  • public과 protected 메서드는 매개변수 값이 잘못되는 경우 던지는 예외들을 문서화 해야 한다.
    • @throws 자바독 태그 사용
    • 예외 종류 : IllegalArgumentException, IndexOutOfBoundsException, NullPointerException
    • 제약을 어겼을 경우의 예외도 추가해야 한다.
public Biginteger mod(Biginteger m) { 
    if (m.signum() <= 0)
        throw new ArithmeticException("계수(m)는 양수여야 합니다. " + m);
    ... // 계산 수행
}
  • private 메서드에서 assert를 사용해 매개변수 유효성 검증
private static void sort(long a[], int offset, int length){
    assert a != null;
    assert offset >= 0 && offset <= a.length;
    assert length >= 0 && length <= a.length - offset;
    ... // 계산 수행
}
  • assert(단언문) 특징
    • 실패하면 AssertionError를 던진다.
    • 런타임에 아무런 효과도, 아무런 성능 저하도 없다.

아이템 50. 적시에 방어적 복사본을 만들라

  • 자바는 네이티브 메서드를 사용하지 않아 C, C++ 같은 언어에서 흔히 볼 수 있는 버퍼 오버런, 배열 오버런, 와일드 포인터 같은 메모리 충돌 오류에서 안전하다.
  • 자바가 다른 클래스의 침범을 모두 막을 수 있는 것은 아니기 때문에, 방어적으로 프로그래밍해야 한다.
  • 어떤 객체든 그 객체의 허락 없이 외부에서 내부를 수정하는 일은 불가능하다.
  • 예외 상황 : 불변식을 지키지 못한 경우
public final class Period {
    private final Date start;
    private final Date end;
    
    public Period(Date start, Date end) { 
        if (start.compareTo(end) > 0)
            throw new IllegalArgumentException( 
                start + "가 " + end + "보다 늦다.");
            this.start = start;
            this.end = end; 
    }
    
    public Date start() { 
        return start;
    }
    
    public Date end() { 
        return end;
    }
    
    ...// 나머지 코드 생략 
}

/* Period 인스턴스 내부 공격 */
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end); 
end.setYear(78); // p의 내부를 수정할 수 있다.
  • Date는 오래된 API이기 때문에 새로운 코드 작성에서는 사용을 하지 않도록 한다.
    • Date 대신 불변인 Instant를 사용하면 된다.
    • 또는 LocalDateTime, ZonedDateTime 사용
  • 외부 공격으로부터 인스턴스 내부를 보호하기 위해서는 생성자에서 받은 가변 매개변수 각각을 방어적 복사(defensive copy)해서 인스턴스 내부에서 원본이 아닌 복사본을 사용한다.
public Period(Date start, Date end) { 
    this.start = new Date(start.getTime()); 
    this.end = new Date(end.getTime());
    
    if (this.start.compareTo(this.end) > 0) 
        throw new IllegalArgumentException(
            this.start + "가 " + this.end + "보다 늦다.");
}
  • 접근자 메서드가 내부의 가변 정보를 직접 드러내는 문제점
    • 가변 필드의 방어적 복사본을 반환한다.
public Date start() {
    return new Date(start.getTime());
}

public Date end() {
    return new Date(end.getTime());
}

아이템 51. 메서드 시그니처를 신중히 설계하라

  • API 설계 요령 리스트
    • 메서드 이름을 신중하게 짓자.
    • 편의 메서드를 너무 많이 만들지 말자.
    • 매개변수 목록은 짧게 유지하자. (4개 이하)
      • 매개변수 목록을 짧게 줄이는 방법
        • 여러 메서드로 쪼갠다.
        • 매개변수 여러 개를 묶어주는 도우미 클래스를 만든다.(정적 멤버 클래스)
        • 객체 생성에 사용한 빌더 패턴을 메서드 호출에 응용한다. 먼저 모든 매개변수를 하나로 추상화한 객체를 정의하고, 클라이언트에서 이 객체의 setter를 호출해 필요한 값을 설정한다.
  • 매개변수 타입으로는 클래스보다 인터페이스가 더 낫다.
    • 클래스를 사용하면 클라이언트에게 특정 구현체만 사용하도록 제한하게 된다.
  • boolean보다 원소 2개짜리 열거 타입이 더 낫다.
    • public enum TemperatureScale { FAHRENHEIT, CELSIUS }

아이템 52. 다중정의는 신중히 사용하라

  • 컬렉션 분류기 프로그램
public class Collectionclassifier {
    public static String classify(Set<?> s) {
        return "집합"; 
    }
    
    public static String classify(List<?> 1st) { 
        return "리스트";
    }
    
    public static String classify(Collection<?> c) { 
        return "그 외";
    }
    
    public static void main(String[] args) { 
        Collection<?>[] collections = {
            new HashSet<String>(),
            new ArrayList<BigInteger>(),
            new HashMap<String, String>().values()
        };
        
        for (Collection<?> c : collections)
            System.out.println(classify(c));
    } 
}
  • 예상 출력 결과는 "집합", "리스트", "그 외" 이지만
  • 실제 출력 결과는 "그 외"를 3번 출력한다.
    • 다중정의(overloading)된 classify는 어느 메서드를 호출할지가 컴파일 타임에 정해지기 때문이다.
    • 컴파일 타임에 for문에 있는 c는 항상 Collection<?> 타입
    • 컴파일타임의 매개변수를 기준으로 classify(Collection<?>) 만 호출한다.
  • 재정의 메서드 호출 메커니즘
class Wine {
    String name() { return "포도주"; }
}

class SparklingWine extends Wine {
    @Override String name() { return "발포성 포도주"; }
}

class Champagne extends SparklingWine { (
    @Override String name() { return "샴페인"; }
}

public class Overriding {
    public static void main(String[] args) {
        List<Wine> wineList = List.of(
            new Wine(), new SparklingWine(), new Champagne());
            
        for (Wine wine : wineList) 
            System.out. printIn(wine.name());
    } 
}
  • 출력 결과 : "포도주", "발포성 포도주", "샴페인"
  • for문에 있는 타입과 무관하게 가장 하위에서 정의한 재정의 메서드가 실행된다.
  • 다중정의 메서드에서는 객체의 런타임 타입이 중요하지 않다.
    • 선택은 컴파일 타임 타입에 의해 이뤄진다.

아이템 53. 가변인수는 신중히 사용하라

  • 가변인수(varargs) 메서드는 명시한 타입의 인수를 0개 이상 받을 수 있다.
    • 메서드를 호출하면, 먼저 인수의 개수와 길이가 같은 배열을 만들고 인수들을 배열에 저장하여 가변인수 메서드에 전해준다.
static int sum(int... args) { 
    int sum = 0;
    for (int arg : args) 
        sum += arg;
    return sum;
}
  • 인수가 1개 이상이어야 하는 가변인수 메서드 : 컴파일 타임이 아닌 런타임에 실패한다는 단점이 있다.
static int min(int... args) { 
    if (args.length = 0)
        throw new IllegalArgumentException("인수가 !개 이상 필요합니다."); 
    int min = args[0];
    for (int i = 1; i < args.length; i++)
        if (args[i] < min) 
            min = args[i];
    return min; 
}
  • 매개변수를 2개 받는 방법을 사용하면 문제를 해결할 수 있다.
static int min(int firstArg, int... remainingArgs) { 
    int min = firstArg;
    for (int arg : remainingArgs)
        if (arg < min) 
            min = arg;
    return min; 
}

아이템 54. null이 아닌, 빈 컬렉션이나 배열을 반환하라

  • null 을 반환하게 되면 API 사용이 어려워지고 오류 처리 코드가 늘어난다.
  • 빈 컬렉션 반환 코드 예시
public List<Cheese> getCheeses() {
    return new ArrayList<>(cheesesInStock);
}

// 최적화 : 빈 컬렉션을 매번 할당하지 않는 코드
public List<Cheese> getCheeses() {
    return cheeseInStock.isEmpty() ? Collections.emptyList()
    : new ArrayList<>(cheesesInStock);
}

아이템 55. 옵셔널 반환은 신중히 하라

  • 자바 8 이전에 값을 반환할 수 없는 경우
    • 예외 던지기
    • null 반환
  • 예외 생성할 경우의 스택 추적 전체를 캡처하는 비용 발생
  • null 값을 갖고있으면 추후에 NullPointerException 발생 위험이 있다.
  • 자바 8 에서 Optional으로 null이 아닌 T 타입 참조를 하나 담거나 아무것도 담지 않을 수 있다.
  • Optional(옵셔널)은 원소를 최대 1개 가질 수 있는 불변 컬렉션
  • 컬렉션에서 최댓값을 구하는 메소드
public static <E extends Comparable<E>> E max(Collection<E> c) { 
    if (c.isEmpty())
        throw new IllegalArgumentException("빈 컬렉션");
    
    E result = null; 
    for (E e : c)
        if (result = null || e.compareTo(resuLt) > 0)
            result = Objects.requireNonNull(e);
    
    return result;
}
  • Optional로 반환하는 경우
public static <E extends Comparable<E>> 
        Optional<E> max(Collection<E> c) { 
    if (c.isEmpty())
        return Optional.empty();
    
    E result = null; 
    for (E e : c)
        if (result = null || e.compareTo(resuLt) > 0)
            result = Objects.requireNonNull(e);
    
    return result;
}
  • 옵셔널을 반환하는 메서드에서는 절대 null 반환하지 않기

아이템 56. 공개된 API 요소에는 항상 문서화 주석을 작성하라

  • API를 올바르게 문서화하기 위해서는 공개된 모든 클래스, 인터페이스, 메서드, 필드 선언에 문서화 주석을 달아야 한다.
  • 메서드용 문서화 주석에는 해당 메서드와 클라이언트 사이의 규약을 명료하게 기술해야 한다.
  • 표준 규약을 지켜서 작성한다.

📌 7장 : 람다와 스트림

아이템 42. 익명 클래스보다는 람다를 사용하라

  • 함수 객체(function object)
    • 이전의 자바에서 함수 타입을 표현할 때 추상메서드를 하난만 담은 인터페이스를 사용했다.
    • 이런 인터페이스의 인스턴스를 함수 객체라고 한다.
    • JDK 1.1 부터 익명 클래스를 사용
    • 자바8에서 함수형 인터페이스라 불리는 인터페이스들의 인스턴스를 람다식을 사용해서 만들 수 있게 됬다.
  • 익명 클래스 사용
Collections.sort(words, new Comparator<String>() { 
  public int compare(String si, String s2) {
    return Integer.compare(si.length(), s2.length()); 
  }
});
  • 람다식 사용
Collections. sort(words,
  (si, s2) -> Integer.compare(si.length(), s2.length()));
  
// 비교자 생성 메서드를 사용
Collections.sort(words, comparinglnt(String::length));

// 자바 8 List 인터페이스에 추가된 sort 메서드 사용
words.sort(comparinglnt(String:: length));
  • 람다와 익명 클래스의 인스턴스를 직렬화하는 일은 하지 않도록 한다.
  • 직렬화해야 하는 경우, private 정적 중첩 클래스의 인스턴스를 사용한다.

아이템 43. 람다보다는 메서드 참조를 사용하라

  • 메서드 참조(method reference)
    • 람다는 익명 클래스보다 간결하다. 메서드 참조는 람다보다 더 간결하게 해준다.

 


아이템 44. 표준 함수형 인터페이스를 사용하라

  • 필요한 용도에 맞는 게 있으면, 직접 구현하지 않고 표준 함수형 인터페이스를 활용하는 것이 좋다.
  • 표준 함수형 인터페이스는 다른 코드와의 상호운용성이 크게 좋아질 것이다.
  • 기본 함수형 인터페이스

  • 직접 만든 함수형 인터페이스는 항상 @FunctionInterface 애너테이션을 사용한다.
  • 애너테이션을 사용하는 이유
    • 해당 클래스의 코드나 설명 문서를 읽는 사람에게 인터페이스가 람다용으로 설계된 것을 알려주기 위해
    • 해당 인터페이스가 추상 메서드 하나만 가지고 있어야 컴파일을 해준다.
    • 유지보수 과정에서 실수로 메서드를 추가하지 못하게 막아준다.

아이템 45. 스트림은 주의해서 사용하라

  • 스트림 API
    • 다량의 데이터 처리 작업을 돕기위해 자바8에서 추가되었다.
    • 스트림(stream) : 데이터 원소의 유한 혹은 무한 시퀀스(sequence)
      • 기본 타입 값 : int, long, double
    • 스트림 파이프라인(stream pipeline) : 원소들로 수행하는 연산 단계를 표현하는 개념
      • 소스 스트림에서 시작해 종단 연산(terminal operation)으로 끝난다.
      • 사이에 하나 이상의 중간 연산(intermediate operation)이 있을 수 있다.
      • 중간 연산은 스트림을 어떠한 방식으로 변환(transform)한다.
      • 스트림 파이프라인은 종단 연산이 호출될 때 지연 평가(lazy evaluation)된다.
  • 스트림을 과하게 사용한 경우 -> 유지보수하기 어려워진다.
public class Anagrams {
    public static void main(String[] args) throws lOException {
        Path dictionary = Paths.get(args[0]);
        int minGroupSize = Integer.parselnt(args[1]);
        
        try (Stream<String> words = Files.lines(dietionary)) { 
            words.collect (
                groupingBy(word -> word.chars().sorted() 
                    .collect(StringBuilder::new,
                        (sb, c) -> sb.append((char) c), 
                        StringBuilder::append).toString()))
            .values().stream()
            .filter(group -> group.size() >= minGroupSize)
            .map(group -> group.size() + ": " + group)
            .forEach(System.out::println);
        }
    }
}

아이템 46. 스트림에서는 부작용 없는 함수를 사용하라

  • 스트림뿐만 아니라 스트림 관련 객체에 건네지는 모든 함수 객체가 부작용이 없도록 사용해야 한다.
  • 종단 연산 중에 forEach는 스트림이 수행한 계산 결과를 보고할 때만 사용해야 한다. 계산할 때는 사용하지 않는다.
  • 수집기(collector)를 사용하면 스트림의 원소를 손쉽게 컬렉션에 모을 수 있다.
    • 수집기 팩터리 종류 : toList, toSet, toMap, groupingBy, joining
List<String> topTen = freq.keyset().stream() 
    .sorted(comparing(freq::get).reversedO) 
    .limit(10)
    .collect(toList());
  • 맵 수집기 이용 : toMap(keyMapper, valueMapper)
    • 스트림 원소를 키에 매핑하는 함수와 값에 매핑하는 함수를 인수로 받는다.
private static final Map<String, Operation> stringToEnum =
    Stream.of(values()).collect(
        toMap(Object::toString, e -> e));
  • groupingBy
    • 입력으로 분류 함수(classifier)를 받고
    • 출력으로는 원소들을 카테고리별로 모아 놓은 맵을 담은 수집기를 반환한다.
    • 분류 함수는 입력받은 원소가 속하는 카테고리를 반환한다.
    • 이 카테고리는 해당 원소의 맵 키(Map key)로 쓰인다
  • joining
    • 문자열 등의 CharSequence 인스턴스의 스트림에만 적용할 수 있다.
    • 매개변수가 없는 joining은 단순히 원소들을 연결하는 수집기를 반환한다.
    • 인수 하나짜리 joining은 CharSequence 타입의 구분문자(delimiter)를 매개변수로 받는다.
    • 연결 부위에 구분문자를 삽입하고, 구분문자로 쉼표를 입력하면 CSV 형태의 문자열을 만들어준다.

아이템 47. 반환 타입으로는 스트림보다 컬렉션이 낫다

  • 스트림은 반복(iteration)을 지원하지 않는다.
  • 스트림과 반복을 알맞게 사용하는 것이 좋은 코드를 만들 수 있다.
  • Stream 인터페이스는 Iterable 인터페이스가 정의한 추상 메서드를 전부 포함하고, Iterable 인터페이스가 정의한 방식대로 동작한다.
  • 하지만 for-each으로 스트림을 반복할 수 없는 이유는 Stream이 Iterable을 확장(extend)하지 않기 때문이다.
  • 원소 시퀀스를 반환하는 메서드를 작성할 때는, 스트림으로 처리하기 원하는 사용자와 반복으로 처리하기 원하는 사용자 양쪽 모두 만족시킬 수 있도록 한다.
  • 반환 전부터 원소들을 컬렉션에 관리하고 있거나 컬렉션에 하나 더 만들어도 될 정도의 원소 개수가 적으면 ArrayList 같은 표준 컬렉션에 담아서 반환한다.
  • 컬렉션 반환이 힘든 경우, Stream이나 Iterable 중 자연스러운 것을 사용한다.

아이템 48. 스트림 병렬화는 주의해서 적용하라

  • 스트림을 잘못 병렬화하면 프로그램을 오동작하거나 성능이 급격히 저하된다.
  • 수정 후에 코드가 정확한지 확인하고 운영 환경과 유사한 조건에서 수행해보며 성능 지표를 관찰해야 한다.
  • 결과적으로 계산이 정확하고 성능이 좋아진 것을 확인하고나서 병렬화 버전을 코드에 반영해야 한다.

📌 6장 : 열거 타입과 애너테이션

아이템 34. int 상수 대신 열거 타입을 사용하라

  • 정수 열거 패턴 단점
    • 타입 안전을 보장할 방법이 없고, 표현력이 좋지 않다.
    • 문자열 출력이 까다롭다.
  • 열거 타입(enum type)
    • 열거 타입 자체는 클래스이다.
    • 상수 하나당 자신의 인스턴스를 만들어 public static final 필드로 공개한다.
    • 밖에서 접근할 수 있는 생성자를 제공하지 않기 때문에 final
    • 열거 타입 선언으로 만들어진 인스턴스는 딱 하나씩 존재하는 것을 보장해준다.

아이템 35. ordinal 메서드 대신 인스턴스 필드를 사용하라

  • 열거 타입 상수는 하나의 정수값과 대응된다.
  • 모든 열거 타입은 해당 상수가 열거 타입에서 몇 번째 위치인지 ordinal이라는 메서드를 제공해준다.
  • 잘못된 사용 예시
    • 상수 선언 순서가 바뀌면 numberOfMusicians 오동작
    • 해결방법 : 열거 타입 상수에 연결된 값은 ordinal 메서드로 얻지 말고 인스턴스 필드에 저장하기
public enum Ensemble {
    SOLO, DUET, TRIO, QUARTET, QUINTET, 
    SEXTET, SEPTET, OCTET, NONET, DECTET;
    
    public int numberOfMusicians() { return ordinal() + 1; } 
}
public enum Ensemble {
    SOLO(l), DUET(2), TRI0(3), QUARTET(4), QUINTET(5), 
    SEXTET(6), SEPTET(7), 0CTET(8), D0UBLE_QUARTET(8), 
    N0NET(9), DECTET(10), TRIPLE_QUARTET(12);
    
    private final int numberOfMusicians;
    Ensemble(int size) { this.numberOfMusicians = size; }
    public int numberOfMusicians() { return numberOfMusicians; }
}​

아이템 36. 비트 필드 대신 EnumSet을 사용하라

  • 비트 필드(bit field)
    • 비트별 OR을 사용해 여러 상수를 하나의 집합으로 모을 수 있는 집합
    • 비트별 연산을 사용해 합집합과 교집합 같은 집합 연산을 효율적으로 수행
    • 정수 열거 상수의 단점을 갖고있다.
  • java.util 패키지의 EnumSet 클래스는 열거 타입의 상수 값으로 구성되 집합을 효과적으로 표현한다.
public class Text {
    public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
    
    // 어떤 Set을 넘겨도 되나, EnumSetoi 가장 좋다.
    public void applyStyles(Set<Style> styles) { ... } 
}​

아이템 37. ordinal 인덱싱 대신 EnumMap을 사용하라

  • 배열이나 리스트에서 원소를 꺼낼 때 ordinal 메서드로 인덱스를 얻는 경우가 있다.
  • 동작은 되지만 배열은 제네릭과 호환이 되지 않아 비검사 형변환을 수행해야 하고 깔끔하게 컴파일 되지 않는다.
  • EnumMap : 열거 타입을 키로 사용하도록 설계한 Map 구현체
Map<Plant.LifeCycle, Set<Plant» plantsByLifeCycle = 
  new EnumMapo(Plant.LifeCycle.class);
for (Plant.LifeCycle Ic : Plant.LifeCycle.values()) 
  plantsByLifeCycle.put(lc, new HashSeto());

for (Plant p : garden)
  plantsByLifeCycle. get (p.lifeCycle).add(p);
System.out.printIn(plantsByLifeCycle);
  • stream과 EnumMap 사용해서 데이터와 열거 타입 매핑
System.out.printIn(Arrays.stream(garden)
  .collect(groupingBy(p -> p.LifeCycle,
    () -> new EnumMap<>(LifeCycle.class), toSet())));​

아이템 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라

  • 타입 안전 열거 패턴은 열거 타입과 다르게 확장할 수 있다.
  • 기본 연산 외에 사용자 확장 연산을 추가해야 하는 경우
    • 연산 코드용 인터페이스를 정의하고 열거 타입이 인터페이스를 구현하게 한다.
    • 하지만 열거 타입끼리 구현을 상속할 수 없다.
public interface Operation {
  double apply(double x, double y);
}

  public enum BasicOperation implements Operation { 
    PLUS("+") {
      public double apply(double x, double y) {return x + y; } 
    MINUS("-") {
      public double apply(double x, double y) {return x - y; } 
    TIMES("*") {
      public double apply(double x, double y) {return x * y; } 
    DIVIDE("/") {
      public double apply(double x, double y) {return x / y; } 
    };
    
    private final String symbol;
    
    BasicOperation(String symbol) { 
      this.symbol = symbol;
    }
    
    @Override public String toString() { 
      return symbol;
    }
  }

아이템 39. 명명 패턴보다 애너테이션을 사용하라

  • 명명 패턴의 단점
    • 실수로 이름을 tsetSafety Override로 지으면 JUnit 3이 메서드를 무시하고 지나치고 테스트가 통과한 것으로 오해할 수 있다.
    • 올바른 프로그램 요소에서만 사용된다는 보증이 없다.
    • 프로그램 요소를 매개변수로 전달할 방법이 없다.
  • 애너테이션
    • JUnit 4에서 도입
    • marker 애너테이션 타입 선언 예시
import j;*ava.lang.annotation.*;

/**
* 테스트 메서드임을 선언하는 애너테이션이다. 
* 매개변수 없는 정적 메서드 전용이다.
*/
@Retention (Retent ionPol.icy .RUNTIME) 
@Target(ElementType.METHOD)
public @interface Test {
}
  • 메타애너테이션(meta-annotation) : 애터테이션 선언에 있는 애너테이션
    • @Test가 런타임에도 유지되어야 한다는 표시

아이템 40. @Override 애너테이션을 일관되게 사용하라

  • 상위 클래스의 메서드를 재정의하려는 모든 메서드에 @Override 애너테이션을 달아야 한다.
  • 예외 : 구체 클래스에서 상위 클래스의 추상 메서드를 재정의 하는 경우

아이템 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라

  • 마커 인터페이스(marker interface) : 아무 메서드를 담고 있지 않고, 자신을 구현하는 클래스가 특정 속성을 가진 것을 표현해주는 인터페이스
    • ex) Serializable : 자신을 구현한 클래스의 인스턴스를 직렬화(serialization)할 수 있도록 알려준다.
  • 마커 인터페이스와 마커 애너테이션
    • 마커 인터페이스는 구현한 클래스의 인스턴스들을 구분하는 타입으로 쓸 수 있다.
    • 마커 애너테이션은 그렇지 않다.
    • 마커 인터페이스는 적용 대상을 더 정밀하게 지정할 수 있다.
    • 마커 애너테이션은 거대한 애너테이션 시스템의 지원을 받는다.
    • 활용
      • 클래스와 인터페이스 외의 프로그램 요소에 마킹하는 경우 -> 마커 애너테이션 사용
      • 새로 추가하는 메서드 없이 타입 정의가 목적인 경우 -> 마커 인터페이스 사용
  • 자바의 직렬화
    • Serializable 마커 인터페이스를 보고 직렬화가 가능한 타입인지 확인

 

📌 5장 : 제네릭

아이템 26. 로 타입은 사용하지 말라

  • 제네릭 타입(generic type)
    • 제네릭 클래스와 제네릭 인터페이스 : 클래스와 인터페이스 선언에 타입 매개변수가 쓰이는 경우
    • 제네릭 타입은 일련의 매개변수화 타입을 정의한다.
    • List : 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입
      • String은 정규 타입 매개변수 E에 해당하는 실제 타입 매개변수
    • 제네릭 타입을 정의하면, 로 타입(raw type)도 정의한다.
  • 로 타입(raw type)
    • 제네릭 타입에서 타입 매개변수를 사용하지 않은 경우
    • List의 로 타입은 List
    • 오류를 발생하고나서 런타임에 문제를 겪는 경우
// 컬렉션의 로 타입
private final Collection stamps = ...;

// 반복자의 로 타입
for (Iterator i = stamps.iterater(); i.hasNext(); ) {
  Stamp stamp = (Stamp) i.next(); // ClassCastException을 던진다.
  stamp.cancel();
}
  • 제네릭을 활용하면 정보를 타입 선언 자체에 녹일 수 있다.
private final Collection<Stamp> stamps = ...;
  • 로 타입을 쓰는 이유는?
    • 기존 코드와 제네릭을 사용하는 새로운 코드가 돌아가도록 해야하는 호환성 때문에
    • List 같은 로 타입은 사용하면 안되지만, List 같은 임의 객체를 허용하는 매개변수 타입은 사용 가능하다.
      아이템 27. 비검사 경고를 제거하라
      • 모든 비검사 경고는 런타임에 ClassCstException을 일으킬 수 있는 잠재적 가능성이 있기 때문에 제거 해야 한다.
      • 경고를 제거할 수 없지만 타입이 안전하다고 확신할 수 있는경우 : @SuppressWarnings("unchecked") 애너테이션을 달아서 경고를 숨긴다.
        • 개별 지역변수 선언부터 클래스 전체까지 어디에나 선언할 수 있지만, 가능한 좁은 범위에 적용한다.

      아이템 28. 배열보다는 리스트를 사용하라
      • 배열과 제네릭 타입의 차이점
        • 배열은 공변(covariant) : Sub가 Super의 하위 타입이면 Sub[]은 Super[]의 하위 타입이 되는 것처럼 같이 변한다.
        • 제네릭은 불공변(invariant) : 타입 Type1, Type2가 있을 경우, List은 List의 하위 타입도 상위 타입도 아니다.
        • 배열은 실체화가 가능하다.
          • 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
          • 제네릭은 타입 정보가 런타임에 소거된다. (원소 타입을 컴파일타입에만 검사한다.)
      • 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.
      • 둘을 같이 쓰다가 컴파일 오류가 생기면 가장 먼저 배열을 리스트로 대체하는 방법을 사용한다.

      아이템 29. 이왕이면 제네릭 타입으로 만들라
      • 클라이언트에서 직접 형변환하는 타입보다 제네릭 타입이 더 안전하고 사용이 간단하다.
      • 새로운 타입을 설계하는 경우 형변환 없이 사용할 수 있도록 제네릭 타입으로 만들어야 하는 경우가 많다.

      아이템 30. 이왕이면 제네릭 메서드로 만들라
      • 클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환하는 메서드보다 제네릭 메서드가 더 안전하고 사용이 간단하다.
      • 타입처럼 메서드도 형변환 없는 것이 편하기 때문에, 제네릭 메서드를 만들도록 한다.

      아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라
      • 불공변 방식보다 유연한 상황이 필요한 경우 : 매개변수화 타입이 불공변인 경우 생기는 오류
      • 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 사용한다.
      •  

 

 


아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라

  • 가변 인수는 메서드를 호출하면 가변 인수를 담기 위한 배열이 자동으로 생성된다.
  • 배열이 클라이언트에 노출이 되어 매개변수에 제네릭이나 매개변수화 타입이 포함되면 컴파일 경고가 발생하게 된다.
  • 매개변수 인수 메서드를 호출할 경우, 매개변수가 실체화 불가 타입으로 추론될 때 경고 형태
    • 매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 힙 오염이 발생한다.
warning: [unchecked] Possible heap pollution from 
    parameterized vararg type List<String>
  • 자바 7에서 @SafeVarargs 애너테이션 추가 : 메서드 작성자가 타입 안전함을 보장하는 장치
  • 제네릭 varargs 매개변수를 안전하게 사용하는 메서드
@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
    List<T> result = new ArrayListo(); 
    for (List<? extends T> list : lists)
        result.addAlKlist); 
    return result;
}
  • 제네릭 varargs 메서드가 안전한 기준
    • varargs 매개변수 배열에 아무것도 저장하지 않는다.
    • 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.

아이템 33. 타입 안전 이종 컨테이너를 고려하라

  • 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다.
  • 컨테이너 자체가 아니라 키를 타입 매개변수로 바꾸면 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다.
  • 타입 토큰 : 타입 안전 이종 컨테이너는 Class를 키로 사용한다.

+ Recent posts