📌 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를 키로 사용한다.

📌 4장 : 클래스와 인터페이스

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

  • 잘 설계된 컴포넌트 : 모든 내부 구현을 완벽히 숨겨, 구현과 API를 깔끔하게 분리한다.
  • 클래스, 인터페이스, 멤버가 의도치 않게 API로 공개되는 일이 없도록 해야 한다.
  • public 클래스에서 public static final 이외에 어떤 public 필드가 있어서는 안된다.
  • public static final 필드가 참조하는 객체가 불변인지 확인한다.
  • 정보 은닉의 장점
    • 시스템을 구성하는 컴포넌트들을 서로 독립시켜서 개발, 테스트, 최적화, 적용, 분석, 수정을 개별적으로 할 수 있게 해주는 것과 연관되어 있다.
    • 시스템의 개발 속도를 높인다. (여러 컴포넌트를 병렬로 개발)
    • 시스템 관리 비용을 낮춘다. (각 컴포넌트 교체 부담이 적다.)
    • 성능 최적화에 도움을 준다.
    • 소프트웨어 재사용성을 높인다. 외부에 거의 의존하지 않고, 독자적으로 동작 가능한 컴포넌트는 낯선 환경에서도 유용하게 쓰일 수 있다.
    • 큰 시스템을 제작하는 난이도를 낮춰준다.
  • 접근 제어 메커니즘
    • 클래스, 인터페이스, 멤버의 접근성(접근 허용 범위)을 명시한다.
    • 각 요소의 접근성은 선언된 위치와 접근 제한자로 정해진다.
    • 접근 제한자를 제대로 활용하는 것이 정보 은닉의 핵심
  • 기본 원칙
    • 모든 클래스와 멤버의 접근성을 가능한 좁혀야 한다.
  • private : 멤버를 선언한 톱레벨 클래스에서만 접근 가능
  • package-private : 멤버가 소속된 패키지 안의 모든 클래스에서 접근할 수 있다.
    • 클라이언트에 피해 없이 다음 릴리스에서 수정, 교체, 제거 가능
  • protected : package-private의 접근 범위를 포함하며, 이 멤버를 선언한 클래스의 하위 클래스에서도 접근 가능
  • public : 모든 곳에서 접근 가능. 공개 API, 하위 호환을 위해 계속 관리 필요
  • 상위 클래스의 메서드를 재정의할 때는 접근 수준을 상위 클래스에서보다 좁게 설정할 수 없다.
    • 상위 클래스의 인스턴스는 하위 클래스의 인스턴스로 대체해 사용할 수 있어야 한다.(리스코프 치환 원칙)
  • public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다.
    • 필드가 가변 객체를 참조하거나 final이 아닌 인스턴스 필드를 public으로 선언하면 그 필드에 담을 수 있는 값을 제한할 힘을 잃는다.
    • 꼭 필요한 구성요소의 상수일 경우 public static final 필드로 공개해도 된다.
  • 클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공하면 안된다.
    • 길이가 0이 아닌 배열은 모두 변경이 가능하다.
    • 클라이언트에서 이런 접근자를 제공하면 그 배열의 내용을 수정할 수 있게 되기 떄문이다.
  • 첫 번째 방법 : public 배열을 private으로 만들고 public 불변 리스트를 추가한다.
private static final Thing[] PRIVATE_VALUES = { ... }; 
public static final List<Thing> VALUES =
    Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
  • 두 번째 방법 : 배열을 private 으로 만들고 복사본을 반환하는 public 메서드를 추가(방어적 복사)
private static final Thing[] PRIVATE_VALUES = { ... }; 
public static final Thing[] values() {
    return PRIVATE_VALUES.clone(); }

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

  • 데이터 필드에 직접 접근할 수 있지만, 캡슐화의 이점을 제공할 수 없는 상황
class Point {
    public double x;
    public double y; 
}

필드들을 모두 private으로 바꾸고 public 접근자(getter) 추가

class Point {
    private double x; 
    private double y;
    
    public Point(double xf double y) { 
        this.x = x;
        this.y = 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; }
}
  • 접근자와 변경자(mutator) 메서드를 활용해 데이터를 캡슐화한다.
  • package-private 클래스 혹은 private 중첩 클래스에서는 데이터 필드를 노출해도 문제가 없다.
  • 패키지 바깥 코드는 변경하지 않아도 데이터 표현 방식을 바꿀 수 있다.

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

  • 불변 클래스 : 인스턴스 내부 값을 수정할 수 없는 클래스
    • 가변 클래스보다 설계, 구현, 사용이 간단하고 오류가 생길 여지가 적다.
  • 자바 플랫폼 라이브러리의 불변 클래스 : String, 기본 타입의 박싱된 클래스, BigInteger, BigDecimal
  • 불변 클래스 만드는 5가지 규칙
    • 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
    • 클래스 확장을 할 수 없게 한다. ex. final
    • 모든 필드를 final으로 선언한다.
    • 모든 필드를 private으로 선언한다.
    • 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
      • 클래스에 가변 객체를 참조하는 필드가 있으면, 클라이언트에서 객체를 참조할 수 없게 해야 한다.
      • 생성자, 접근자, readObject 메서드에서 방어적 복사를 수행해라
  • 함수형 프로그래밍
    • 피연산자에 함수를 적용해서 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴
    • 절차적 혹은 명령형 프로그래밍은 메서드에서 피연산자인 자신을 수정해 자신의 상태가 변한다.
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 double realPartf) { return re; } 
    public double imaginaryPart() { return im; }

    public Complex plus(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }
    
    ...
}
  • 불변 객체는 근본적으로 스레드 안전하여 따로 동기화가 필요없다.
  • 불변 클래스는 한 번 만든 인스턴스를 최대한 재활용한다.
  • 가장 쉬운 재활용 방법 : 자주 쓰이는 값들을 상수로 제공한다. ex. public static final
  • 불변 클래스는 자주 사용되는 인스턴스를 캐싱해서 같은 인스턴스를 중복 생성하지 않도록 정적 팩터리를 제공할 수 있다.
  • 정적 패터리를 사용하면 여러 클라이언트가 인스턴스를 공유하여 메모리 사용량과 가비지 컬렉션 비용을 줄어든다.
  • public 생성자 대신에 정적 팩터리를 만들어두면, 클라이언트를 수정하지 않아도 캐시 기능을 덧붙일 수 있다.
  • 방어적 복사도 필요 없기 때문에 clone 메서드나 복사 생성자를 제공하지 않는 것이 좋다.
  • 불변 객체는 자유롭게 공유할 수 있고, 불변 객채꼐리 내부 데이터를 공유할 수 있다.
  • 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다.
  • 불변 객체는 그 자체로 실패 원자성을 제공한다.
  • 단점으로는 값이 다르면 반드시 독립된 객체로 만들어야 한다는 점이다.
  • 중간 단계에서 만들어진 객체들이 모두 버려지는 경우 성능 문제
    • 문제 대처 방법 : 다단계 연산(multistep operation) 들을 예측해서 기본 기능으로 제공하는 방법
      • 클라이언트가 원하는 복잡한 연산들을 정확하게 예측하면 package-private의 가변 동반 클래스만으로도 충분하다.
  • 모든 클래스를 불변으로 만들 수 없다. 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄인다.
  • 다른 합당한 이유가 없으면 모든 필드는 private final 이어야 한다.
  • 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.

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

  • 상속 : 클래스가 다른 클래스를 확장하는 구현 상속
  • 메서드 호출과 다르게 상속은 캡슐화를 깨뜨린다.
    • 상위 클래스에 따라 하위 클래스의 동작에 이상이 생길 수 있다.
  • 잘못된 상속 예시 : addAll 메서드로 원소 3개를 더했을 때
InstrumentedHashSet<String> s = new InstrumentedHashSeto();
s.addAlKList.of("틱", "탁탁", "펑,,));
  • getAddCount 메서드를 호출하면 3을 반환해야 하지만 6을 반환한다.
  • HashSet의 addAll 메서드가 add 메서드를 사용해 구현되어서 addCount에 값이 중복해서 더해져서 최종 값이 6으로 늘어났다.
  • 하위 클래스에서 addAll 메서드를 재정의하지 않으면 문제를 고칠 수 없다.
public class InstrumentedHashSet<E> 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.sizeO;
        return super.addAll(c);
    }
    
    public int getAddCount() { 
        return addCount;
    }
}
  • 컴포지션(composition) : 기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하는 방법
  • 새 클래스의 인스턴스 메서드들은 기존 클래스의 대응하는 메서드를 호출해서 결과를 반환한다.
  • 래퍼 클래스
    • 다른 Set 인스턴스를 감싸고 있는 뜻에서 래퍼 클래스라고 한다.
    • 다른 계측 기능을 덧씌운다는 뜻에서 데코레이터 패턴(decorator pattern) 이라고도 부른다.
    • 단점이 거의 없지만, 래퍼 클래스가 콜백(callback) 프레임워크와는 어울리지 않는다는 점을 주의해야 한다.
public class InstrumentedSet<E> extends Fon시ardingSet<E> {
    private int addCo나nt = 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);
    }
    
    public int getAddCount() { 
    return addCount;
}
  • 재사용할 수 있는 전달(forwarding) 클래스
public class ForwardingSet<E> implements Set<E> { 
    private final Set<E> s;
    public ForwardingSet(Set<E> s) { this.s = s; }
    
    public void clear() { s.clearO; }
    public boolean contains(Object o) { return s.contains(o); }
    public boolean isEmpty() { return s.isEmpty(); }
    public int size() { return s.size(); }
    public Iterator<E> iterater() { return s.iterator(); }
    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.addAH(c); } 
    public boolean removeAll(Collection<?> c){ return s. removeAH(c); } 
    public boolean retainAlKCollection<?> c){ return s. retainAlKc); } 
    public Object[] toArrayO { return s.toArray(); } 
    public <T> T[] toArray(T[] a) { return s.toArray(a); } 
    @Override public boolean equals(Object o){ return s.equals(o); } 
    @Override public int hashCode() { return s.hashCodef); } 
    @Override public String toString() { return s.toString(); }
}

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

  • 상속용 클래스는 재정의할 수 있는 메소드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다.
  • 내부 메커니즘을 문서로 남기는 것이 상속을 위한 설계의 전부는 아니다.
  • 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있따.
  • 상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 유일하다.
  • clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다.
  • clone이 잘못되면 복제본말고도 원본 객체에도 피해를 줄 수 있다.
  • 상속용으로 설계하지 않은 클래스는 상속을 금지하는 것이 나을 수 있다.
  • 상속을 금지하는 방법 2가지
    • 클래스를 final으로 선언하는 방법
    • 모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩터리를 만들어주는 방법
      • 생성자 모두를 외부에서 접근할 수 없도록 만든다.

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

  • 자바가 제공하는 다중 구현 메커니즘 : 인터페이스, 추상클래스
  • 두 메커니즘 공통점 : 인스턴스 메서드를 구현 형태로 제공 가능
  • 차이점 : 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다.
    • 추상 클래스 방식은 새로운 타입을 정의하는 데 커다란 제약이 생긴다.
    • 인터페이스 방식은 선언한 메서드를 모두 정의하고 일반 규약을 잘 지킨 클래스라면 어떤 클래스를 상속해도 같은 타입으로 취급된다.
  • 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해 넣을 수 있다.
  • 인터페이스가 요구하는 메서드를 추가하거나 클래스 선언에 implements 구문만 추가하면 된다.
  • 추상 클래스 방식은 클래스 계층 구조에 혼란을 일으켜 새로 추가된 추상 클래스의 모든 자손이 상속을 하게 되는 문제가 생길 수 있다.
  • 인터페이스는 믹스인(mixin) 정의에 안성맞춤이다.
  • 믹스인 : 클래스가 구현할 수 있는 타입.
    • 믹스인을 구현한 클래스에 원래의 '주된 타입' 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다.
    • 대상 타입의 주된 기능에 선택적 기능을 혼합한다고 해서 믹스인이라고 부른다.
  • 인터페이스로 계층 구조가 없는 타입 프레임워크를 만들 수 있다.
  • 타입을 계층적으로 정의하면 개념을 구조적으로 잘 표현 가능하지만, 표현이 어려운 개념도 있다.
public interface Singer { 
    AudioClip sing(Song s);
}

public interface Songwriter {
    Song compose(int chartPosition);
}
  • 작곡도 하는 가수가 있기 때문에 가수 클래스가 Singer와 Songwriter 모두를 구현해도 문제가 되지 않는다.
  • 가능한 조합 전부를 각각의 클래스로 정의한 복잡한 계층 구조가 만들어질 수 있다.
  • 래퍼 클래스 관용구와 함께 사용하면 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 된다.
  • 인터페이스와 추상 골격 구현(skeletal implementation) 클래스를 함께 제공하는 방법으로 인터페이스와 추상 클래스 장점을 모두 취하는 방법도 있다.
  • 템플릿 메서드 패턴 : 인터페이스로 타입 정의, 필요하면 디폴트 메서드도 제공, 골격 구현 클래스는 나머지 메서드까지 구현한다.
  • 인터페이스 이름이 Interface 이면, 골격 구현 클래스 이름은 AbstractInterface
  • 골격 구현을 사용해 완성한 구체 클래스 예시
static List<Integer> intArrayAsList(int[] a) {
    Objects.requireNonNull(a);
    
    // 다이아몬드 연산자를 이렇게 사용하는 건 자바 9부터 가능하다. 
    // 더 낮은 버전을 사용한다면 <Integer>로 수정하자.
    return new AbstractListo() {
        @Override public Integer get(int i) { 
            return a[i]; // 오토박싱(아이템 6)
        }

        @Override public Integer set(int i, Integer val) { 
            int oldVal = a[i];
            a[i] = val; // 오토언박싱
            return oldVal; // 오토박싱
        }
        
        @Override public int size() { 
            return a.length;
        } 
    };
}

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

  • 자바 8 이전에는 추가된 메서드가 우연히 기존 구현체에 이미 존재할 가능성이 낮기 때문에, 기존 구현체를 깨뜨리지 않으면 인터페이스에 메소드를 추가할 방법이 없었다.
  • 자바 8 이후부터는 기존 인터페이스에 메서드를 추가할 수 있도록 디폴트가 생겼지만, 위험이 아예 없어진 것은 아니다.
    • 디폴트 메서드를 선언하면, 인터페이스 구현 후 디폴트 메서드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰이게 된다.
    • 모든 기존 구현체들이 매끄럽게 연동되는 보장이 없다.
  • 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드 작성은 어렵다.
  • 디폴트 메서드는 컴파일은 성공해도, 기존 구현체에 런타임 오류를 일으킬 수 있다.
  • 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 것이 아니면 피해야 한다.
  • 새로운 인터페이스를 만드는 경우에는 표준적인 메서드 구현을 제공하는 데 유용한 수단이 된다.

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

  • 인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할
  • 자신의 인스턴스로 무엇을 할 수 있는지 클라이언트에게 알려줄 수 있다.
  • 지침에 맞지 않는 예시 : static final 필드로만 가득찬 인터페이스
public interface PhysicalConstants {
    // 아보가드로 수 (1/몰)
    static final double AVOGADROS_NUMBER = 6.022_140_857e23; 
    
    // 볼츠만 상수 (J/K)
    static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
    
    // 전자 질량 (kg)
    static final double ELECTRON_MASS = 9.109_383_56e-31; 
}

아이템 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);
        }
    }
}
  • 태그가 있는 클래스 단점
    • 열거 타입 선언, 태그 필드, switch 문 등등 쓸데없는 코드가 너무 많아 가독성이 나쁘다.
    • 많은 메모리 사용
    • 필드를 final로 선언하려면 쓰이지 않는 필드도 생성자에서 초기화해야 한다.
    • 태그 달린 클래스는 장황하고, 오류가 생기기 쉽고, 비효율적이다.
  • 자바와 같은 객체지향 언어에서는 타입 하나로 다양한 의미의 객체를 표현하는 나은 수단을 제공한다.
    • 클래스 계층 구조를 활용하는 서브타이핑(subtyping)
  • 태그 달린 클래스를 클래스 계층 구조로 변환
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; } 
}

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

  • 중첩 클래스(nested class)
    • 다른 클래스 안에 정의된 클래스
    • 자신을 감싼 바깥 클래스에서만 사용해야 한다.
    • 종류 : 정적 멤버 클래스. (비정적) 멤버 클래스, 익명 클래스, 지역 클래스
    • 정적 멤버 클래스를 제외한 나머지는 내부 클래스(inner class)
  • 정적 멤버 클래스
    • 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에 접근할 수 있다.
    • 바깥 클래스와 함께 쓰일 때 유용한 public 도우미 클래스로 쓰인다.
    • 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있으면 정적 멤버 클래스로 만들어야 한다.
  • 비정적 멤버 클래스
    • 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this를 사용해 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있다.
    • 정규화된 this : 클래스명.this
    • 바깥 인스턴스 없이 생성할 수 없다.
    • 비정적 멤버 클래스의 인스턴스 안에 관계 정보가 만들어져 메모리 공간을 차지하고, 생성 시간이 더 걸린다.
    • 어댑터 정의에 주로 사용된다.
    • 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없으면 정적 멤버 클래스로 만들어준다.
  • 익명 클래스
    • 쓰이는 시점에 선언과 동시에 인스턴스가 만들어진다.
    • 선언한 지점에서만 인스턴스를 만들 수 있고, 클래스 이름이 필요한 작업은 수행할 수 없다.
    • 여러 인터페이스를 구현할 수 없고, 인터페이스를 구현하는 동시에 다른 클래스를 상속할 수 없다.
    • 주로 사용되는 경우는 팩터리 메서드 구현
  • 지역 클래스
    • 가장 드물게 사용되는 중첩 클래스
    • 지역변수를 선언할 수 있는 곳이면 어디서든 선언 가능
    • 유효 범위도 지역변수와 같다.
    • 멤버 클래스처럼 이름이 있고 반복해서 사용 가능
    • 익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있고, 정적 멤버는 가질 수 없고, 가독성을 위해 짧게 작성해야 한다.

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

  • 컴파일러가 한 클래스에 대한 정의를 여러 개 만들지 않도록, 소스 파일 하나에 톱레벨 클래스를 하나만 담는다.

📌 3장 : 모든 객체의 공통 메서드

아이템 10. equals는 일반 규약을 지켜 재정의하라

  • 아래의 상황 중 하나에 해당하면 재정의하지 않는 것이 좋다.
    • 각 인스턴스가 본질적으로 고유하다.
    • 인스턴스의 '논리적 동치성(logical equality)'을 검사할 일이 없다.
    • 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다.
    • 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없다.
  • 값 클래스 = Integer와 String 처럼 값을 표현하는 클래스
  • equals를 재정의해야 하는 경우?
    • 주로 값 클래스가 해당된다.
    • 값이 같은 인스턴스가 둘 이상 만들어지지 않음을 보장하는 인스턴스 통제 클래스면 equals 재정의를 하지 않아도 된다.
  • Object 명세 규약
    • equals 메서드가 쓸모 있으려면 모든 원소가 같은 동치류에 속한 어떤 원소와도 서로 교환할 수 있어야 한다.
      • 반사성(reflexivity) : 자기 자신과 같아야 한다.
      • 대칭성(symmetry) : 두 객체는 서로에 대한 동치 여부에 똑같이 답해야 한다.
      • 추이성(transitivity) : 첫 번째 객체와 두 번째 객체가 같고, 두 번째 객체와 세 번째 객체가 같으면, 첫 번째 객체와 세 번째 객체도 같아야 한다.
      • 일관성(consistency) : 두 객체가 같으면 앞으로도 영원히 같아야 한다.
      • null-아님

아이템 11. equals를 재정의하려거든 hashCode도 재정의하라

  • hashCode도 재정의하지 않으면, hashCode 일반 규약을 어기게 되어 해당 클래스의 인스턴스를 컬렉션의 원소로 사용할 때 문제가 생긴다.
  • Object 명세 규약
    • equals 비교에 사용되는 정보가 변경되지 않았으면, 애플리케이션이 실행되는 동안 그 객체의 hashCode 메서드는 몇 번 호출해도 일관되게 항상 같은 값을 반환해야 한다.
    • equals(Object)가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.
    • equals(Object)가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다.
  • hashCode 재정의를 잘못하게 되면, 2번째 조항에 문제가 생긴다.
  • 논리적으로 같은 객체는 같은 해시코드를 반환해야 한다.
  • AutoValue 프레임워크를 사용하면 equals, hashCode를 자동으로 만들어준다.

아이템 12. toString을 항상 재정의하라

  • toString 일반 규약
    • 간결하면서 사람이 읽기 쉬운 형태의 유익한 정보를 반환해야 한다.
    • 모든 하위 클래스에서 이 메서드를 재정의하라고 한다.
  • 실전에서 toString은 그 객체의 모든 주요 정보를 반환하는 게 좋다. ex. 전화번호
  • toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공하자.
  • 모든 구체 클래스에서 Object의 toString을 재정의하자. (상위 클래스에서 이미 알맞게 재정의한 경우는 예외)

아이템 13. clone 재정의는 주의해서 진행하라

  • Cloneable : 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스(mixin interface)
  • 문제점 : clone 메서드가 선언된 곳이 Cloneable이 아닌 Object + protected
    • Cloneable을 구현한다고 외부 객체에서 clone 메서드를 호출할 수 없다.
  • Cloneable 인터페이스는 Object의 protected 메서드인 clone 동작 방식을 결정한다.
  • 인스턴스에서 clone을 호출하면 그 객체의 필드들을 복사한 객체를 반환하고, 그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 던진다.
  • 실무에서 Cloneable을 구현한 클래스는 clone 메서드를 public으로 제공하고, 사용자는 당연히 복제가 제대로 이뤄지 않아야 한다.
  • clone 메서드는 사실상 생성자와 같은 효과를 낸다.
  • clone은 원본 객체에 아무런 해를 끼치지 않고 복제된 객체의 불변식을 보장해야 한다.
@Override public Stack cloneO { 
  try {
    Stack result = (Stack) super.clone(); 
    result.elements = elements.clone(); 
    return result;
  } catch (CloneNotSupportedException e) { 
    throw new AssertionError();
  } 
}
Entry deepCopy() {
  return new Entry(key, value,
    next = null ? null : next.deepCopy());
}
  • 연결리스트를 복제하는 방법으로, deepCopy를 사용하는 것은 좋지 않다.
  • 재귀 호출 때문에 리스트의 원소 수만큼 스택 프레임을 소비하기 때문에 리스트가 길면 스택 오버플로를 일으킬 위험이 있다.
  • 이 문제를 피하려면, deepCopy를 재귀 호출 대신 반복자를 써서 순회하는 방법으로 수정한다.
Entry deepCopy() {
  Entry result = new Entry(key, value, next);
  for (Entry p = result; p.next != null; p = p.next)
    p.next = new Entry(p.next.key, p.next.value, p.next.next); 
  return res나It;
}
  • 생성자에서는 재정의 될 수 있는 메서드를 호출하지 않아야 한다.
    • clone 메소드에서도 하위 클래스에서 재정의한 메소드를 호출하면, 하위 클래스는 복제 과정에서 자신의 상태를 수정할 기회가 없어지기 때문에 원본과 복제본의 상태가 달라질 수 있다.
    • put(key, value) 메서드는 final 혹은 private 이어야 한다.
  • 상속용 클래스는 Cloneable을 구현하면 안된다.
@Override
protected final Object cloneO throws CloneNotSupportedException {
   throw new CloneNotSupportedException(); 
}
  • 기본 원칙은 '복제 기능은 생성자와 팩터리를 이용하는게 좋다.'
  • 배열은, clone 메서드 방식이 가장 깔끔하다.

아이템 14. Comparable을 구현할지 고려하라

  • Comparable 인터페이스의 메서드 compareTo
  • equals와의 차이점은?
    • compareTo는 단순 동치성 비교에 더해 순서 비교도 가능하고, 제너릭하다.
  • compareTo는 equals와 다르게, 타입이 다른 객체를 신경쓰지 않아도 된다.
  • 타입이 다르면 ClassCastException을 던져도 된다.
  • 비교를 활용하는 클래스의 예시
    • 정렬된 컬렉션 : TreeSet, TreeMap
    • 검색과 정렬 알고리즘을 활용하는 유틸리티 클래스 : Collections, Arrays
  • compareTo 메서드 작성 요령
    • Comparable은 타입을 인수로 받는 제네릭 인터페이스 이기 때문에, compareTo 메서드의 인수 타입은 컴파일 타임에 정해진다.
    • null을 인수로 넣어 호출하면 NullPointerException을 던져야 한다.
    • 각 필드가 동치인지 비교하는 게 아니라 순서를 비교한다.
    • compareTo 메서드에서 필드 값을 비교할 때 <와 > 연산자는 쓰지 말아야 한다.
    • 그 대신 박싱된 기본 타입 클래스가 제공하는 정적 compareTo 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용한다.
    • 핵심 필드가 여러개인 경우, 중요 필드부터 비교한다.
public int compareTo(PhoneNumber pn) {
  int result = Short.compare(areaCode, pn.areaCode); // 가장 중요
  if (result = 0) {
    result = Short.compare(prefix, pn.prefix); // 두 번째로 중요
    if (result = 0)
      result = Short.compare(lineNumf pn.lineNum); // 세 번째로 중요
  }
  return result; 
}
  • 해시코드 값의 차를 기준으로 하는 비교 : 추이성 위배
static Comparator<Object> hashCodeOrder = new Comparatero() { 
  public int compare(Object ol, Object o2) {
    return ol.hashCode() - o2.hashCode(); 
    }
};
  • 정적 compare 메서드를 활용한 비교자
static Comparator<0bject> hashCodeOrder = new Comparatoro() { 
  public int compare(Object ol, Object o2) {
    return Integer.compare(ol.hashCode(), o2.hashCode()); 
    }
};
  • 비교자 생성 메서드를 활용한 비교자
static Comparator<Object> hashCodeOrder = 
  Comparator.comparinglnt(o -> o.hashCode());

+ Recent posts