Giter Club home page Giter Club logo

study-effective-java-3e's People

Stargazers

 avatar

Watchers

 avatar  avatar  avatar

study-effective-java-3e's Issues

이펙티브 자바 3판 - 9. 일반적인 프로그래밍 원칙 ~ 10. 예외

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

적합한 인터페이스만 있다면 매개변수뿐 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하라. 인터페이스를 타입으로 사용하는 습관을 길러두면 프로그램이 훨씬 유연해질 것이다.

//좋은 예
Set<Son> sonSet = new LinkedHashSet<>();


//나쁜 예
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();

적합한 인터페이스가 없다면 당연히 클래스로 참조해야한다.

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

리플렉션의 단점

  • 컴파일타임 타입 검사가 주는 이점을 하나도 누릴 수 없다.
  • 리플렉션을 이용하면 코드가 지저분하고 장황해진다.
  • 성능이 떨어진다.
public static void main(String[] args) {
        // Translate the class name into a Class object
        Class<? extends Set<String>> cl = null;
        try {
            cl = (Class<? extends Set<String>>)  // Unchecked cast!
                    Class.forName(args[0]);
        } catch (ClassNotFoundException e) {
            fatalError("Class not found.");
        }

        // Get the constructor
        Constructor<? extends Set<String>> cons = null;
        try {
            cons = cl.getDeclaredConstructor();
        } catch (NoSuchMethodException e) {
            fatalError("No parameterless constructor");
        }

        // Instantiate the set
        Set<String> s = null;
        try {
            s = cons.newInstance();
        } catch (IllegalAccessException e) {
            fatalError("Constructor not accessible");
        } catch (InstantiationException e) {
            fatalError("Class not instantiable.");
        } catch (InvocationTargetException e) {
            fatalError("Constructor threw " + e.getCause());
        } catch (ClassCastException e) {
            fatalError("Class doesn't implement Set");
        }

        // Exercise the set
        s.addAll(Arrays.asList(args).subList(1, args.length));
        System.out.println(s);
    }

컴파일타임에는 알 수 없는 클래스를 사용하는 프로그램을 작성한다면 리플렉션을 사용해야 할 것이다.
단, 되도록 리플렉션은 인스턴스 생성에만 쓰고, 이렇게 만든 인스턴스는 인터페이스나 상위 클래스로 참조해 사용하자.

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

네이티브 메서드란 C나 C++같은 네이티브 프로그래밍 언어로 작성한 메서드를 말한다. 네이티브 메서드의 주요 쓰임은 다음과 같다.

  • 레지스트리 같은 플랫폼 특화 기능
  • 네이티브 코드로 작성된 기존 라이브러리 사용
  • 성능 개선을 위해 네이티브 언어로 작성

성능을 개선할 목적으로 네이티브 메서드를 사용하는 것은 거의 권장하지 않는다.

네이티브 메서드의 심각한 단점은 다음과 같다.

  • 메모리 훼손 오류로부터 안전하지 않음
  • 플랫폼을 많이 타서 이식성이 낮음
  • 디버깅이 어려움
  • 메모리를 자동 회수하지 못하고 추적조차 할 수 없음
  • 네이티브 메서드와 자바 코드 사이의 접착 코드를 작성해야하는데 가독성이 떨어진다

ITEM 67 최적화는 신중히 하라

빠른 프로그램보다는 좋은 프로그램을 작성하라

  • 완성 후 쉽게 바꾸기 어려운 API, 네트워크 프로토콜, 영구 저장용 데이터 포맷을 설계할 때는 성능을 염두해라
  • 프로파일러를 사용해 문제의 원인이 되는 지점을 찾아 최적화를 수행하라

우리는 어떻게 최적화를 하고 있는가?

ITEM 68 일반적으로 통용되는 명명 규칙을 따르라

10장. 예외

ITEM 69 예외는 진짜 예외 상황에만 사용하라

예외처리로 인해 발생할 수 있는 문제

  • 예외는 최적화가 되어있지 않을 가능성이 크다.
  • 스택트레이스와같은 동작이 수행속도를 느리게 만든다.
  • 흐름제어에 잘못사용된 예외처리가 버그를 숨겨줄 수 있다.

상태의존적인 메서드를 제공하는 클래스라면 상태 검사 메서드도 함께 제공해야한다. ex) Iterator의 next와 hasNext
혹은 올바르지 않은 상태 일 때 빈 옵셔널이나 null 같은 특수한 값을 반환하는 방법도있다.

  • 외부 동기화 없이 여러 스레드가 동시에 접근할 수 있거나 외부 요인으로 상태가 변할 수 있다면 옵셔널이나 특정 값을 사용
  • 성능이 중요한 상황에서 상태 검사 메서드가 상태 의존적 메서드의 작업 일부를 중복 수행한다면 옵셔널이나 특정 값을 사용
  • 다른 모든 경우에는 상태 검사 메서드 방식이 조금 더 낫다.

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

자바에는 문제상황을 알리는 타입으로 검사 예외, 런타임 예외, 에러 세가지를 제공한다.
언제 무엇을 사용해야할까?

Object
 +--- Throwable
       +--- Exception
       |     +--- RuntimeException
       |     +--- ...
       |
       +--- Error
             +--- ...
  • 호출하는 쪽에서 복구하리라 여겨지는 상황이라면 검사 예외를 사용
  • 프로그래밍 오류를 나타낼 때는 런타임 예외를 사용(복구가 불가능하거나 더 실행해봐야 득보다 실이 많으면 중단하는게 나음)
  • 에러는 보통 JVM이 자원 부족, 불변식 깨짐 등 더 이상 수행을 계속 할 수 없는 상황을 나타낼때 사용하며 Error클래스를 상속해 하위 클래스를 만드는 일은 자제하자

Exception 역시 어떤 메서드라도 정의할 수 있는 완벽한 객체이므로 예외의 메서드는 주로 그 예외를 일으킨 상황에 관한 정보를 코드형태로 전달하는데 쓰인다.

ITEM 71 필요 없는 검사 예외 사용은 피하라

API 호출자가 예외 상황에서 복구할 방법이 없다면 비검사 예외를 던지자.
복구가 가능하고 호출자가 그 처리를 해주길 바란다면 우선 옵셔널을 반환해도 될지 고민하자.
옵셔널만으로는 상황을 처리하기에 충분한 정보를 제공할 수 없을 때만 검사 예외를 던지자.

ITEM 72 표준 예외를 사용하라

표준 예외를 사용하면 좋은점

  • 익숙해진 규약을 그대로 따르기 때문에 익히고 사용하기 쉬워진다.
  • 예외 클래스 수가 적을수록 메모리 사용량도 줄고 클래스를 적재하는 시간도 적게 걸린다.

Exception, RuntimeException, Error는 직접 재사용하지는 말자. 이 예외들은 상위 클래스이므로, 즉 여러 성격의 예외들을 포괄하는 클래스이므로 안정적으로 테스트할 수 없다.

ITEM 73 추상화 수준에 맞는 예외를 던져라

상위 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던져야 한다. 이를 예외 번역(Exception translation)이라 한다.

try {
  ...
} catch (LowerLevelException e) {
  throw new HigherLevelException(...);
}

예외를 번역할 때, 저수준 예외가 디버깅에 도움이 된다면 예외 연쇄(exception chaining)를 사용하는게 좋다. 예외 연쇄란 문제의 근본 원인인 저수준 예외를 고수준 예외에 실어 보내는 방식이다. getCause 메서드로 저수준의 예외를 꺼내볼 수 있다.

try {
  ...
} catch (LowerLevelException cause) {
  throw new HigherLevelException(cause);
}

무턱대고 예외를 전파하는 것보다 예외 번역이 우수한 방법이지만 가능하다면 저수준 메서드가 반드시 성공하도록하는게 좋다.

ITEM 74 메서드가 던지는 모든 예외를 문서화하라

검사 예외는 항상 따로따로 선언하고, 각 예외가 발생하는 상황을 자바독의 @throws 태그를 사용하여 정확히 문서화하자.
비검사 예외는 메서드 선언의 throws 목록에 넣지 말자. 명확하게 사용자가 해야할 일을 구분해주는 것이 좋다.

이펙티브 자바 3판 - 3.모든객체의 공통메소드(item13) ~ 4. 클래스와 인터페이스 (item17)

ITEM 13 clone 재 정의는 주의해서 진행해라.

Cloneable 은 복제해도 되는 클래스를 명시하는 Mixin Interface 이지만 의도한 목적을 제대로 이루지 못했음.

clone 메서드가 선언된것이 Object -> 재 정의를 강요할 수 없음

그마저도 protected 로 정의되어 있기 때문에 외부 객체에서 호출할 수 없음.

리플렉션을 사용하면 가능하지만 이 마저도 100%가 아니다.

그럼에도 불구하고 널리 쓰이니까 알아두면 좋음

clone 메서드가 잘 동작하게 하는 구현 방법과 언제 그렇게 해야하는지, 가능한 다른 선택지에 대해 논한다.

Cloneable 인터페이스는 놀랍게도 Objectprotected 메서드인 clone() 의 동작방식을 결정함.

Cloneable 을 구현한 클래스에서는 clone() 을 호출하면 그 객체의 필드들을 하나한 복사한 클래스를 반환하고 구현하지 않았을 경우에는 CloneNotSupportException 을 뱉는다.

이는 인터페이스를 굉장히 이례적으로 사용한 케이스니 따라하지 말것. 인터페이스를 구현한다는 것은 일반적으로 그 클래스가 인터페이스에서 정의한 기능을 제공한다는 의미지만 Cloneable 은 상위 클래스에 정의된 메서드의 동작방식을 변경한다.

!왜 이따위로 만들었지

Object 의 명세에서 가져온 clone 의 규약은 굉장히 허술함

객체의 복사본을 생성해 반환한다. 일반적으로 다음과 같은 규약을 따른다
x.clone() != x
x.clone().getClass() == x.getClass()
x.clone().equals(x)

관습적으로 반환되는 객체는 super.clone() 을 호출하여 얻어야 한다. 만약 Object 를 제외한 모든 클래스와 슈퍼클래스가 이 규약을 지킨다면 x.clone().getClass() == x.getClass() 는 참이다.

관례상 반환된 객체와 원본 객체는 독립적이여야 한다. 이를 만족하려면 super.clone() 으로 얻은 객체의 필드중 하나 이상을 반환전에 수정해야 함. 강제성이 없다는 것을 제외하면 생성자 체인과 같다.

super.clone() 이 아닌 자기 자신의 생성자를 호출하면 하위 객체에서 clone() 을 호출했을 때 원하는 타입을 받지 못한다. 사실 말도 안되는게 super.clone() 으로 Object.clone() 의 동작방식에 기대지 않을 꺼면 Cloneable 을 구현할 필요 자체가 없다.

공변 반환 타이핑 covariant return type : 재정의한 메서드의 반환타입은 상위클래스가 반환하는 타입의 하위타입일 수 있다. super.~ 로 호출한 아이는 하위타입으로 캐스팅이 가능하다.

clone 의 구현이 가변 객체를 참조하면 재앙이 벌어짐
clone 메소드는 사실상 생성자와 같은 효과를 내므로 원본 객체에 아무런 영향을 끼치지 않는 동시에 객체의 불변식을 보장해야한다.

@Override public Stack clone() {
  try {
    Stack result = (Stack) super.clone();
    result.elements = elements.clone();
    return result;
  } catch (CloneNotSupportException e) {
    log.error(e);
    // re-throwing
  }
}

배열의 clone() 은 형변환이 필요없다. 런타임타입/컴파일타임타입 모두 원본의 타입을 반환하기 때문 (Object.clone() 이 아닌 Arrayclone() 을 사용한다.)

여기서 elementsfinal 이면 이 방법은 동작하지 않기 때문에 Cloneable 의 아키텍처는 '가변 객체를 참조하는 필드는 final 로 선언하라' 는 일반적인 용법과 충돌한다.

깊은 복사를 할때, clone 을 재귀적으로 호출하는 것만으로는 부족한 경우가 있음 (HashTable 의 clone 메소드)
Entry // LinkedList 의 길이가 너무 길어지면 재귀호출 중 스택오버플로우가 발생, 이런 경우 순회로 깊은 복사를 구현할 것

또다른 방법은 super.clone() 으로 초기상태를 결정한 다음 원본 객체와 똑같은 상태로 만드는 고수준 API들을 만드는것이다. (같은 값으로 set 하는것)

생성자에서는 재정의 될 수 있는 메소드를 호출하지 말아야 하는데, clone 메서드에서도 마찬가지.
따라서 앞서 언급한 메서드는 final 이거나 private 일 것이다.

재정의한 값을 throw을 없앨것

상속용 클래스는 Cloneable을 구현하지 말것

!복사하는게 의미가 없어서? 하위클래스에게 위임하는게 맞아서?

근데 이런거 복잡하니까 복사 생성자 (변환 생성자) / 복사 팩터리 (변환 팩터리) 를 만드는게 더 낫다.

public Yum (Yum yum) {...}
public Yum newInstance(Yum yum) {...}

왜냐하면

  1. 언어모순적이고 위험천만한 (생성자를 사용하지 않는) 객체 생성메커니즘이 없고
  2. 엉성하게 문서화된 규약에 기대지도 않고
  3. 정상적인 final 용법과도 충돌하지 않으며
  4. 불필요한 checked-exception 을 뱉지도 않고
  5. 형변환도 필요없다.

더욱이 인터페이스를 인자로 받음으로써 유연한 변환도 제공할 수 있게 된다.
범용 컬렉션들이 Collection 이나 Map 을 인자로 받는 생성자를 사용하는 이유

결론

배열 외에 clone 은 쓰지마라

ITEM 14. Comparable 을 구현할 지 고려해라.

equals 와 비슷하지만 동치성 비교외에 순서까지 비교할 수 있다.
Comparable 을 구현했다는 것은 자연적인 순서가 있다는 것을 의미한다.

Comparable 을 구현하면 이 인터페이스를 활용하는 수많은 제네릭 알고리즘과 컬렉션의 힘을 누릴 수 있다. 좁쌀만한 노력으로 코끼리만한 효과를 누릴수 있는것이다. 자연적인 순서를 갖는 값 클래스라면 꼭 구현하도록 하자.

다음과 같은 규약을 지켜야 한다.

  1. 역 성립 sgn(x.compareTo(y)) == -sgn(y.compareTo(x))

  2. 추이성 성립 x.compareTo(y) > 0 && y.compareTo(z) -> x.compareTo(z) > 0

  3. x.compareTo(y) == 0 && y.compareTo(z) == 0 -> x.compareTo(z) == 0

  4. x.compareTo(y) == 0 -> x.equals(y) = true

3번은 필수는 아니지만 만약 Comparable을 구현하는데 이게 성립되지 않을 경우 꼭 클래스의 순서는 equals 메서드와 일치하지 않는다 를 명시해야한다.

마지막 규약은 되도록 지키는게 좋은게 정렬된 컬렉션들은 equals 메서드의 규약을 따른다고 되어 있지만 실제로는 동치성을 비교할 때 equals 대신 compareTo 를 사용하는 경우가 많다.

TreeMap.java

if (cpr != null) {
  do {
    parent = t;
    cmp = cpr.compare(key, t.key);
    if (cmp < 0)
    t = t.left;
    else if (cmp > 0)
    t = t.right;
    else
    return t.setValue(value);
  } while (t != null);
}

Collections.java

int indexedBinarySearch(List<? extends Comparable<? super T>> list, T key) {
  int low = 0;
  int high = list.size()-1;

  while (low <= high) {
    int mid = (low + high) >>> 1;
    Comparable<? super T> midVal = list.get(mid);
    int cmp = midVal.compareTo(key);

    if (cmp < 0)
    low = mid + 1;
    else if (cmp > 0)
    high = mid - 1;
    else
    return mid; // key found
  }
  return -(low + 1);  // key not found
}

구현은 다음처럼 자바에서 제공하는 비교자나 직접 비교자를 구현하면 된다.

public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
  public int compareTo(CaseInsensitiveString cis) {
    return String.CASE_INSENSITIVE_ORDER.comapre(s, cis.s);
  }
}
public int compareTo(PhoneNumber pn) {
  int result = Short.compare(areaCode, pn.areaCode);
  if (result == 0) {
    result = Short.comapre(prefix, pn.prefix);
    if (result == 0) {
      result = Short.comapre(lineNum, pn.lineNum)
    }
  }
  return result;
}

아니면 함수형 인터페이스 Comparator 를 구현하면 좀 더 선언적으로 가능

private static final Comparator<PhoneNumber> COMPARATOR =
  comparingInt((PhoneNumber pn) -> pn.areaCode)
    .thenComparingInt(pn -> pn.prefix)
    .thenComparingInt(pn -> pn.lineNum);

public int compareTo(PhoneNumber pn) {
  return COMPARATOR.compare(this, pn);
}

관계 연산자를 (> , <, ==) 를 직접 쓰는것보다 (거추장스럽고 오류를 유발한다. 맨날 헷갈림) 될 수 있으면 박싱 클래스의 compare 정적 메서드를 활용해라.

비교자를 구현할 때는 성능을 위해 핵심적인 요소부터 비교할것

값의 차 (-) 를 기준으로 Comparator 를 구현하지 말것.
정수 오버플로우와 부동소수점 계산 바식에 따른 오류를 낼 수 있다.

다음 두가지 방법을 써라

static Comparator<Object> hashCodeOrder = new Comparator<>() {
  public int compare(Object o1, Object o2) {
    return Integer.compare(o1.hashCode(), o2.hashCode());
  }
};

static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());
결론

순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하여, 그 인스턴스를 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야한다.

compareTo 메서드에서 필드의 값을 비교할 때 <와 >연산자는 쓰지 말아야한다. 그 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.

클래스와 인터페이스

ITEM 15 클래스와 멤버의 접근 권한을 최소화하라

내부 데이터와 내부 구현은 외부로부터 완벽히 숨겨 구현과 API 를 분리하고, 외부에서는 이 API 를 통해서만 소통해야한다.

  • 시스템 개발 속도를 높인다.
  • 시스템 관리 비용을 낮춘다.
  • 성능 최적화에 도움을 준다.
  • 소프트웨어 재사용성을 높인다.
  • 제작의 난이도를 낮춰준다.

public 클래스의 인스턴스 필드는 되도록 public 이 아니여야 한다.

  • 스레드 안전 불가
  • 길이가 0이 아닌 배열을 주의할 것
  • static 이더라도 참조하는 객체가 가변객체면 말짱 도로묵
private static final Thing[] PRIVATE_VALUES = {...};
public static final List<Thing> VALUES =
  Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
private static final Thing[] PRIVATE_VALUES = {...};
public static final Thing[] values() {
  return PRIVATE_VALUES.clone();
};

Java9 모듈?

결론

프로그램 요소의 접근성은 가능한 최소한으로 하라. 꼭 필요한 것만 골라 최소한의 public API 를 설계하자.

그 외에는 클래스, 인터페이스, 멤버가 의도치 않게 API로 공개되는 일이 없도록 해야한다.

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

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

접근자를 제공하고 필드는 private 로 선언할것

결론

public 클래스는 절대 가변 필드를 직접 노출해서는 안 된다.

불변 필드라면 노출해도 덜 위험하지만 완전히 안심할 수는 없다.

하지만 package-private 클래스나 private 중첩 클래스에서는 종종 필드를 노출하는 편이 나을 때도 있다.

ITEM 17. 변경 가능성을 최소화하라

불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전하다.

  • 객체의 상태를 변경하는 메서드를 제공하지 않는다.
  • 클래스를 확장할 수 없도록 한다.
  • 모든 필드를 final 로 선언한다.
  • 모든 필드를 private 로 선언한다.
  • 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다. (방어적 복사)

불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요 없다.
그러므로 안심하고 공유할 수 있고, 최대한 재활용 하기를 권한다.

방어적 복사도 필요없다. 아무리 복사해봐야 원본과 똑같으니 복사 자체가 의미가 없다.
불변 객체는 자유롭게 공유할 수 있음은 물론, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
불변 객체는 그 자체로 실패 원자성을 제공한다.

메서드에서 예외가 발생한 후에도 그 객체는 여전히 유효한 상태여야 한다..

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

  • 흔히 쓰일 다단계 연산을 예측하여 기본 기능으로 제공한다. (package-private 의 가변 동반클래스를 사용한다.)

BigInteger.java

// Compute the modular inverse
int inv = -MutableBigInteger.inverseMod32(mod[modLen-1]);

// Convert base to Montgomery form
int[] a = leftShift(base, base.length, modLen << 5);

MutableBigInteger q = new MutableBigInteger(),
a2 = new MutableBigInteger(a),
b2 = new MutableBigInteger(mod);

MutableBigInteger r= a2.divide(b2, q);
table[0] = r.toIntArray();

// Pad table[0] with leading zeros so its length is at least modLen
if (table[0].length < modLen) {
  int offset = modLen - table[0].length;
  int[] t2 = new int[modLen];
  for (int i=0; i < table[0].length; i++)
  t2[i+offset] = table[0][i];
  table[0] = t2;
}

// Set b to the square of the base
int[] b = squareToLen(table[0], modLen, null);
b = montReduce(b, mod, modLen, inv);
// 생략
return new BigInteger(1, t2);
  • 만약 정확한 예측이 안되면 동반 가변클래스를 같이 제공해라 (String / StringBuilder)

생성

자신을 상속하지 못하게 하는 가장 쉬운 방법은 final 클래스로 선언하는 것이지만, 모든 생성자를 private 나 package-private 로 두고 public 정적 팩터리 메서드를 제공하는 방법이 더 유연하다.

BigInteger 와 BigDecimal 을 설계할땐 불변 객체가 사실상 final 이어야 하는 인식이 널리 퍼지지 않았다.

그래서 이 두 클래스 모두 재정의할 수 있게 설계되었고, 안타깝게도 하위호환성이 발목을 잡아 지금까지도 이 문제를 고치지 못했다. 만약 신뢰할 수 없는 클라이언트로 부터 이 두 클래스의 인스턴스를 인수로 받는다면 주의해야한다.

신뢰할 수 없는 하위클래스라고 생각된다면 가변이라 가정하고 방어적으로 복사해서 사용해야한다.

결론

게터가 있다고 무조건 세터를 만들지 마라. 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.

String과 BigInteger 처럼 무거운 값 객체도 불변으로 만들 수 있는지 고심해야 한다. 어쩔 수 없다면 불변 클래스와 쌍을 이루는 가변 동반 클래스를 제공해라

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

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

이펙티브 자바 3판 - 11. 동시성 (item83~84) ~ 12. 직렬화(item85 ~ 90)

11. 동시성

Item 83. 지연 초기화는 신중히 사용하라

지연 초기화란(lazy initialization)?

  • 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법.
  • 값이 전혀 쓰이지 않으면 초기화도 결코 일어나지 않음.
  • 주로 성능 최적화 용도로 쓰임.
  • 클래스와 인스턴스 초기화 때 발생하는 위험한 순환 문제를 해결하는 효과도 가짐

지연초기화는 "필요할때가지는 하지말라"

  • 양날의 검
  • 그럼에도 어쩔수 없이 해야 한다면 직접 적용 전후 테스트를 해볼것.
  • 초기화된 각 필드를 얼마나 빈번히 호출하느냐에 따라 지연 초기화가( 다른 수많은 최적화와 마찬가지로) 실제로 성능을 느려지게 할 수도 있다.
  • 멀티스레드 환경에서의 지연 초기화가 까다로움
  • 대부분의 상황에서는 일반적인 초기화가 지연 초기화보다 나음

인스턴스 필드를 초기화하는 방법들

  • 1 일반적인 방법
    private final FieldType field1 = computeFieldValue();

초기화 순환성(initialization circularity)을 깨뜨릴 것 같으면 synchronized를 단 접근자를 사용해야 한다.

  • 2 지연 초기화 - synchronized 접근자 방식
    private FieldType field2;
    private synchronized FieldType getField2() {
        if (field2 == null)
            field2 = computeFieldValue();
        return field2;
    }

성능 때문에 정적 필드를 지연 초기화해야 한다면 지연 초기화 홀더 클래스(lazy initialization holder class) 관용구를 사용한다.

    1. 지연 초기화 홀더 클래스(lazy initialization holder class) 관용구
    private static class FieldHolder {
        static final FieldType field = computeFieldValue();
    }

    private static FieldType getField() { return FieldHolder.field; }
  • getField()를 처음 호출하는 순간 FieldHolder.field가 처음 읽히면서 비로소 FieldHolder 클래스 초기화를 촉발함.

성능 때문에 인스턴스 필드를 지연 초기화해야 하는 경우 - 이중검사(double-check) 관용구를 사용한다.

  • 4 이중검사 관용구
    private volatile FieldType field4;

    private FieldType getField4() {
        FieldType result = field4;
        if (result != null)    // 첫 번째 검사 (락 사용 안 함)
            return result;

        synchronized(this) {
            if (field4 == null) // 두 번째 검사 (락 사용)
                field4 = computeFieldValue();
            return field4;
        }
    }

반복해서 초기화해도 상관없는 인스턴스 필드를 지연 초기화해야 할때는 단일검사(single-check) 관용구를 사용한다.

  • 5 단일검사 관용구
// 코드 83-5 단일검사 관용구 - 초기화가 중복해서 일어날 수 있다! (445쪽)
    private volatile FieldType field5;

    private FieldType getField5() {
        FieldType result = field5;
        if (result == null)
            field5 = result = computeFieldValue();
        return result;
  • 필드의 타입이 long과 double을 제외한 다른 기본타입인 경우 필드 선언에서 volatile 한정자를 없애도 된다.(짜릿한 단일검사 관용구 - racy single-check)

핵심정리

  • 대부분의 필드는 지연시키지 말고 곧바로 초기화해야 한다.
  • 지연 초기화를 써야하는 경우 올바른 지연 초기화 기법을 사용하자
    • 인스턴스 필드 -> 이중검사 관용구
    • 정적 필드 -> 지연 초기화 홀더 클래스 관용구

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

여러 스레드가 실행 중이면 운영체제의 스레드 스케줄러가 어떤 스레드를 얼마나 오래 실행할지를 결정함. 구체적인 스케줄링 정책이 운영체제마다 다를수 있음.
그래서 잘 작성된 프로그램은 이 정책에 좌지우지돼서는 안 된다.(정확성이나 성능이 스레드 스케줄러에 따라 달리진다면 다른 플랫폼에 이식하기 어려움.)

좋은 프로그램의 원칙

  • 실행 가능한 스레드의 평균적인 수를 프로세서 수보다 지나치게 많아지지 않도록 해야 한다.
  • 각 스레드가 유용한 작업을 완료한 후에는 다음 일거리가 생길때까지 대기하도록 하는 것(스레드는 당장 수행해야 할 작업이 없다면 실행돼서는 안 됨.)
  • 스레드 풀 크기를 적절히 설정하고, 작업은 짧게 유지한다.
  • 스레드는 절대 busy waiting(바쁜 대기) 상태가 되면 안 된다.(공유 객체의 상태가 바뀌는것을 쉬지 않고 검사해서는 안 됨.)

피해야 할 극단적인 코드 예제

public class SlowCountDownLatch {
    private int count;

    public SlowCountDownLatch(int count) {
        if (count < 0)
            throw new IllegalArgumentException(count + " < 0");
        this.count = count;
    }

    public void await() {
        while (true) {
            synchronized(this) {
                if (count == 0)
                    return;
            }
        }
    }
    public synchronized void countDown() {
        if (count != 0)
            count--;
    }
}

스레드 관련 개발시 주의사항

  • Thread.yield를 써서 문제를 고쳐보려는 유혹을 떨쳐내자.
    • 당장의 증상 호전은 있을수 있지만, 이식성은 떨어질 것이다.
    • Thread.yield를 테스트할 수단이 없음.
  • 스레드 우선순위를 조절하는 방법도 왠만하면 하지 말자.
    • 스레드 우선순위는 자바에서 이식성이 가장 나쁜 특성중 하나.
    • 심각한 응답 불가 문제를 스레드 우선순위로 해결하려는 시도는 절대 합리적이지 않음.

핵심 정리

  • 프로그램의 동작을 스레드 스케줄러에 기대지 말자.
  • Thread.yield와 스레드 우선순위에 의존해서도 안 된다.(스레드 스케줄러에 제공하는 힌트일뿐)

12. 직렬화

자바가 객체를 바이트 스트림으로 인코딩하고(직렬화) 그 바이트 스트림으로부터 다시 객체를 재구성하는(역질렬화) 매커니즘

Item 85. 자바 직렬화의 대안을 찾으라

배경

  • 1997년 자바에 직렬화 도입됨. 대중적 언어에 적용된건 처음이어서 다소 위험하지 않겟냐는 이야기가 있었음.

자바 직렬화의 문제점

  • 공격 범위가 너무 넓고 지속적으로 넓어져서 방어하기 어려워진다는 점.
    • 바이트 스트림을 역질렬화하는 과정에서 이 메서드는 그 타입들 안의 모든 코드를 수행할수 있는데, 이 말인즉슨, 그 타입들의 코드 전체가 공격 범위에 들어간다는 의미.
  • 신뢰할 수 없는 스트림을 역직렬화하면 원격 코드 실행(remote code execution, RCE), 서비스 거부(denial-of-service, Dos) 등의 공격으로 이어질 수 있음.
  • 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메소드(gadget) 중에는 하드웨어의 네이티브 코드를 마음대로 실행할 수 있는 가젯 체인도 발견되곤 함.(아주 신중하게 제작한 바이트 스트림만 역직렬화해야 함.)
  • 역직렬화에 시간이 오래 걸리는 짧은 스트림을 역직렬화하는 것만으로도 서비스 거부 공격에 쉽게 노출될 수 있음. -> 역직렬화 폭탄(deserialization bomb)

역직렬화 폭탄(deserialization bomb)

public class DeserializationBomb {
    public static void main(String[] args) throws Exception {
        System.out.println(bomb().length);
        deserialize(bomb());
    }

    static byte[] bomb() {
        Set<Object> root = new HashSet<>();
        Set<Object> s1 = root;
        Set<Object> s2 = new HashSet<>();
        for (int i = 0; i < 100; i++) {
            Set<Object> t1 = new HashSet<>();
            Set<Object> t2 = new HashSet<>();
            t1.add("foo"); // t1을 t2와 다르게 만든다.
            s1.add(t1);
            s1.add(t2);
            s2.add(t1);
            s2.add(t2);
            s1 = t1;
            s2 = t2;
        }
        return serialize(root); // 이 메서드는 effectivejava.chapter12.Util 클래스에 정의되어 있다.
    }
}
  • 스트림 전체 크기는 5,744바이트이지만, 역직렬화는 태양이 불타 식을 때까지도 끝나지 않을 것
  • HashSet 인스턴스를 역직렬화하려면 그 원소들의 해시코드를 계산해야 하는것떄문
    반복문에 의해 100단계까지 만들어진 이 HashSet을 역직렬화하려면 hashCode 메소드를 2^100번 넘게 호출해야 함.

역직렬화를 써야 하는가?

  • 아무것도 역직렬화하지 않는 것이 좋다 ( 애초에 신뢰할 수 없는 바이트 스트림을 역직렬화하는 일 자체가 스스로 공격에 노출하는 행위)
    • 일반적으로 새롭게 작성하는 시스템에서 자바 직렬화를 써야 할 이뉴는 전혀 없다.

크로스-플랫폼 구조화된 데이터 표현(cross-platform structured-data representation)

  •  JSON, protobuf 등
  • 자바 직렬화보다 훨씬 간단함.
  • JSON은 사람이 읽을 수 있고, 효과적
  • protobuf는 이진 표현이라 효율이 좋음. protofub는 사람이 읽을수 있는 텍스트 표현(pbtxt)도 지원하기는 함.

역직렬화를 배제할 수 없는 경우에

기본 전제는 레거시 시스템에서 완전비 배제할수 없다면 차선책은 "신뢰할 수 없는 데이터는 절대 역직렬화 하지 않는 것"

  • 역직렬화 필터링(java.io.ObjectInputFilter) 사용
    • Java9에 추가되었고(이전 버전에도 이식됨)
    • 데이터 스트림이 역직렬화되기 전에 필터를 설치

역직렬화 필터링(java.io.ObjectInputFilter)

  • 기본 수용 모드
    • 블랙리스트에 기록된 잠재적으로 위험한 클래스들을 거부
  • 기본 거부 모드
    • 화이트 리스트에 기록된 안전하다고 알려진 클래스들만 수용.
  • 블랙리스트 방식보다는 화이트 리스트 방식을 추천
  • 이 방식에서 직렬화 폭탄은 걸러낼 수 없음.

결론

  • 자바 직렬화를 사용하는 시스템을 관리해야 한다면 시간과 노력을 들여서라도 크로스-플랫폼 구조화된 표현으로 마이그레이션하는 것을 심각하게 고민해야 한다.

핵심정리

  • 직렬화는 위험하니 피하라.
  • 신뢰할 수 없는 데이터는 역직렬화하지 말자. 꼭 해야 한다면 객체 역직렬화 필터링을 사용하자.
  • 클래스가 직렬화를 지원하도록 만들지 말고, 그래야 한다면 정말 신경써야 작성하라.

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

Serializable interface

  • 어떤 클래스의 인스턴스를 직렬화할 수 있게 하려면 클래스 선언에 implements Serializable만 덧붙이면 된다.(자바의 기본방식)

기본 Serializable 방식을 사용할 떄의 주의사항

  • 1 Serializable을 구현하면 릴리스한 뒤에는 수정이 어렵다.
    • 직렬화된 바이트 스트림 인코딩(직렬화 형태)도 하나의 공개 API
    • 커스텀 직렬화 형태를 설계하지 않고 기본 방식을 사용하면 직렬화 형태는 최초 적용 당시 클래스의 내부 구현 방식에 영원히 묶여버림.
    • 직렬화 가능 클래스를 만들고자 한다면, 길게 보고 감당할수 있을 만큼 고품질의 직렬화 형태도 주의해서 함께 설계해야 함.

직렬화의 문제점1 - 클래스 개선을 방해한다.

  • 직렬 버전 UID(serial version UID)
    • 스트림 고유 식별자
    • 모든 직렬화된 클래스는 이것을 부여 받음.
    • static final long serialVersionUID 필드로, 이 번호를 명시하지 않으면 시스템이 런타임에 암호 해시 함수(SHA-1)을 적용해 자동으로 클래스 안에 생성해 넣음.
      • 자동으로 생성되는 값은 클래스 구조가 바뀌면 UID값도 바뀔 수 있음(쉽게 호환성이 깨져 버림)

직렬화의 문제점2 -버그와 보안 구멍이 생길 위험이 높아진다.

  • 생성자가 아닌 다른 방식으로 객체를 생성할 수 있기 때문
  • 불변식 깨짐과 허가되지 않은 접근에 쉽게 노출됨.

직렬화의 문제점3 -해당 클래스의 신버전을 릴리스할 때 테스트할 것이 늘어난다는 점.

  • 테스트해야 할 양이 직렬화 가능 클래스의 수와 릴리스 횟수에 비레해 증가함.

Serializable 구현 원칙

  • Serializable 구현 여부는 가볍게 결정할 사안이 아님.
  • 값 클래스(BIgInteger, Instant) 등은 구현, 스레드 풀처럼 '동작'하는 객체들은 대부분 구현하지 않음.
  • 상속용으로 설계된 클래스도 대부분 구현해서는 안되고, 인터페이스도 대부분 Serializable을 확장해서는 안 된다.
  • 인스턴스 필드 중 기본값(정수형은 0, boolean은 false, 객체 참조타입은 null)으로 초기화되면 위배되는 불변식이 있을때 readObjectNoData 메서드를 반드시 추가해야 함.
    • 기존의 직렬화 가능 클래스에 직렬화 가능 상위 클래스를 추가하는 드문 경우를 위한 메소드
private void readObjectNoData(0 throws InvalidObjectException {
    throw new InvalidObjectException("스트림 데이터가 필요합니다");
}
  • 내부 클래스는 직렬화를 구현하지 말아야 한다.
    • 정적 멤버 클래스는 Serializable을 구현해도 됨.

핵심정리

  • 클래스의 여러 버전이 상호작용할 일이 없고 서버가 신뢰할 수 없는 데이터에 노출될 가능성이 없는 등, 보호된 환경에서만 쓰일 클래스가 아니라면 Serializable 구현은 아주 신중히 이뤄져야 함.
  • 상속할 수 있는 클래스라면 주의사항이 더욱 많아짐.

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

클래스가 Serializable을 구현하고 기본 직렬화 형태를 사용한다면 커스텀 직렬화 형태를 고려하자

  • 그 이유는 기본 직렬화 형태는 현재의 구현에 영원히 묶이게 되기 떄문.
  • 고민해보고 괜찮다고 판단될 때만 기본 직렬화 형태를 사용하라.
    • 일반적으로 직접 설계하더라도 기본 직렬화 형태와 거의 같은 결과가 나올 경우에만 기본 형태를 써야 함.
  • 객체의 물리적 표현과 논리적 내용이 같다면 기본 직렬화 형태라도 무방하다.

readObject()

  • 기본 직렬화 형태가 적합하다고 결정했더라도 불변식 보장과 보안을 위해 제공해야 함.

@serial 태그

-직렬화 형태도 공개 API에 속하기 떄문에 모두 문서화 해야 한다.

  • 직렬화 형태를 설명하는 용도의 태그

기본 직렬화 형태가 적합하지 않은 케이스

public final class StringList implements Serializable {
    private int size   = 0;
    private Entry head = null;

    private static class Entry {
        String data;
        Entry  next;
        Entry  previous;
    }

    // 지정한 문자열을 이 리스트에 추가한다.
    public final void add(String s) {  }

    /**
     * 이 {@code StringList} 인스턴스를 직렬화한다.
     *
     * @serialData 이 리스트의 크기(포함된 문자열의 개수)를 기록한 후
     * ({@code int}), 이어서 모든 원소를(각각은 {@code String})
     * 순서대로 기록한다.
     */
    private void writeObject(ObjectOutputStream s)
            throws IOException {
        s.defaultWriteObject();
        s.writeInt(size);

        // 모든 원소를 올바른 순서로 기록한다.
        for (Entry e = head; e != null; e = e.next)
            s.writeObject(e.data);
    }

    private void readObject(ObjectInputStream s)
            throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        int numElements = s.readInt();

        // 모든 원소를 읽어 이 리스트에 삽입한다.
        for (int i = 0; i < numElements; i++)
            add((String) s.readObject());
    }

   ...
  • 기본 직렬화 형태를 사용하는 경우 양방향 연결 정보를 포함해 모든 엔트리(Entry)를 철두철미하게 기록해야 함.

객체의 물리적 표현과 논리적 표현의 차이가 클 때 기본 직렬화 형태를 사용했을떄 생기는 문제

  • 1 공개 API가 현재 내부 표현 방식에 영구히 묶인다.
  • 2 너무 많은 공간을 차지할 수 있다.
  • 3 시간이 너무 많이 걸릴 수 있다.
  • 4 스택 오버플로를 일으킬 수 있다.

합리적인 커스텀 직렬화 형태를 갖춘 StringList

public final class StringList implements Serializable {
    private transient int size   = 0;
    private transient Entry head = null;
  • transient(일시적) 한정자를 사용하여 처리하면 직렬화 되지 않음.

    • 해당 인스턴스 필드가 기본 직렬화 형태에 포함되지 않는다는 표시
  • 클래스의 인스턴스 필드 모두가 transient이면 defaultWriteObject와 defaultReadObject를 무조건 호출해야 한다.(직렬화 명세)

transient 사용시 주의사항

  • 해당 객체의 논리적 상태와 무관한 필드라고 학신할 떄만 사용하자.
  • 역직렬화 과정에서 기본값으로 초기화됨. 기본값을 그대로 사용해서는 안된다면 readObject 메소드에서 defaultReadObject를 호출하고 해당 필드를 원하는 값으로 복원 처리 해주어야 함.

직렬화 - 동기화 관련

  • synchronized로 선언하여 스레드 안전하게 만든 객체에서 기본 직렬화를 사용하려면 writeObject에도 synchronized로 선언 해야 함.
private synchronized void wirteObject(ObjectOutputStream s) throws IOException {
    s.defaultWriteObject();
}

-wirteObject 메소드 안에서 동기화하고 싶다면 클래스의 다른 부분에서 사용하는 락 순서를 똑같이 적용해야 자원 순서 교착 상태(resource-ordering deadlock)을 피할수 있음.

직렬화시 항상 고려해야 하는 것

  • 어떤 직렬화 형태를 택하든 직렬화 가능 클래스 모두에 직렬 버전 UID를 명시적으로 부여하자.
    • 직렬버전 UID를 명시하지 않으면 런타임에 이 값을 생성하느라 복잡한 연산을 수행하게 되기 떄문.
    • 구버전으로 직렬화된 인스턴스들과의 호환성을 끊으려는 경우를 제외하고는 직렬버전 UID를 수정하지 말자.

핵심정리

  • 자바의 기본 직렬화 형태는 객체를 직렬화한 결과가 해당 객체의 논리적 표현에 부합할떄만 사용
  • 그렇지 않은 경우 커스텀 직렬화 형태를 고려하라.

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

readObject 메소드 주의사항

  • 실질적으로 또 다른 public 생서자나 다름없기 때문에 생성자와 똑같은 수준으로 주의를 기울여야 함.
  • readObject는 매개변수로 바이트 스트림을 받는 생성자
  • 역직렬화 과정에서 불변식을 깨뜨리는 객체를 만들어낼수도 있기 떄문에 불변식을 만족하는지 readObject에서 처리해주어야 한다.
  • 객체를 역직렬화할 때는 클라이언트가 소유해서는 안되는 객체 참조를 갖는 필드를 모두 반드시 방어적으로 복사해야 한다.
  • 방어적 복사를 수행하고, 불변식을 만족하는지 검사(유효성 검사)한다.

기본 readObject를 써도 좋을지 판단하는 방법

  • transient 필드를 제외한 모든 필드의 값을 매개변수로 받아 유효성 검사 없이 필드에 대입하는 public 생성자를 추가해도 괜찮은가? 가 아니오라면 커스텀 readObject를 만들어야 함.(말이 어려움..)
    • 클래스의 필드 멤버들이 유효성 검사 없이 들어와도 관계 없다면 기본 readObject를 써도 됨.
    • 그게 아니면 모든 유효성 검사와 방어적 복사를 수행해야 한다.
  • 직렬화 프록시 패턴을 사용해도 됨.

핵심 정리

  • readObject 메서드를 작성할 때는 언제나 public 생성자를 작성하는 자세로 임해야 한다.
  • 안전한 readObject 메서드를 작성하는 지침
    • private이어야 하는 객체 참조 필드는 각 필드가 가리키는 객체를 방어적 복사하라.(불변 클래스 내의 가변요소)
    • 모든 불변식을 검사하여 어긋나면 exception을 던지고, 방어적 복사 이후 불변식 검사가 뒤따라야 한다.
    • 역직렬화 후 객체 그래프 전체의 유효성을 검사해야 한다면 ObjectInputValidation 인터페이스를 사용하라.
    • 재정의할 수 있는 메서드를 호출하지 말자.

Item 89. 인스턴스 수를 통제해야 한다면 readResolve보다는 Enum을 사용하라.

싱글턴 클래스에 implements Serializable을 했을 때

  • 더이상 싱글턴이 아니게 된다.
  • 기본 직렬화를 쓰지 않더라도, 명시적인 readObject를 제공하더라도 소용 없음.
  • 어떤 readObject를 사용하든 이 클래스가 초기화될 때 만들어진 인스턴스와는 별개인 인스턴스를 반환하게 됨.

readResolve 기능

  • 이 기능을 이용하면 readObject가 만들어낸 인스턴스를 다른것으로 대체할 수 있음.
// 인스턴스 통제를 위한 readResovle - 개선의 여지가 있다!
private Object readResolve() {
    // 진짜 Elvis를 반환하고, 가짜 Elvis는 가비지 컬렉터에 맡긴다.
    return INSTANCE;
}
  • 이렇게 처리하면 역직렬화한 객체는 무시하고, 클래스 초기화 때 만들어진 Elvis인스턴스를 반환한다.
  • readResolve를 인스턴스 통제 목적으로 사용한다면 객체 참조 타입 인스턴스 필드는 모두 transient로 선언해야 함.
    • readResolve 메서드가 수행되기 전에 역직렬화된 객체의 참조를 공격할 여지가 남게 됨
  • transient로 선언하여 공격 위협으로부터 안전하게 고칠 수 있지만 Elvis를 원소 하나짜리 열거 타입으로 바꾸는것이 더 나은 선택

readResolve 메서드의 접근제한

  • final 클래스에서라면 private으로 선언해야 함.
  • public이나 protected이면서 하위 클래스에서 재정의하지 않으면, 하위 클래스의 인스턴스를 역직렬화했을떄 상위 클래스의 인스턴스를 생성하여 ClassCastException이 발생할 수 있다.

핵심 정리

  • 불변식을 지키기 위해 인스턴스를 통제해야 한다면 가능한 한 열거타입을 사용하자.
  • 불가피한 경우 readResolve 메서드를 작성하고 모든 참조타입 인스턴스 필드를 transient로 선언해야 한다.

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

직렬화 프록시 패턴(serialization proxy pattern)

  • 직렬화와 관련하여 발생할수 있는 버그와 보안문제를 줄일 수 있는 기법
  • writeReplace 메소드
    • 자바의 직렬화 시스템이 바깥 클래스의 인스턴스 대신 프록시의 인스턴스를 반환하게 하는 역할
  • 원본 클래스의 readObject 메소드는 공격을 막기 위해 Exception을 던져야 함.
  • 프록시 클래스의 readResolve 메서드
    • 역직렬화시에 직렬화 시스템이 직렬화 프록시를 다시 원본 클래스의 인스턴스로 변환해주는 역할
// 방어적 복사를 사용하는 불변 클래스
public final class Period implements Serializable {
    private final Date start;
    private final Date end;

    /**
     * @param  start 시작 시각
     * @param  end 종료 시각; 시작 시각보다 뒤여야 한다.
     * @throws IllegalArgumentException 시작 시각이 종료 시각보다 늦을 때 발생한다.
     * @throws NullPointerException start나 end가 null이면 발행한다.
     */
    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(
                    start + "가 " + end + "보다 늦다.");
    }

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

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

    public String toString() { return start + " - " + end; }


    // 코드 90-1 Period 클래스용 직렬화 프록시 (479쪽)
    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;
        }

        // Period.SerializationProxy용 readResolve 메서드
        private Object readResolve() {
            return new Period(start, end);
        }

        private static final long serialVersionUID =
                234098243823485285L; // 아무 값이나 상관없다. (아이템 87)
    }

    // 직렬화 프록시 패턴용 writeReplace 메서드 (480쪽)
    private Object writeReplace() {
        return new SerializationProxy(this);
    }

    // 직렬화 프록시 패턴용 readObject 메서드 (480쪽)
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
        throw new InvalidObjectException("프록시가 필요합니다.");
    }
}
  • 가짜 바이트 스트림 공격과 내부 필드 탈취 공격을 프록시 수준에서 차단할수 있음.
  • 역직렬화한 인스턴스와 원래의 직렬화된 인스턴스의 클래스가 달라도 정상작동한다.

직렬화 프록시 패턴의 한계

  • 클라이언트가 멋대로 확장할 수 있는 클래스에는 적용할 수 없다.
  • 객체 그래프에 순환이 있는 클래스에 적용할 수 없다.
  • 속도가 느리다.(방어적 복사 케이스보다 14% 느린 테스트 결과)

핵심 정리

  • 제3자가 확장할 수 없는 클래스라면 가능한 한 직렬화 프록시 패턴을 사용하자. 이는 중요한 불변식을 안정적으로 직렬화해주는 가장 쉬운 방법

이펙티브 자바 3판 - 2. 객체 생성과 파괴

이펙티브 자바 3판 - 2. 객체 생성과 파괴

  • 객체를 만들어야 할 때와 만들지 말아야 할 때를 구분하는 법
  • 올바른 객체 생성 방법과 불필요한 생성을 피하는 방법
  • 제때 파괴됨을 보장하고 파괴 전에 수행해야 할 정리 작업

Item1. 생성자 대신 정적 팩터리 메서드를 고려하자.

  • 클래스는 생성자와 별도로 정적 팩터리 메서드(static factory method)를 제공할 수 있다.
public static Boolean valueOf(boolean b) {
	return b ? Boolean.TRUE : BOolean.FALSE;
}

정적 팩터리 메서드가 생성자보다 좋은 장점 5가지

  • 1 이름을 가질 수 있다.
    • BigInteger(int, int, Random)과 정적 팩터리 메서드인 BigInteger.probablePrime 중 '어느 쪽이 값이 소수인 BigInteger를 반환한다'는 의미를 더 잘 설명할 것 같은지?
    • 각 목적에 맞는 생성자가 여러개 있다고 했을때, 각각의 생성자가 어떤 역할을 하는지 정확히 기억하기 어려워 엉뚱한 것을 호출하는 실수를 할 수 있다.
  • 2 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
    • 불변 클래스(immutable class)는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.
    • Boolean.valueOf(boolean) 메서드는 객체를 아예 생성하지 않음.(성능 향상에 기여)
    • 반복되는 요청에 같은 객체를 반환하는 식으로 인스턴스를 철저히 통제할 수 있음 - 통제(instance-controlled) 클래스
    • 이렇게 통제하는 이유
      • 싱글턴(singleton)으로 만들수도, 인스턴스화 불가(noninstantiable)로 만들 수 있음
      • 동치인 인스턴스가 단 하나뿐임을 보장 가능(열거타입과 같이)
      • 플라이웨이트 패턴의 근간( 나중에 보는걸로..)
  • 3 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
    • 추상화된 구조인 경우 인터페이스를 정적 팩터리 메서드의 반환 타입으로 해서 구현 클래스를 공개하지 않고 객체를 반환할 수 있음.
      • API를 작게 유지할 수 있다.
      • Collection 프레임워크는 45개 클래스를 공개하지 않기 때문에 API 외견을 훨씬 작게 만들수 있었음.
    • 자바8부터는 인터페이스가 정적 메서드를 가질 수 없다는 제한이 풀려서 인스턴스화 불가 동반 클래스를 둘 이유가 별로 없다.
    • 자바9에서는 private 정적 메서드까지 허락하지만, 정적 필드와 정적 멤버 클래스는 여전히 public이어야 한다.
  • 4 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있음.
    • EnumSet 클래스는 public 생성자 없이 오직 정적 팩터리만 제공.
      • 원소가 64개 이하면 ㅣong변수 하나로 관리 -> RegularEnumSet
      • 원소가 65개 이상이면 long 배열로 관리하는 JumboEnumSt
      • 클라이언트는 위와 같은 사실을 몰라도 됨
  • 5 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
    • 이러한 유연함은 Service Provider 프레임워크의 근간이 된다. 대표적인 예로 JDBC 있다.
      • DriverManager.registerDriver() 메서드로 각 DBMS별 Driver를 설정한다. (제공자 등록 API)
      • DriverManager.getConnection() 메서드로 DB 커넥션 객체를 받는다. (service access API)
      • Connection Interface는 DBMS 별로 동작을 구현하여 사용할 수 있다. (service Interface)
    • 위와 같이 동작하게 된다면 차후에 다른 DBMS가 나오더라도 같은 Interface를 사용하여 기존과 동일하게 사용이 가능하다.

정적 팩터리 메서드 단점

  • 1 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.

    • 상속보단 컴포지션을 사용하도록 유도하고 불변 타입으로 만들려면 이 제약을을 지켜야 하는건 오히려 장점
  • 2 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.

    • 생성자처럼 잘 드러나지 않음.
    • API 문서를 잘 써놓고 메서드 이름도 널리 알려진 규약을 따라 지어야 함.
명명 규칙 설명 예시
from 매개변수를 하나를 받아서 생성 Date d = Date.from(instant)
of 여러 매개변수를 받아서 생성 Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
valueOf from과 of의 더 자세한 버전 BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
instace 혹은 getInstance 매개변수로 명시한 인스턴스를 반환, 같은 인스턴스 보장(x) StackWalker luke = StackWalker.getInstnace(options);
create 혹은 newInstance 매번 새로운 인스턴스를 생성해 반환 Object newArray = Array.newInstace(classObject, arrayLen);
get"Tpye" 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 FileStore fs = Files.getFileStore(path);
new"Type" 매번 새로운 인스턴스를 생성해 반환하지만 다른 클래스에 팩터리 메서드를 정의할 때 BufferedReader br = Files.newBufferedReader(path);
"type" getType과 newType의 간결한 버전 List<Complaint> litany = Collections.list(legacyLitany);

핵심 정리

  • 정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다.

Item2. 생성자에 매개변수가 많다면 빌더를 고려하라

점층적 생성자 패턴

  • 과거에 주로 사용했던 방식
public NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 0);
    }

    public NutritionFacts(int servingSize, int servings,
                          int calories) {
        this(servingSize, servings, calories, 0);
    }

    public NutritionFacts(int servingSize, int servings,
                          int calories, int fat) {
        this(servingSize, servings, calories, fat, 0);
    }

    public NutritionFacts(int servingSize, int servings,
                          int calories, int fat, int sodium) {
        this(servingSize, servings, calories, fat, sodium, 0);
    }
    public NutritionFacts(int servingSize, int servings,
                          int calories, int fat, int sodium, int carbohydrate) {
        this.servingSize  = servingSize;
        this.servings     = servings;
        this.calories     = calories;
        this.fat          = fat;
        this.sodium       = sodium;
        this.carbohydrate = carbohydrate;
    }
  • 점층적 생성자 패턴을 쓸수도 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.
  • 타입이 같은 매개변수가 연달아 늘어서 있으면 찾기 어려운 버그로 이어질 수 있다.
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27); // 각각의 값이 어떤걸 뜻하는지 한눈에 파악할 수 없음.

자바빈즈 패턴(JavaBeans pattern)

  • 매개변수가 없는 생성자로 객체를 만든 후, 세터(setter) 메서드들을 호출해주는 방식
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
  • 자바빈즈 패턴에서는 객체 하나를 만들려면 메서드를 여러개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성(consistency)이 무너진 상태에 놓이게 된다.
  • 클래스를 불변으로 만들 수 없는 것도 문제
  • 이러한 단점을 완화하고자 생성이 끝난 객체를 수동으로 freezing해서 변경할 수 없도록 하기도 한다.(그래도 별로)

빌더 패턴(Builder pattern)

  • 실제로 실무에서 많이 사용됨
  • 빌더 패턴을 고려한다면 lombok을 사용하자 lombok
@Entity
@Getter
@Builder
public class Category {
    @Id
    @GeneratedValue
    private int id;

    @Column
    private String name;

    @Column
    private String icon;

    private Level level;


    // @OneToMany(mappedBy = "id", cascade = CascadeType.ALL)
    private List<Category> subCagetoryList;

}

 @Test
    public void builderTest() {
        Category.builder()
                .id(1)
                .level(Level.FIRST)
                .name("팬션의류/잡화")
                .icon("패션의류 아이콘.png")
                .subCagetoryList(new ArrayList<>())
                .build();
    }
  • 빌더패턴은 파이썬과 스칼라에 있는 명명된 선택적 매개변수(named optional parameters)를 흉내낸 것.
  • 잘못된 매개변수를 최대한 일찍 발견하려면 빌더의 생성자와 메서드에서 입력 매개변수를 검사하고 build 메소드가 호출하는 생성자에서 여러 매개변에 걸친 불변식(invariant)을 검사하자.
    • lombok을 사용하더라도 bulderMethod를 지정한다던지 해서 검증 가능.
  • 불변(immutable 혹은 immutability)과 불변식(invariant)
    • 불변(immutable) - 어떠한 변경도 허용하지 않겠다는 뜻으로 가변(mutable) 객체와 구분하는 용도로 쓰인다. 대표적으로 String 객체는 한번 생성되면 절대 바꿀 수 없는 불변 객체
    • 불변식(invariant) - 프로그램이 실행되는 동안, 혹은 정해진 기간동안 반드시 만족해야 하는 조건(리스트의 크기는 반드시 0 이상)
    • 가변(mutable) 객체에서도 불변식(invariant)가 존재할 수 있음.
  • 계층적으로 설계된 클래스와 함께 쓰기에도 좋음.
    • Builder도 상속해서 처리하는데 lombok을 사용하는 것이 더 좋은 방식이라고 생각됨.
@NoArgsConstructor
@Getter
@Builder
public class BaseFashionItem implements FashionItem {
    private String brand;
    private Category category;
    private String name;
    private long price;
}

public class Shirt extends BaseFashionItem {
    private ColorType colorType;
    private SizeType sizeType;

    @Builder
    public Shirt(String brand, Category category, String name, long price, ColorType colorType, SizeType sizeType) {
        super(brand, category, name, price);
        this.colorType = colorType;
        this.sizeType = sizeType;
    }
}

 @Test
    public void builderTest() {
        Shirt shirt = Shirt.builder()
                .category(Category.builder().build())
                .brand("brand")
                .name("name")
                .price(15000)
                .colorType(ColorType.BLACK)
                .sizeType(SizeType.M)
                .build();

    }
  • 빌더 패턴의 단점

    • 객체를 만들려면 그에 앞서 빌더부터 만들어야 하는 점.(성능에 민감한 상황에서는 문제가 될 수도 있다.)
  • API는 시간이 지날수록 매개변ㄴ수가 많아지기 때문에 애초에 빌더를 고려하는 편이 나을 때가 많다.

Item3. private 생성자나 열거 타입으로 ㅅ싱글턴임을 보증하라.

싱글턴이란?

  • 인스턴스를 오직 하나만 생성할 수 있는 클래스
  • 싱글턴의 전형적인 예로는 함수와 같은 무상태(stateless) 개체나 설계상 유일해야 하는 시스템 컴포넌트를 들수 있다.
    • spring bean 객체를 보통 singleton scope를 이용해서 사용함.
  • 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워 짐.
    • 싱글턴 인스턴스를 mock 구현으로 대체할 수 없음.
    • 인터페이스를 구현해서 만든 싱글턴이면 가능
  • private 생성자를 만들면 클라이언트에서는 객체 생성 할수 있는 방법이 존재하지 않아 인스턴스가 전체에서 하나뿐임을 보장할 수 있음.
  • 리플렉션 API를 이용해서 호출 가능한데, 이를 방어하기 위해서 생성자에서 두번째 객체가 생성되려 할떄 예외를 던지게 하면 됨.

싱글턴을 만드는 방법

  • 1 public static final 필드 방식
    • 싱글턴임이 API에 명백히 드러나고, 간결하다는 장점이 있음.
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() { }

    public void leaveTheBuilding() {
        System.out.println("Whoa baby, I'm outta here!");
    }

    // 이 메서드는 보통 클래스 바깥(다른 클래스)에 작성해야 한다!
    public static void main(String[] args) {
        Elvis elvis = Elvis.INSTANCE;
        elvis.leaveTheBuilding();
    }
}
  • 2 정적 팩터리 메서드 방식
    • API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있음.(getInstnace 메소드를 변경하면 됨)
    • 정적 팩터리를 제네릭 싱글턴 팩터리로 변경할 수 있음.
    • 메서드 참조를 공급자로 사용할 수 있음.
public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { }
    public static Elvis getInstance() { return INSTANCE; }

    public void leaveTheBuilding() {
        System.out.println("Whoa baby, I'm outta here!");
    }

    // 이 메서드는 보통 클래스 바깥(다른 클래스)에 작성해야 한다!
    public static void main(String[] args) {
        Elvis elvis = Elvis.getInstance();
        elvis.leaveTheBuilding();
    }
}
  • 3 Enum
    • 더 간결하고, 추가 노력 없이 직렬화 할 수 있음.
    • 복잡한 직렬화 상황, 리플렉션 공격에서도 완벽히 방어
    • 대부분 상황에서는 원소가 하나뿐인 Enum이 싱글턴을 만드는 가장 좋은 방법
      • Enum에서 Enum을 참조하는 경우 순환 참조에 주의

실글턴 클래스를 직렬화하는 방법

  • 모든 인스턴스 필드를 일시적(transient)이라고 선언하고 readResolve 메소드를 제공(Item 89)
private Object readResolve() {
return INSTNACE;
}

Item4. 인스턴스화를 막으려거든 private 생성자를 사용하라.

  • 정적 메서드와 정적 필드만을 담은 클래스를 만들고 싶을때가 있다. 객체지향적이지는 않지만 나름의 쓰임새가 있음.(ex java.lang.Math, Utils 시리즈)
  • 추상클래스를 만들어서 인스턴스화를 금지하는 것은 안됨(이렇게 해본적도 없음.)
  • private 생성자를 추가해서 클래스의 인스턴스화를 막자.
    • lombok을 이용하면 편함.
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ContentLanguageConfig {
	public static final List<Language> SERVICE_LIST;
	public static final List<Language> EMAIL_PUSH_SUPPORT_LIST;
	public static final List<Language> EMAIL_JOIN_SUPPORT_LIST;

	static {
		SERVICE_LIST = Arrays.asList(Language.ENGLISH, Language.SIMPLIFIED_CHINESE, Language.TRADITIONAL_CHINESE, Language.THAI, Language.INDONESIAN, Language.JAPANESE);
		EMAIL_PUSH_SUPPORT_LIST = Arrays.asList(Language.ENGLISH, Language.SIMPLIFIED_CHINESE, Language.TRADITIONAL_CHINESE, Language.JAPANESE);
		EMAIL_JOIN_SUPPORT_LIST = Arrays.asList(Language.ENGLISH, Language.SIMPLIFIED_CHINESE, Language.TRADITIONAL_CHINESE, Language.JAPANESE);
	}

	/**
	 * 서비스중인 언어인지 확인
	 * @param language
	 * @return
	 */
	public static boolean isService(Language language) {
		return SERVICE_LIST.contains(language);
	}
}
  • 생성자 내에서 Exception을 던지도록 한다면 주석을 달아두자
  • 이 방식을 적용했을때 상속을 불가능하게 하는 효과도 가져옴.

Item5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라.

  • 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않음.
  • 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식을 사용

의존 객체 주입은 유연성과 테스트에 용이하다.

public class SpellChecker {
	private final Lexicon dictionary;

	public SpellChecker(Lexicon dictionary) {
		this.dictionary = Objects.requireNonNull(dictionary);
	}
	
	public boolean isVlaid(String word) { ...	}
	
	public List<String> suggestions(String type) { ...	}
}
  • 의존 객체 주입을 생성자, 정적 팩터리, 빌더 등 아무런 방법에 적용하면 됨.

팩터리 메서드 패턴

  • 의존 객체 주입의 쓸만한 변형 방식
  • 생성자에 자원 팩터리 객체를 넘겨주는 방식
  • Supplier 인터페이스를 사용하면 됨.
  • 한정적 와일드카드 타입(bounded wildcard type)을 사용해 팩터리의 타입 매개변수를 제한
Mosaic create(Supplier<? extends Tile> titleFactory) { ... }
  • 의존 객체 주입이 유연성과 테스트 용이성을 개선해주긴 하지만, 의존성이 많아지면 코드를 어렵게 만들기도 함.

핵심 정리

자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다.(의존 객체 주입을 통해 하자)

Item 6. 불필요한 객체 생성을 피하라

똑같은 기능의 객체를 매번 생성하기 보다는 객체 하나를 재사용하는 편이 나을 때가 많다.(당연한 이야기)

String 인스턴스 관련

String s = new String("bikini"); // 따라하지 말것.

String s = "bikini";
  • 생성자로 생성하는 케이스는 매번 새로운 String 인스턴스를 생성한다.

  • 2번쨰 방식을 사용하면 하나의 String 인스턴스를 사용하고, 가상 머신 안에서 이와 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함이 보장된다.

  • Boolean(String) 생성자 <<<< Boolean.valueOf(String)

생성비용이 비싼 객체 처리

  • 비싼 객체가 반복해서 필요하다면 캐싱하여 재사용한다.
  • String.matches 메소드를 쓰면 간편하지만, 성능이 중요한 상황에서 반복해서 사용하기엔 적합하지 않음.
    • 해당 메소드에서 생성하는 Pattren 객체는 한번 쓰고 버려짐.
    • Pattren 유한 상태 머신(finite sate machine)을 만들기 때문에 인스턴스 생성 비용이 높음.
  • Regular Expression -> Pattren 객체를 이용
  • String.matches vs Pattern.matchers 성능 비교
    • 1.1마이크로s / 0.17 마이크로s

오토박싱

  • 오토박싱이란 기본타입과 박싱된 기본 타입을 섞어 쓸때 자동으로 상호 변환해주는 기술.
  • 불필요한 객체를 만들어내는 예 중 하나이다.
  • 오토 박싱이 기본 타입과 그에 대응하는 박싱된 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아님.
private static long sum() {
        Long sum = 0L;
        for (long i = 0; i <= Integer.MAX_VALUE; i++)
            sum += i;
        return sum;
    }
  • sum 변수의 long이 아닌 Long으로 선언해서 불필요한 Long 인스턴스가 생성됨.

  • 박싱된 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.

객체 생성이 비싸니 피해야 한다(?) -> 객체 풀

  • 아주 무거운 객체가 아닌 다음에야 단순히 객체 생성을 피하고자 객체 풀을 만들다던지는 안하는 것이 좋음.
  • DB 연결같은 경우 생성비용이 비싸니 재사용하는 편이 낫지만 그렇지 않은 경우가 많음.
  • 객체 풀은 코드를 헷갈리게 만들고 메모리 사용량을 늘리고, 성능을 떨어뜨린다.

방어적 복사 vs 불필요한 객체 생성

  • 방어적 복사가 필요한 상황에서 객체를 재사용했을 때의 피해는 필요 없는 객체를 반복 생성했을 때의 피해보다 훨씬 크다.
    • 잘 모르면 차라리 불필요한 객체 생성은 여러번 하는게 나을수도 있음.

Item 7. 다 쓴 객체 참조를 해제하라

메모리 누수

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        return elements[--size];
    }

    /**
     * 원소를 위한 공간을 적어도 하나 이상 확보한다.
     * 배열 크기를 늘려야 할 때마다 대략 두 배씩 늘린다.
     */
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }

}
  • 위 스택을 오래 수행하다보면 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 성능 저하 발생
  • 메모리 누수의 원인?
    • 객체들의 다 쓴 참조(obsolete reference)을 여전히 가지고 있기 때문. ( elements 배열의 활성 영역 밖 )
  • 해법
    • 해당 참조를 다 썼을 때 null 처리(참조 해제) - pop 시점에 null 처리
public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
       Object result = elements[--size];
       elements[size] = null; // 다쓴 참조 해제
        return result;
    }

객체 참조를 null 처리 해야 하는 경우

  • 객체 참조를 null 처리하는 일은 예외적인 경유여야 한다.
  • 가장 좋은 참조 해제 방법
    • 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것
  • 자기 메모리를 직접 관리하는 클래스 인 경우 프로그래머가 항상 메모리 누수에 주의해야 함.
  • 캐시 역시 메모리 누수를 일으키는 주범
  • WeakHashMap
public class WeakHashMapTest {

    public static void main(String[] args) {
        WeakHashMap<Integer, String> map = new WeakHashMap<>();

        Integer key1 = 1000;
        Integer key2 = 2000;

        map.put(key1, "test a");
        map.put(key2, "test b");

        key1 = null;

        System.gc();  //강제 Garbage Collection

        map.entrySet().stream().forEach(el -> System.out.println(el));

    }
}
  • LinkedHashMap

  • 리스너(Listener) 혹은 콜백(Callback)

    • 클라이언트 코드에서 콜백을 등록만 하고 명확히 해제하지 않는 경우에 발생할 수 있음.
    • 콜백을 약한 참조(weak reference)로 저장하면 즉시 수거 ( WekHashMap에 키로 저장)

핵심 정리

  • 메모리 누수는 겉으로 잘 드러나지 않음.
  • 철저한 코드리뷰 힙 프로파일러 같은 디버깅 도구를 동원해야만 발견되기도 함.
  • 즉 발견하기 어렵기 때문에 예방법을 잘 익히자!

출석표

출석표

출석 박성수 김래선 윤지수 조민혜 지성인 양성만
2/14 V V V 지각 V V
2/21 V 지각 V 지각 지각 개인 사정으로 인한 불참
3/7 V V 지각 V V V
3/21 V V V 사정으로 불참 V V
3/28 V V 불참 V V V
4/18 사정으로 불참 불참 불참 불참 V V
4/24 사정으로 불참 불참 V V 불참 V
5/9 V V V V V V

규칙

보증금 지불 방식

  • 일일 참석 : 5000원
  • 지각시 : 2500원
  • 불참시 0원

보증금 지급일

  • 스터디 종료일

참조 : 이펙티브 자바 3/E 효과적으로 읽기 - SLiPP 스터디 목차

이펙티브 자바 3/E 효과적으로 읽기
자바 6까지 다루던 이펙티브 자바 2판이 자바 7, 8, 9를 다루기 위해 3판으로 다시 돌아왔습니다.
그동안 객체 지향에 치중하던 자바에 새로 도입된 함수형 프로그래밍 요소도 자세히 알아봅니다.

이펙티브 자바 Effective Java 3/E(http://www.yes24.com/24/Goods/65551284)를 읽으며 스터디를 진행합니다.
자바 8, 9에서 새로 다룬 내용이 과연 무엇인지 비교 정리합니다.
기존 2판에서 다루던 내용보다 3판에서 새로 다루는 내용에 많은 시간을 편성합니다.
2명 씩 조를 나누고 돌아가면서 각 주차의 주제에 대해 공부 후 발표하는 식으로 진행합니다.
Java Enum 활용기(http://woowabros.github.io/tools/2017/07/10/java-enum-uses.html)와 같은 사례들을 찾아보고 정리하면 더 좋습니다.
아래의 유튜브를 참고하며 진행합니다.

1주차 : 1장 들어가기, 2장 객체 생성과 파괴 읽기
2주차 : 3장 모든 객체의 공통 메서드 읽기, 4장 클래스와 인터페이스 읽기
3주차 : 5장 제네릭 읽기, 6장 열거 타입과 애너테이션 읽기
4주차 : 7장 람다와 스트림 읽기
5주차 : 중간 세미나
6주차 : 8장 메서드, 9장 일반적인 프로그래밍 원칙 읽기
7주차 : 10장 예외, 11장 동시성 읽기
8주차 : 12장 직렬화 읽기
9주차 : 회고

이펙티브 자바 3판 - 4. 클래스와 인터페이스 (item18~24)

Item18. 상속보다는 컴포지션을 사용하라

상속은 코드를 재사용하는 강력한 수단이지만 항상 최선은 아니다.

확장할 목적으로 설계되었고 문서화도 잘 된 클래스(item19)이거나 같은 프로그래머가 통제한다면 괜찮지만

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

  • 상위클래스에 따라 하위 클래스의 동작에 이상이 생길 수 있음

  • 상위클래스는 릴리즈 마다 내부 구현이 달라질 수 있음 -> 하위클래스 영향 받을 수 있음

  • 상위클래스를 확장을 고려하지 않고 설계 하거나, 문서화도 하지 않으면 하위 클래스도 매번 수정 돼야 함

  • HashSet을 확장한 MyHashSet

public class MyHashSet<E> extends HashSet<E> {
    private int addCount = 0; // 추가된 원소의 개수

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

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

    public int getAddCount() {
        return addCount;
    }
}

MyHashSet<String> mySet = new MyHashSet<>();
mySet.addAll(List.of("탁","탁탁","펑"));

// 출력되는 값은? 6 
System.out.println(mySet.getAddCount());
  • HashSet(AbstractSet)의 addAll 메서드
    • HashSet의 addAll 메서드가 add 메서드를 사용하여 구현되어서..2번 더해지는..
public boolean addAll(Collection<? extends E> c) {
    boolean modified = false;
    for (E e : c)
        if (add(e))
            modified = true;
    return modified;
}
    • 다음 릴리즈에서 상위 클래스에 새로운 메서드가 추가 된다면???
  • 매서드 재정의가 문제!

재정의 대신 새로 만든다면?

  • 메서드를 재정의하는 것보다 새로 만드는 게 조금 더 나을 수도 있다.
  • 안전하긴 하지만 하위 클래스에 추가한 메서드와 시그니처가 같고 리턴 타입만 다르다면? 그 클래스는 컴파일조차 되지 않음.

컴포지션(Composition)

  • 기존 클래스를 확장하는 대신 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 한다.
  • 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻에서 이를 컴포지션(Composition)이라고한다.
  • 새로운 클래스의 인스턴스 메서드들은 기존클래스에 대응하는 메서드를 호출해 그 결과를 반환. 이를 전달(Forwarding)이라고 함
  • 새 클래스의 메서드들은 전달 메서드

이렇게 되면 새로운 클래스는 기존 클래스의 영향이 적어지고 기존 클래스 안에 새로운 메서드가 추가되어도 안전함

  • 위의 예제를 컴포지션으로 수정
 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); }
    // ... 생략
}
  • 다른 Set 인스턴스를 감싸고 있다는 뜻에서 MySet과 같은 클래스를 래퍼 클래스라고 함
  • 다른 Set에 계측 기능을 덧씌운다는 뜻에서 데코레이터 패턴(Decorator Pattern)
  • 컴포지션과 전달의 조합은 넓은 의미로 위임(delegation)이라고하지만, 엄밀히 따지면 래퍼 객체가 내부 객체에 자기 자신의 참조를 넘기는 경우에만 해당됨

언제 상속을 해야 할까?

  • 클래스가 B가 클래스 A와 is-a 관계일때만
  • 반드시 하위 클래스가 상위 클래스의 진짜 하위 타입인 상황에서만
    • 클래스 A를 상속하는 클래스 B를 만드려고 한다면, “B가 정말 A인가?” 를 생각
  • 그 조건이 아니라면 A를 클래스 B의 private 인스턴스로 두면 됨. A는 B의 필수 구성요소가 아니라 구현하는 방법 중 하나로..

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

상속가능한 클래스는 재정의 할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다.

  • 재정의 가능 = public, protected 메서드 중 final이 아닌 모든 메서드
  • 어떤 순서로 호출하는지, 호출결과가 이어지는 결과에 어떤 영향을 주는지..
  • 재정의 가능 메서드를 호출할 수 있는 모든 상황을 문서로 남겨야 함

'어떻게' 가 아닌 '무엇을' 하는지를 설명해야 하지만, 상속이 캡슐화를 해치기 때문에 클래스를 안전하게 상속하기 위해서 내부 구현 방식을 설명할 수 밖에 없음..

@implSpec 태그

  • API문서 마지막에 "Implementation Requirements" 에서 메서드 내부 동작 설명
  • java8에서 @implSpec 태그 도입
  • 활성화 하려면 명령줄 매개변수로 -tag "implSpec:a:Implementation Requirements:"

protected 메서드 공개

  • 문서만 남긴다고 상속을 위한 설계는 아님
  • 클래스의 내부 동작 과정에서 중간에 끼어들 수 있는 hook을 protected메서드 형태로 공개
  • 어떤 메서드를? 심사숙고해서.. 실제 하위 클래스를 만들어 보고..
  • 최대한 적게, 하지만 너무 적어서 상속의 이점을 해치치 않게..

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

상속용 클래스의 생성자는 재정의 가능 메서드를 호출하면 안된다

  • 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행 됨
  • 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자 보다 먼저 호출 됨
public class Super {
	public Super() {
		overrideMe();
	}
	
	public void overrideMe() {
	}
}
public final class Sub extends Super {
	private final Instant instant;
	
	Sub() {
		instant = Instant.now();
	}
	//상위 클래스의 생성자 호출 됨 
	@Override public void overrideMe() {
		System.out.println(instnat);
	}
	
	public static void main(String[] args) {
		Sub sub = new Sub();
		sub.overrideMe();
	}
}
  • null - instant 출력
    • 상위 클래스의 생성자는 하위 클래스의 생성자가 인스턴스 초기화도 하기 전에 overrideMe를 호출

Cloneable / Serializable

  • 둘 중 하나라도 구현한 클래스를 상속할 수 있게 설계하는 것은 좋지 않음
  • clone/readObject 는 생성자와 비슷한 효과를 냄 (새로운 객체 만듦)
  • 직 간접적으로 재정의 가능 메서드를 호출하면 안됨
  • clone이 잘 못 복제되면 원본에도 영향을 줄 수 있고
  • Serializable을 구현한 상속용 클래스가 readResolve나 writeReplace 메서드를 갖는다면 protected로 선언해야함. private로 선언되면 하위 클래스에서 무시 됨

구체 클래스

  • final도 아니고 상속용으로 설계되지도, 문서화도 안됨
  • 클래스의 변화가 일어날 때마다 오동작 가능성 생김
  • 상속용으로 설계되지 않았으면 상속을 금지하라
    • 클래스를 final로 선언
    • 모든 생성자를 private이나 package-private로 선언하고 public 정적 팩터리로 만들어 줘라

클래스 내부에서 어떻게 하는지 모두 문서로 남기고 문서화 한 것은 반드시 지켜야 한다.

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

  • 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 함

  • 인터페이스가 선언한 메서드를 모두 정의하고 그 일반 규약을 지켰다면 어떤 클래스를 상속하든 같은 타입

  • 인터페이스 추가 구현은 다소 쉽지만, 기존 클래스 위에 새로운 추상 클래스를 끼워 넣는일은 일반적으로 어려 움

    • 두 하위 클래스 위에 추상 클래스를 넣으려면 추상클래스는 자손들에 공통 조상이어야 함

mixin

  • 인터페이스는 mixin 정의에 안성맞춤
  • 클래스가 구현할 수 있는 타입으로 믹스인을 구현한 클래스에 원래 타입외 선택적 행위를 제공
  • 기존 클래스에 덧 씌울 수 없어서 추상클래스로는 불가능
  • 계층 구조로는 합리적인 위치가 없음
  • 인터페이스는 계층 구조 없는 타입 프레임워크 만들 수 있음
  • 타입을 추상클래스로 정의하면 그 타입에 기능 추가는 상속 뿐..

템플릿 메서트 패턴

  • 인터페이스와 추상 골격 구현 클래스를 함께 제공
  • 인터페이스로 타입 , 디폴트 메소드 제공
  • 골격 구현 클래스는 나머지 메서드 구현
  • 관례적으로 인터페이스는 Interface, 구현클래스는 AbstractInterface 로 이름 짓는다
  • ex) AbstractCollection, AbstractSet, AbstractList, AbstractMap
  • 제대로 설계 되었다면 골격 구현은 그 인터페이스만으로도 개발자를 수월하게 해 줌
static List<Integer> intArrayAsList(int[] a) {
	Object.requireNonNull(a);
	
	return new AbstractList<>() {
		@Override public Integer get(int i) {
			return a[i];
		}
		@Override public Integer set(int i, Integer val) {
			int oldVal = a[i];
			a[i] = val; //오토언박싱
			return oldVal; 
		}
		
		@Override public int size() {
			return a.length;
		}
	};
}
			
  • AbstractList 골격 구현 , List 구현체 -> 이 경우 확장하는것만으로도 인터페이스 구현함
  • 추상클래스처럼 구현을 도와주지만 제약에서는 자유로움
  • ==> 시뮬레이트한 다중 상속 : 다중 상속의 장점 제공, 단점 피함

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

  • ~ java8 : 기존 구현체를 깨트리지 않고 인터페이스에 메서드 추가 불가능
  • java8 ~ : 기존 인터페이스에 default 메서드 추가 가능

default 메서드

  • default를 선언하면 인터페이스를 구현한 후 디폴트 메서드를 재정의 하지 않은 모든 클래스에서 디폴트가 쓰임

  • java8에서 추가된 자바 라이브러리의 디폴트 메서드는 코드 품질이 높고 범용적이라 대부분 잘 동작하지만,

  • java7까지는 모든 클래스가 현재 인터페이스에 새로운 메서드가 추가될 일이 없다고 생각하고 구현 됨

  • java8 컬렉션 인터페이스들에 다수의 디폴트 메서드 추가 됨 (람다 활용을 위해, 실제로 기존의 많은 코드들이 영향 받음)

    • ex) Collection 인터페이스의 removeIf 메서드
      • 디폴트 메서드가 추가됨에 따라 자바 플랫폼 라이브러리에서는 디폴트 메서드를 재정의 하고, 다른 메서드에서는 디폴트 메서드 호출 전에 필요한 작업을 수행하도록 함
      • 자바 플랫폼이 아닌 3rd party 구현체들은 이런 언어차원의 인터페이스 변화에 발맞춰 수정되지 못함.
  • 기존 인터페이스에 새로운 디폴트 메서드를 추가 하는 일은 꼭 필요한 경우가 아니라면 피해야 함

  • 새로운 인터페이스라면 표준 메서드 구현을 제공하는데 아주 유용한 수단이며, 그 인터페이스를 더 쉽게 구현에 활용할 수 있게 하는 것 임

  • 인터페이스를 설계 할 때는 여전히 주의 해야 함
  • 새로운 인터페이스는 릴리스 전에 반드시 테스트

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

인터페이스?

자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 함

클래스가 인터페이스를 구현하는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에게 알려주는 것

=> 이 용도로만 사용해야한다!

잘못된 예) 상수 인터페이스

  • 메서드 없이 static final 필드만 가득 찬 인터페이스
  • 클래스 내부에서 사용하는 상수는 외부 인터페이스가 아닌 내부 구현임
  • java.io.ObjectStreamConstatns등 자바 플랫폼 라이브러리에도 몇개 존재하나, 잘못한 예 이니 따라하지 말 것

상수 공개 목적

  • 특정 클래스나 인터페이스와 강하게 연관된 상수라면 그 클래스나 인터페이스 자체에 추가 해야 함
  • 열거타입으로 타나내기 적합한 상수라면 열거타입으로 만들어 공개 (Item34)
  • 인스턴스화 할 수 없는 유틸리티 클래스에 담아 공개(Item4)
    • 유틸리티 클래스에 정의된 상수를 클라이언트에서 하용하려면 클래스 이름까지 명시 해야 한다.
      • ex) PhysicalContains.AVOGADROS_NUMBER
    • 유틸리티 클래스의 상수를 빈번히 사용한다면 static import 하여 사용

인터페이스는 타입을 정의하는 용도로만 사용해야한다. 상수 공개용 수단으로 사용하지 말자.

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

태그 달린 클래스?

두 가지 이상의 의미를 표현하고, 그 중 현재 표현하는 의미를 태그 값으로 알려주는 클래스

//code

태그 달린 클래스 단점

  • 쓸때없는 코드가 많다 (ex, Enum 타입, 태그필드, switch문 등..)
  • 가독성이 안좋음
  • 메모리 많이 사용 (다른 의미를 위한 코드도 언제나 있음), 필드를 final로 선언하려면 쓸떼없는 필드드 까지 생성자에서 초기화
  • 다른 의미를 추가하려면 코드 수정 필요 (ex, switch문도 찾아서 수정..), 런타임 오류 가능성 높아짐
  • => 장황하고, 오류내기 쉽고, 비효율적

객체 지향 언어는 타입 하나로 다양한 의미의 객체를 표현하는 "서브타이핑" 제공

태그달린 클래스 -> 클래스 계층 구조 변경 방법

  1. 계층 구조의 root가 될 추상 클래스 정의
  2. 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언
  3. 태그 값에 상관없이 동작이 일정한 메서드들을 루트 클래스에 일반 메서드로 추가
  4. 하위 클래스에서 공통으로 사용하는 데이터 필드도 루트 클래스로 올림
  5. 루트 클래스를 확장한 구체 클래스를 의미별로 정의
  6. 루트 클래스가 정의한 추상 메서드를 각자의 의미에 맞게 구현
abstract class Figure {
    abstract double area();
}

class circle extends Figure {
    final double radius;
    Circle(double radius) { this.radius = radius; }
    @Override double area() {return Math.PI * (radius * radius);}
}

class Rectangle extends Figure {
    final double length;
    final double width;
    
    Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }
    
    @Override double area() { return length * width ; }
}

클래스 계층 구조

  • 간결, 명확, 관렶없고 쓸떼없는 코드가 없음
  • 각 클래스의 생성자가 모든 필드를 초기화 하고 추성 메서드를 구현했는지 컴파일러가 확인 가능
  • 독립적으로 확장 가능
  • 유연성

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

Item24. 멤버 클래스는 되도록 static 으로 만들라

중첩 클래스를 언제, 왜 사용해야 하는지..

  • 중첩 클래스?
    • 다른 클래스 안에 정의된 클래스
    • 자신을 감싼 바깥 클래스에서만 쓰여야 함
    • 종류
      • 정적 멤버 클래스
      • inner class : (비정적) 멤버 클래스, 익명 클래스, 지역 클래스
  1. 정적 멤버 클래스

    • 다른클래스 안에 선언, 바깥 클래스의 private 멤버에 접근 가능
    • 정적 멤버와 접근 규칙 동일
    • ex) Calculator.Operation.PLUS 와 같은 형태로 사용 가능
    • private 정적 멤버 클래스
      • 바깥 클래스가 표현하는 객체의 한 부분을 나타 낼 때 사용됨
  2. 비정적 멤버 클래스

    • 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결
    • 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this를 사용해 바깥 인스턴스의 메서드 호출 하거나 참조 가져옴
    • 클래스명.this 로 명시
    • 비정적 멤버 클래스와 바깥 인스턴스 사이의 관계는 멤버 클래스가 인스턴스화 될때 생기며, 더 이상 변경할 수 없음
      • 바깥 클래스의 인스턴스 메서드에서 비정적 멤버 클래스의 생성자를 호출할 때 보통 자동으로 생성
      • 직접 바깥 인스턴스 클래스.new MemberClass(args)로 생성 가능
      • 비정적 멤버 클래스의 인스턴스 안에 만들어져 메모리 공간 차지, 생성시간 오래 거림
    • 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스 처럼 보이게 하는 뷰로 사용 --> 어댑터 정의 할때 주로 쓰임

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

  • 시간과 공간이 더 필요
  • gc가 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수 발생 가능

멤버 클래스가 공개된 클래스의 public 이나 protected 멤버라면, 멤버클래스 역시 공개 API가 됨으로 추후 하위 호환성을 위해서라도 정적/비정적은 더 중요함

  1. 익명 클래스
    • 이름이없고, 바깥 클래스의 멤버도 아님
    • 쓰이는 시점에 선언과 동시에 인스턴스가 생성 됨
    • 코드의 어디서든 만들 수 있음
    • 제약이 많음
      • 선언한 지점에서만 인스턴스 생성 가능
      • instanceof 검사나 클래스 이름이 필요한 경우 사용 불가
      • 여러 인터페이스 구현 불가
      • 인터페이스 구현과 다른 클래스 상속 불가
      • 그 익명 클래스의 상위에서 상속한 멤버 외 호출 불가
    • 가독성 떨어짐
    • 람다 이전에 즉석에서 작은 함수나 객체 리턴하기 위해 많이 쓰였음. 지금은 람다로 대체
    • 정적 팩터리 메서드 구현시 주로 활용 됨
  2. 지역 클래스
    • 지역 변수 선언할 수 있는 곳에서 선언 가능. 유효범위 동일
    • 이름이 있고
    • 반복사용가능
    • 정적멤버 가질 수 없음
    • 가독성 위해 짧아야 함

이펙티브 자바 3판 - 8.메서드(item 56)~ 9. 일반적인 프로그래밍 원칙(item63)

Item56 공개된 API 요소에는 항상 문서화 주석을 작성하라

  • 자바독

  • 공개된 모든 클래스, 인터페이스, 메서드, 필드 선언에 문서화 주석을 달아야 한다.

  • 메서드용 문서화 주석에는 해당 메서드와 클라이언트 사이의 규약을 기술 해야 한다

    • 어떻게가 아니라 무엇을 하는지를 기술
    • 클라이언트가 해당 메서드를 호출 하기 위한 전제조건 (@throws 태그로 비검사 예외를 선언하며 암시적으로)
    • 수행 후 사후조건
    • 부작용 ex) 백그라운드 스레드
  • 스레드가 안전하든/안전하지 않든 스레드 안전 수준을 반드시 API설명에 포함해야 한다

  • 직렬화할 수 있는 클래스라면 직렬화 형태도 API에 기술 해야한다.

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

지역변수, 제어구조, 라이브러리, 데이터 타입, 리플렉션, 네이티브 메서드, 최적화와 명명 규칙

Item57 지역변수의 범위를 최소화하라

  • 지역변수의 유효 범위를 최소로 줄이면 코드 가독성과 유지보수성이 높아지고 오류 가능성은 낮아진다
  • 가장 좋은 방법은 '가장 처음 쓰일 때 선언'
  • 거의 모든 지역변수는 선언과 동시에 초기화
  • 초기화 하기에 충분하지 않다면 가능할 때 까지 미뤄라
    • 예외) try-catch문 : 변수를 초기화 하며 검사 예외가 발생할 가능성이 있다면 try문에서 초기화
  • 반복변수의 값을 반복문 종류 후에도 사용하는게 아니라면 while 문 보다 for문
    • 반복 변수의 범위는 반복문의 몸체와 for 키워드 안으로 제한되기 때문
  • 메서드를 작게 유지하고 한 가지 기능에 집중
    • 한 메서드에 여러 기능을 넣는다면 불필요한 변수에 접근 가능

Item58 전통적인 for 문보다는 for-each 문을 사용하라

  • enhanced for statement
  • 반복자와 인덱스 변수 사용 하지 않음
  • 배열이든 컬렉션이든 하나의 관용구로 사용 가능하며 속도도 동일
  • 중첩일수록 가독성 높아짐
    enum Suit { CLUB, DIAMOND, HEART, SPACE}
    enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,
                NINE, TEN, JACK, QUEEN, KING}

                ...
    static Collection<Suit> suits = Arrays.asList(Suit.values());
    static Collection<Rank> ranks = Arrays.asList(Rank.values());

    List<Card> deck = new ArrayList<>();
    for (Iterator<Suit> i = suits.iterator(); i.hasNext(); ) 
        for(Iterator<Rank> j = ranks.iterator(); j.hasNext();)
            deck.add(new Card(i,next(), j.next()));
  • 마지막 줄 i.next()는 Suit 마다 한번씩이 아닌 Rank 마다 한번 씩 불리게 되어 다 사용하게 되면 NoSuchElementException 이 발생
for (Suit suit : suits)
	for(Rnak rank : ranks)
		deck.add(new Card(suit, rank);
  • for-each를 사용하지 못하는 경우

    • 파괴적 필터링 : 컬렉션을 순회하면서 선택된 원소 제거. java8 부터는 Collection의 RemoveIf 사용
    • 변형 : 리스트나 배열을 순회하면서 원소의 값을 교체 하는 경우
    • 병렬 반복
  • 가능한 모든 곳에서 for 대신 foreach를 사용하라

Item59 라이브러리를 익히고 사용하라

  1. 표준 라이브러리를 사용하면 그 코드를 작성한 전문가의 지식과 앞서 사용한 다른 프로그래머들의 경험을 활용할 수 있다.
    • 표준 라이브러리는 알고리즘에 능통한 개발자가 설계와 구현과 검증에 시간을 들여 개발했고, 여러 전문가가 동작을 검증 함

    • 릴리즈 후 수 많은 개발자들이 사용하고, 버그가 발견 된다면 다음 릴리즈에서 수정 될 것이다~~

    • Java7~ Random 대신 ThreadLocalRandom 을 사용하면 대부분 잘 동작 (고 품질의 무작위 수 생산, 속도 빠름)

  2. 핵심적인 일과 크게 관련없는 문제를 해결하느라 시간 허비하지 않아도 됨. 어플리케이션 개발에 집중
  3. 노력하지 않아도 성능이 지속해서 개선 됨
  4. 기능이 점점 많아 짐
  5. 읽고 유지보수 쉬움
  • 라이브러리에 기능이 있는줄 몰라서 직접 구현하는 낭비를 하지 말고 릴리즈를 읽어보자!
  • 라이브러리를 사용하려고 시도 -> 서드파티 라이브러리 찾고 -> 직접 구현

Item60 정확한 답이 필요하다면 float와 double은 피하라

  • float 와 double은 공학 계산용으로 설계 됨
  • ex) System.out.println(1.00 - 9 * 0.10); // 0.09999999999999998
  • 반올림을 해도 틀릴 수 있다
  • 금융계산에는 BigDecimal, int, long을 사용하라
  • BigDecimal 단점 : 기본 타입보다 쓰기가 불편하고 느림
  • int, long : 값의 크기가 제한되고, 소수점을 직접 관리해야 함

Item61 박싱된 기본 타입보다는 기본 타입을 사용하라

  • 자바의 data type
    • 기본 타입 : int, double boolean
    • 참조 타입 : Integer, Double, Boolean
    • 오토박싱과 오토언박싱으로 구분하지 않고 사용할 수는 있음
기본타입 참조타입
값 + 식별성
값은 언제나 유효 null 가능
시간 메모리 효율적 덜 효율적
 Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
       

naturalOrder.compare(new Integer(42), new Integer(42)); ?? 1

i < j는 잘 동작 : i, j가 참조하는 기본 값 타입으로 변환 됨
i == j 에서 객체 참조의 식별성 검사를 하면 서로 다른 Integer 인스턴스라면 값은 같아도 비교 결과는 false

  • 같은 객체를 비교하는것이 아니라면 박싱된 기본 타입에 == 연산자를 사용하면 오류가 나타 남
  • Comparator 로 비교 하는 것이 맞음
 Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
 	int i = iBoxed; int j = jBoxed; //오토박싱 
	return i < j ? -1 : (i == j ? 0 : 1);
	
  • 박싱된 타입을 각각 기본타입으로 변환 후 비교
static Integer i; // int i;

    public static void main(String[] args) {
        if(i == 42)
            System.out.println("블라블라");
    }
}
  • i == 42는 Integer와 비교 하게 되어 NullPointerException
  • 기본 타입과 박싱된 타입을 혼용한 연산에서는 박싱된 기본 타입의 박싱이 자동으로 풀림
  • null 참조를 unboxing 하면 NullPointerException

Item62 다른 타입이 적절하다면 문자열 사용을 피하라

  • 문자열은 다른 값 타입을 대신하기에 적합하지 않다
  • 기본 타입이든 참조 타입이든 적절한 값이 있다면 그것들 사용하고 없다면 새로 하나 작성 하라
  • 문자열은 열거 타입을 대신하기에 적합하지 않다.
  • 혼합된 데이터를 하나의 문자열로 표현하는 것은 좋지 않다 ex) String compoundKey = className + "#" + i.next();
  • 개별 요소에 접근하려면 파싱, 느리고, 귀찮고 오류가능성만 높아짐. 적절한 equals, toString, compareTo 메서드 제공 불가

Item63 문자열 연결은 느리니 주의하라

  • 문자열 연결 연산자 + 는 편리하지만 성능 저하
  • + 로 문자열 N개를 잇는 시간은 N의 제곱에 비례 함
  • String 대신 StringBuilder를 사용 하자

이펙티브 자바 3판 - 2. 객체 생성과 파괴~ 3.모든객체의 공통메소드(item11)

finalizer 와 cleaner 의 사용을 피하라

GC는 컨트롤 가능한가?

  • 내가 원할때 소멸시키는가 / 아니다.
  • finalizer의 대안 cleaner 역시 문제가 많다.
  • try with resource(auto closable) vs finalize gc 성능이 50배(12ns vs 550ns) 차이 난다.
  • 그럼 언제 저것들을 쓰고 있나? / 효과있나?
    • 닫지 않은 파일/커넥션등을 아주~늦게 나마 회수해준다.(FileInputStream, ThreadPoolExecutor)
    • 네이티브피어(jni 같이 c 등 다른 언어 메소드를 연결하는 것) 객체(자바 객체가 아니니 알지 못해서) //이
      때는 성능저하가 불가피 할 듯 보이고 close()를 꼭 해야할 것 같다.
  • 이 대안은 그럼 무엇?
    • AutoCloseable을 구현한다.
public class Room implements AutoCloseable {
    private static final Cleaner cleaner = Cleaner.create();

    // 청소가 필요한 자원. 절대 Room을 참조해서는 안 된다!
    private static class State implements Runnable {
        int numJunkPiles; // Number of junk piles in this room

        State(int numJunkPiles) {
            this.numJunkPiles = numJunkPiles;
        }

        // close 메서드나 cleaner가 호출한다.
        @Override public void run() {
            System.out.println("Cleaning room");
            numJunkPiles = 0;
        }
    }

    // 방의 상태. cleanable과 공유한다.
    private final State state;

    // cleanable 객체. 수거 대상이 되면 방을 청소한다.
    private final Cleaner.Cleanable cleanable;

    public Room(int numJunkPiles) {
        state = new State(numJunkPiles);
        cleanable = cleaner.register(this, state);
    }

    @Override public void close() {
        cleanable.clean();
    }
}
  • 사실 위의 코드는 정확한 흐름을 모르겠다.(수정필요)
  • 그럼 힙 메모리 세팅, gc 종류 선택 등에 대해 공유(난 경험이 없다..)

try - finally 보다 try-with-resource를 사용하라

  • 자원(파일, 커넥션) 을 닫는 것을 클라이언트가 놓칠 수 있다.(커넥션이 계속 열고 안닫아지면..)
  • 일반적으로 finally에 close를 많이 했다. ( 내 경우 )
static String firstLineOfFile(String path) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(path));
        try {
            return br.readLine();
        } finally {
            br.close();
        }
 }
  • 만약 한번더 오픈을 한다면?
 static void copy(String src, String dst) throws IOException {
        InputStream in = new FileInputStream(src);
        try {
            OutputStream out = new FileOutputStream(dst);
            try {
                byte[] buf = new byte[BUFFER_SIZE];
                int n;
                while ((n = in.read(buf)) >= 0)
                    out.write(buf, 0, n);
            } finally {
                out.close();
            }
        } finally {
            in.close();
        }
    }
  • 이 경우 어떤 문제에 의해서 close에서도 문제가 생기다면? 두번째(close)예외의 메시지만 준다. 그래서 문제 파악을 힘들게 만든다.( 그런데 장치의 고장이라고 하는데 구체적으로 어떤 상황일가. 파일 인풋 아웃풋일텐데. 예상으론 통신시 갑작스런 오류? / 디스켓...? )
  • 아래는 try with resource 로 고친 코드다
 static String firstLineOfFile(String path) throws IOException {
        try (BufferedReader br = new BufferedReader(
                new FileReader(path))) {
            return br.readLine();
        }
  }

static void copy(String src, String dst) throws IOException {
        try (InputStream   in = new FileInputStream(src);
             OutputStream out = new FileOutputStream(dst)) {
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while ((n = in.read(buf)) >= 0)
                out.write(buf, 0, n);
        }
 }

equals 는 일반 규약을 지켜 재정의하라

언제 재정의 해야하는가

  • 목표는 객체 식별성이 아니라 논리적 동치성을 확인해야 한다.

  • 상위 클래스가 논리적 동치성을 비교하도록 되어있지 않을 경우다.

  • 주로 값을 표현하는 클래스들이 해당한다.(ex: integer, String)

  • 값 클래스라 하더라도 인스턴스가 둘 이상 만들어지지 않는(Enum) 의 경우도 재정의가 불필요하다. (스태틱도?)

  • 대칭성

    • 상 하위 모두 호환 가능해야한다.
    • caseinsensitiveString 은 string을 알고 있지만, 반대는 아니다.
public final class CaseInsenstiveString{
     private final String s;
     public CaseInsensitiveString(String s){
         this.s = Objects.requireNorNull(s);
     }
 }

@Override 
public boolean equals(Object o) {
    if (o instanceof CaseInsensitiveString)
        return s.equalsIgnoreCase(
                ((CaseInsensitiveString) o).s);
    if (o instanceof String)  // 한 방향으로만 작동한다!
        return s.equalsIgnoreCase((String) o);
    return false;
}
@Override 
public boolean equals(Object o) {
    return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
  • 추이성
    • 하위 상속 클래스와 같고 상속하는 클래스와 그 하위의 클래스와 같다면, 최상위와 최 하위도 같아야 한다.
    • 아래는 좌표멤버를 멤버로(x,y) 같는 Point클래스, 상속하는 멤버(Color) 를 갖는 상속 클래스이다. 이 경우 equals의 동작은 어떠할가. 동작은 하지만 색상 정보를 놓치니 용납할 수 없다.
public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override public boolean equals(Object o) {
        if (!(o instanceof Point))
            return false;
        Point p = (Point)o;
        return p.x == x && p.y == y;
    }
}

public class ColorPoint extends Point {
    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
}
대칭성 실패
@Override 
public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        return super.equals(o) && ((ColorPoint) o).color == color;
}

추이성 실패
  @Override 
public boolean equals(Object o) {
    if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint) o;
        return cp.point.equals(point) && cp.color.equals(color);
}

- 리스코프 치환법칙 실패
    * 리스코프 치환 법칙 : 타입의 메소드는 하위 타입에도 모두 작동해야한다.

@Override
public boolean equals(Object o) {
    if (o == null || o.getClass() != getClass())
        return false;
    Point p = (Point) o;
    return p.x == x && p.y == y;
}
  • 작은 결론 1 : 하위 클래스를 구체화 하면서 모든 규칙을 지킬 수 없다.
  • 그럼 방법은? 상속 대신 컴포지션을 활용하라
    • 상위 클래스 타입의 변수를 멤버로 두고 그것을 반환하는 뷰 메소드를 public으로 작성한다.
    • 아무 값도 갖지 않는 클래스를 베이스로 두고 확장한다.(베이스 클래스를 인스턴스하지 않기 때문에 위배되지 않는다)
public class ColorPoint {
    private final Point point;
    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        point = new Point(x, y);
        this.color = Objects.requireNonNull(color);
    }

    /**
     * 이 ColorPoint의 Point 뷰를 반환한다.
     */
    public Point asPoint() {
        return point;
    }

    @Override public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint) o;
        return cp.point.equals(point) && cp.color.equals(color);
    }
}
  • equlas 의 판단에 가변적인 자원이 사용되지 않도록 한다.

    • java.net.URL의 equals가 예다. URL과 ip의 호스트를 비교하는데 이때 네트웍을 통하게 된다.
  • 모든 객체는 null 이 아니어야한다.

    • 동치성 검사를 위해 적절한 형변환 후 값을 비교한다.
    • instanceof는 비교하는 객체가 null인지 검사한다.(그래서 ==null 을 할 필요 없다)
  • primitive type (float, double 제외) 는 "==" 연산자로 비교하고, 레퍼런스 타입 필드는 equals 로 float,double은 compare 메소드로 비교한다.

  • 성능이 걱정된다면 cost가 적을 것 같은 필드부터 비교한다.

  • 상태(lock)등을 연산하지 말자. (논리적 상태만을 비교하자)

  • 캐쉬된 값을 저장하는 파생클래스 변수가 있을 경우 활용하자.

  • 완벽한 equals 를 보여주는 코드

public final class PhoneNumber {
    private final short areaCode, prefix, lineNum;

    public PhoneNumber(int areaCode, int prefix, int lineNum) {
        this.areaCode = rangeCheck(areaCode, 999, "지역코드");
        this.prefix   = rangeCheck(prefix,   999, "프리픽스");
        this.lineNum  = rangeCheck(lineNum, 9999, "가입자 번호");
    }

    private static short rangeCheck(int val, int max, String arg) {
        if (val < 0 || val > max)
            throw new IllegalArgumentException(arg + ": " + val);
        return (short) val;
    }

    @Override public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof PhoneNumber))
            return false;
        PhoneNumber pn = (PhoneNumber)o;
        return pn.lineNum == lineNum && pn.prefix == prefix
                && pn.areaCode == areaCode;
    }
}
  • 주의사항
    • Object 타입 이외의 파라메터로 구현하지 말자.(이것은 재정의가 아니라 다중정의다.)
  • 좋은 툴(라이브러리
    • 구글의 AutoValue가 있다. 알아서 해준다. ide에서도 해준다( 결국 ide, 괜찮은 lib을 쓰자)

equals 를 정의할땐 hashcode도 정의하자

Hashmap, hashSet등의 원소로 사용될 경우 문제가 발생한다.

HashMap<PhonNumber,String> m = new Hashmap<>();
m.put(new PhoneNumber(707, 867, 5309),"jenny");
m.get(new PhoneNumber(707, 867, 5309));

what happened?

  • return null
  • 넣을때 한번, 꺼낼때 한번 객체를 생성했다. 하지만 이들은 논리적 동치이다. 둘의 해쉬가 다르기 때문이다.
  • PhoneNumber 클래스에 dashcode 를 적절하게? 구현해준다.
@override
public int hashcode(){ return 42;}
  • 이렇게 하면 O(1) 이 아니라 O(n) 이 된다. 모든 해쉬가 같기 때문에~ . 결국 다른 객체에 다른 해쉬를 반환하도록 하는것이 성능을 고려하는 측면이다.
  • 왠지 나만 모를 것 같은 내용 (https://ratsgo.github.io/data%20structure&algorithm/2017/10/25/hash/)
  • 해쉬함수 만드는 방식
    *
  • 위 PhoneNumber 클래스 해시함수 예시
@Override
public int hashCode() {
        int result = Short.hashCode(areaCode);
        result = 31 * result + Short.hashCode(prefix);
        result = 31 * result + Short.hashCode(lineNum);
        return result;
}

@Overrride
public int hashCode(){
    return Object.hash(lineNum,prefix,areaCode);
}
// 나라면 그냥 이렇게 쓸듯 하지만 성능이 아쉽다고 한다.
  • 성능을 고려하는 목저으로 핵심필드를 빼고 hashcode를 정의하지 말자.(해시테이블 성능을 놓치게된다)

toString을 항상 재정의하라

항상 적합한 문자열을 반환하지 않는다.

  • PhoneNumber@adbbb (클래스이름@16진수해쉬코드) 를 반환한다.
  • 하위 클래스에서는 간결하고 읽기 쉬운(핵심필드 들을) 형태로 toString을 정의해주는 것이 좋다.
  • 문제(디버깅)에 용이하게 만든다.
  • 모든 핵심 필드들을 출력하는 것이 좋다.
    /**
     * 이 전화번호의 문자열 표현을 반환한다.
     * 이 문자열은 "XXX-YYY-ZZZZ" 형태의 12글자로 구성된다.
     * XXX는 지역 코드, YYY는 프리픽스, ZZZZ는 가입자 번호다.
     * 각각의 대문자는 10진수 숫자 하나를 나타낸다.
     *
     * 전화번호의 각 부분의 값이 너무 작아서 자릿수를 채울 수 없다면,
     * 앞에서부터 0으로 채워나간다. 예컨대 가입자 번호가 123이라면
     * 전화번호의 마지막 네 문자는 "0123"이 된다.
     */
@Override 
public String toString() {
        return String.format("%03d-%03d-%04d",
                areaCode, prefix, lineNum);
}
  • 하위 클래스에서 상위클래스의 적절한 toString이 있다면 말고 없다면 꼭 구현해주어야한다.

0. 스터디 진행방식

1. 스터디 가능한 평일 요일/시간/장소

  • 매주 목요일 평일 19:30부터 시작
  • 알파돔4타워 6층 게스트 미팅룸1
  • 시작일자 : 2018.01.16(수)

2. 책임비

  • 인당 5만원씩 걷어서 진행
  • 책임비에 대한 규칙은 첫번째 스터디 시간에 정하기
  • 책임비 입금 내역
이름 책임비
조민혜 O
지성인 O
윤지수 O
양성만 O
허세조자룡선생 O
박성수 O

3. 스터디 진행방식에 대한 의견

3.1 item 단위로 진행, 챕터를 적절하게 나눠서 진행

  • 장수 기준으로 item 나누고, 돌아가면서 진행

3.2 토이 프로젝트(스프링 부트 기반 웹앱 프로젝트)에 간단히 코드 개발하면서 진행

  • 필수로 한다면 부담이 될수도 있음
  • 주제를 정해놓고 뭔가를 만들면 좋을듯
  • 스터디 발표하는 사람 외에 각자 개발해본것들을 서로 공유

3.3 발표자

  • 1인

3.4 주기

  • 주 1회(기본)
  • 특수한 사정이 생기거나 하면 상황에 따라서 미룬다던지 융통성있게 진행

3.5 발표 순서

  • 미정

이펙티브자바 3판 - item 49 ~ item 55

Chapter 8. 메서드

메서드를 설계할 때 주의할 점

ITEM 49 매개변수가 유효한지 검사하라.

오류는 가능한 한 빨리 (발생한 곳에서) 잡아야 한다.
메서드 바디가 실행되기 전에 잡지 않으면 수행되는 중간에 모호한 에러를 뱉으며 죽을지도 모른다. 더 최악엔 에러도 없이 객체를 망가뜨려 실패 원자성을 해칠지도 모른다.

공개되지 않은 메서드라면 단언문 (assert) 를 사용해라.
단언문은 다음과 같은 특징이 있는데

  1. 실패하면 AssertionError 를 던진다.
  2. 런타임에 아무런 효과도, 성능저하도 없다.

물론 예외는 있다
유효성 검사 비용이 지나치게 높거나 실용적이지 않을 때, 혹은 계산 과정에서 암묵적으로 검사가 수행될 때
예를 들면 Collection.sort(List) 에서 상호 비교가능 유무 같은 케이스가 있겠다. (어차피 정렬 과정에서 검사가 수행되므로)

그렇다 한들 시간이 지나면 수행하면서 검사되는 지점과 매개변수를 받는 지점이 멀어지는것 같다. 명시적으로 검사하는게 좋지 않을까

매개변수에 제약을 두는게 좋다는게 아니다. 메서드는 최대한 범용저긍로 설계하되, 구현하려는 개념 자체가 특정한 제약을 내재한 경우도 드물지 않다.

결론

메서드나 생성자를 작성할 때면 그 매개변수에 어떤 제약이 있을지 검사해야한다. 그 제약들을 문서화하고 메서드 코드 시작 부분에서 명시적으로 검사해야한다.

ITEM 50 객체에 방어적 복사본을 만들라.

클라이언트가 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍 해야한다.

public Period(Date start, Date end) {}
// no-setter

가 존재할 때 다음과 같이 setter 가 없더라도 객체 내부를 수정할 수 있다.

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // p의 내부가 수정된다.

다음과 같이 방어한다

public Period(Date start, Date end) {
  this.start = new Date(start.getTime());
  this.end = new Date(end.getTime());
}

두번째로는 getter 로 반환받은 객체를 수정해 내부를 변경할 수 있다.

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.getEnd().setYear(98); // p 내부가 수정된다.

다음과 같이 방어한다.

public Date getStart() {
  return new Date(start.getTime());
}

정리

클래스가 클라이언트로부터 받는 혹은 클라이언트로 반환하는 구성요소가 가변이라면, 그 요소는 반드시 방어적으로 복사해야한다.
복사 비용이 너무 크거나 클라이언트가 그 요소를 수정할 일이 없음을 신뢰하고 불변식이 깨지더라도 그 영향이 오로지 클라이언트에게만 끼친다면
방어적 복사를 수행하는 대신 해당 구성요소를 수정했을 때 책임이 클라이언트에 있음을 문서에 명시한다.

ITEM 51 메서드 시그니처를 신중히 설계하라

  • 메서드 이름을 신중히 짓자
  • 편의 메서드 이름을 너무 많이 만들지 말라
  • 매개변수 목록은 짧게 유지하라
    • 여러 메서드로 쪼갠다. 잘못하면 메서드가 너무 많아질 수 있지만, 직교성을 높여 오히려 메서드 수를 줄여주는 효과도 있다.
    • 매개변수 여러개를 묶어주는 도우미 클래스를 만들어라.
    • 빌더패턴을 메서드 호출에 응용해라.
  • 매개변수의 타입으로는 클래스보다 인터페이스가 낫다.
  • boolean 보다는 원소 2개짜리 열거타입이 낫다.

직교성: 공통점이 없는 기능들이 잘 분리되어 있다. 기능을 원자적으로 쪼개 제공한다.

ITEM 52 다중정의는 신중히 사용하라

public class CollectionClassifier {
  public static String classify(Set<?> s) {
    return "집합"
  }
  public static String classify(List<?> l`) {
    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));
  }
}

다음 코드의 결과는 예상과 다르게 "그외" 만 세번 출력된다.
재정의한 메서드는 동적으로 (런타임에) 선택되고, 다중정의한 메서드는 정적으로 (컴파일타임에) 선택되기 때문인데

왜? 이해안됨

API 사용자가 매개변수를 넘기면서 어떤 다중정의 메서드가 호출될지 모른다면 프로그램이 오동작하기 쉽다. 런타임에 이상하게 행동할 것이며 API 사용자들은 디버깅하는데 시간을 낭비할 것이다.

그러니 다중정의가 혼동을 일으키는 상황은 피해야 한다.

  1. 매개변수 수가 같은 다중정의는 만들지마라, 다중정의 하는 대신 메서드 이름을 다르게 지어보자.
  2. 매개변수 수가 같더라도 매개변수들이 "근본적으로 다르다(radically difference)" 면 상관없다.

위 코드는 다음과 같이 바꾼다면 깔끔하다

public static String classify(Collection<?> c) {
  return c instanceof Set ? "집합" :
         c instanceof List ? "리스트" : "그외";
}

근본적으로 다르다는건 두 타입의 (null이 아닌) 값을 서로 어느쪽으로든 형변환 할 수 없다는 뜻이다. 다만 한가지 함정이 있는데
오토박싱과 제네릭이 이 생태계를 교란했다.

List 를 예로 들 수 있겠는데

for (int i = 0; i < 3; i++) {
  set.remove(i);
  list.remove(i);
}

List 인터페이스가 remove(Object) 와 remove(int) 를 다중정의 했다. 전자는 Object 를 제거하고 후자는 해당 int 인덱스의 원소를 제거한다. 반면 set.remove(Object) 밖에 없으므로 i가 오토박싱되어 기대한 대로 동작을 한것

두번째 함정은 람다와 메서드 참조에서 발생한다.

new Thread(System.out::println).start();

ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);

양쪽다 Runnable을 받는 다중정의 메서드를 갖고 있지만 1은 제대로 동작하고 2는 제대로 동작하지 않는데 그 이유는 submit 의 다중정의 메서드 중에 Callable 를 받는 메서드도 있다는 데 있다.

상식적으로 생각하기엔 println 이 void 를 반환하니 반환값이 존재하는 Callable 과 헷갈릴리 없다고 생각되지만 다중정의 해소 알고리즘은 이렇게 동작하지 않는다.

암시적 타입 람다식이니 부정확한 메서드 참조 인수 표현식이니 하는데 넘어가래서 넘어가기러 했다

정리하자면 메서드를 다중정의할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안된다. 서로 다른 함수형 인터페이스라도 서로 근본적으로 다르지 않다는 뜻이다.

정리

프로그래밍 언어가 다중정의를 허용한다고 해서 다중정의를 꼭 활용하란 뜻은 아니다.
일반적으로 매개변수 수가 같을 때는 다중정의를 피하는게 좋다. 상황에 따라, 특히 생성자라면 이 조언을 따르기가 불가능 할 수 있다.
그럴때는 헷갈릴 만한 매개변수는 형변환하여 정확한 다중정의 메서드가 선택되도록 해야 한다.
이것이 불가능하면, 예컨대 기존 클래스를 수정해 새로운 인터페이스를 구현해야 할 때는 같은 객체를 입력받는 다중 정의 메서드들이 모두 동일하게 동작하도록 만들어야 한다.
그렇지 못하면 프로그래머들은 다중정의된 메서드나 생성자를 효과적으로 사용하지 못할 것이고, 의도대로 동작하지 않는 이유를 이해하지도 못할 것이다.

ITEM 53 가변인수는 신중히 사용하라

정리

인수 개수가 일정하지 않은 메서드를 정의해야 한다면 가변인수가 반드시 필요하다. 메서드를 정의할 때 필수 매개변수는 가변인수 앞에 두고, 가변인수를 사용할 때는 성능 문제까지 고려하자. (인수를 0개 넣었을때 런타임에서야 알아차리는걸 막기위해)

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

흔히 성능 핑계를 대는데 두가지 면에서 틀린 주장이다.
첫번째, 성능 분석 결과 이 할당이 성능 저하의 주범이라고 확인되지 않는 한 이정도의 성능차이는 신경 쓸 수준이 못 된다.
두번째, 빈 컬랙션과 빈 배열은 굳이 새로 할당하지 않고도 반환할 수 있다.

성능이 문제가 되면 매번 똑같은 빈 "불변" 컬랙션을 반환하면 되지 않는가
길이 0인 배열도 불변이다.

정리

null 이 아닌 빈 배열이나 컬랙션을 반환하라. null 을 반환하는 api 는 사용하기 어렵고 오류 처리 코드도 늘어난다. 그렇다고 성능이 좋은것도 아니다.

ITEM 55

옵셔널은 checked exception 과 취지가 비슷하다. 즉 반환값이 없을 수도 있음을 API 사용자에게 명확히 알려준다. 클라이언트는 값을 받지 못했을 때 취할행동을 선택할 것이다.

결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional 를 반환하자

핵심정리

값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환값이 없을 가능성을 염두에 둬야 하는 메서드라면 옵셔널을 반환해야 할 상황일 수 있다. 하지만 옵셔널 반환에는 성능 저하가 뒤따르니, 성능에 민감한 메서드라면 null 을 던지는 편이 나을수도 있다. 그리고 옵셔널을 반환값 이외의 용도로 쓰는 경우는 매우 드물며, 옵셔널을 필드로 가지고 있어야할 클래스가 있다면 나쁜 냄새일 가능성이 높으니 다시한번 생각해볼것

이펙티브 자바 3판 - 10. 예외 (item75~77) ~ 6. 동시성 (item78~82)

Item 75: 예외의 상세 메시지에 실패 관련 정보를 담으라

  • 실패 순간을 포착하려면 발생한 예외에 관여된 모든 매개변수와 필드의 값을 실패 메시지에 담아야 한다.
  • 예외 메시지는 사용자가 아니라 프로그래머를 위한 것이기 때문에 가독성보다는 담긴 내용이 훨씬 중요하다.

Item 76: 가능한 한 실패 원자적으로 만들라

  • 실패 원자적(failure-atomic) -- 호출된 메서드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지해야 한다.
  • 불변 객체는 태생적으로 실패 원자적이다.
  • 가변 객체를 실패 원자적으로 만드는 방법
    • 작업 수행에 앞서 매개변수의 유효성을 검사
    • 객체의 임시 복사본에서 작업을 수행한 다음, 작업이 성공적으로 완료되면 원래 객체와 교환
    • 작업 도중 발생하는 실패를 가로채는 복구 코드를 작성
  • 실패 원자성은 항상 달성할 수 있는 것은 아니다.
  • 이 규칙을 지키지 못한다면 API 설명에 명시해야 한다.

Item 77: 예외를 무시하지 말라

  • catch 블록을 비워두면 예외가 존재할 이유가 없어진다.
  • 예외를 무시하기로 했다면 catch 블록 안에 그렇게 결정한 이유를 주석으로 남기고 예외 변수의 이름도 ignored로 바꿔놓도록 하자.
  • 예외를 무시하면 아무 상관없는 곳에서 갑자기 죽어버릴 수도 있다.

11. 동시성

Item 78: 공유 중인 가변 데이터는 동기화해 사용하라

  • 동기화는 일관성이 깨진 상태를 볼 수 없게 하는 것은 물론, 동기화된 메서드나 블록에 들어간 스레드가 같은 락의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다.
  • longdouble 외의 변수를 읽고 쓰는 동작은 원자적(atomic)이다.
    • 하지만 한 스레드가 저장한 값이 다른 스레드에게 '보이는가'는 보장하지 않는다.
  • 동기화는 배타적 실행뿐 아니라 스레드 사이의 안정적인 통신에 꼭 필요하다.
// 원래 코드
while (!stopRequested)
    i++;

// 최적화한 코드
if (!stopRequested)
    while (true)
        i++;
  • 응답 불가(liveness failure) -- 프로그램이 더 이상 진전이 없는 상태
    • 쓰기와 읽기 모두가 동기화되지 않으면 동작을 보장하지 않는다.
  • volatile 한정자는 배타적 수행과는 상관없지만 항상 가장 최근의 기록된 값을 일게 됨을 보장한다.
private static volatile int nextSerialNumber = 0;

public static int generateSerialNumber() {
    return nextSerialNumber++;
}
  • 증가 연산자(++)는 원자적이지 않다.
  • 안전 실패(safety failure) -- 프로그램이 잘못된 결과를 계산 해내는 오류
  • 가변 데이터는 단일 스레드에서만 쓰도록 하자.

Item 79: 과도한 동기화는 피하라

  • 응답 불가와 안전 실패를 피하려면 동기화 메서드나 동기화 블록 안에서는 제어를 절대로 클라이언트에 양도하면 안 된다.
  • 외계인 메서드(alien method)가 하는 일에 따라 동기화된 영역은 예외를 일으키거나, 교착상태에 빠지거나, 데이터를 훼손할 수도 있다.
  • 자바 언어의 락은 재진입(reentrant)을 허용한다.
  • CopyOnWriteArrayList는 내부를 변경하는 작업은 항상 깨끗한 복사본을 만들어 수행하도록 구현했다.
  • 동기화 영역에서는 가능한 한 일을 적게 하는 것이다.

Item 80: 스레드보다는 실행자, 태스크, 스트림을 애용하라

  • 실행자 프레임워크에서는 작업 단위와 실행 메커니즘이 분리된다.
  • 작업 단위를 나타내는 핵심 추상 개념이 태스크다. (RunnableCallable)
  • 가벼운 서버라면 Executors.newCachedThreadPool
  • 무거운 서버라면 Executors.newFixedThreadPool

Item 81: waitnotify보다는 동시성 유틸리티를 애용하라

  • waitnotify는 올바르게 사용하기가 아주 까다로우니 고수준 동시성 유틸리티를 사용하자.
  • ConcurrentHashMapget 같은 검색 기능에 최적화 되었다.
  • Collections.synchronizedMap 보다는 ConcurrentHashMap을 사용하는 게 훨씬 좋다.
  • BlockingQueue는 작업 큐(생산자-소비자 큐)로 쓰기에 적합하다.
  • CountDownLatch는 일회성 장벽으로, 하나 이상의 스레드가 또 다른 하나 이상의 스레드 작업이 끝날 때까지 기다리게 한다.
  • 시간 간격을 잴 때는 항상 System.currentTimeMillis가 아닌 System.nanoTime을 사용하자.
    • System.nanoTime은 더 정확하고 정밀하며 시스템의 실시간 시계의 시간 보정에 영향받지 않는다.
  • CyclicBarrier는 여러 개의 CountDownLatch를 대체할 수 있다.
  • wait 메서드를 사용할 때는 반드시 대기 반복문(wait loop) 관용구를 사용하라. 반복문 밖에서는 절대로 호출하지 말자.

Item 82: 스레드 안전성 수준을 문서화하라

  • 메서드 선언에 synchronized 한정자를 선언할지는 구현 이슈일 뿐 API에 속하지 않는다.
  • 멀티스레드 환경에서도 API를 안전하게 사용하게 하려면 클래스가 지원하는 스레드 안전성 수준을 정확히 명시해야 한다.
    • 불변(immutable)
    • 무조건적 스레드 안전(unconditionally thread-safe)
    • 조건부 스레드 안전(conditionally thread-safe)
    • 스레드 안전하지 않음(not thread-safe)
    • 스레드 적대적(thread-hostile)
  • 서비스 거부 공격(denial-of-service attack)을 막으려면 synchronized 메서드 대신 비공개 락 객체를 사용해야 한다.

이펙티브 자바 3판 - 4. 클래스와 인터페이스(item25) ~ 5. 제네릭 (item26~31)

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

소스 파일 하나에 톱레벨 클래스를 여러개 선언해도 자바 컴파일러는 문제삼지 않으나, 경우에 따라 문제될 수 있다.
동일한 클래스명으로 각각 다른 파일에 중복 정의되어 있는 경우 컴파일이 실패하거나, 컴파일 순서에 따라 어떻게 동작할 지 예측 할 수 없다.

5장. 제네릭

ITEM 26 로(raw) 타입은 사용하지 말라

26-1. raw type이란?
클래스와 인터페이스 선언에 타입 매개변수(ex. )가 있으면 제네릭 클래스, 제네릭 인터페이스라고 함.
제네릭 타입을 정의할 때 타입 매개변수를 쓰지 않는 경우를 raw type이라한다.

List<E>은 제네릭 타입이며, 이의 raw type은 List임

26-2. raw type의 사용을 비추천한다.
오류는 컴파일 타임에 발견되는 것이 가장 이상적이다. raw type을 사용할 경우 unchecked 경고가 나오며
잘못된 타입을 add할 수도 있다.

// 코드 26-4 런타임에 실패한다. - unsafeAdd 메서드가 로 타입(List)을 사용 (156-157쪽)
public class Raw {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        unsafeAdd(strings, Integer.valueOf(42));
        String s = strings.get(0); // 컴파일러가 자동으로 형변환 코드를 넣어준다.
    }

    private static void unsafeAdd(List list, Object o) {
        list.add(o);
    }
}

제네릭 타입을 쓴다면 컴파일 단계에서 타입 불변, 안정성을 확보 할 수 있다.

26-3. raw type, 와일드 카드(), 의 차이 raw type 은 타입에 안전하지 않으나, 와일드카드는 안전하다.

private static void addObjectToList1(final List<?> aList, final Object o ) {
    aList.add(o);
} //컴파일 실패함. null만 add가능

private static void addObjectToList2(final List<Object> aList, final Object o ) {
    aList.add(o);
} //컴파일 실패함. List<Object>인데 main에서 List<String>을 인자로 넘기려함

private static <T> void addObjectToList3(final List<T> aList, final T o ) {
    aList.add(o);
} //정상적으로 컴파일되고 실행 가능함.


public static void main(String[] args) {
    List<String> testList = new ArrayList<String>();
    String s = "Add me!";
    addObjectToList1(testList, s);
    addObjectToList2(testList, s);
    addObjectToList3(testList, s);
}

26-4. raw type을 쓰는 예외 상황
class의 리터럴은 raw type으로 써야함. List.class는 되지만 List<String>.class는 허용되지 않는다.
instanceof 연산자는 런타임에서 타입을 비교한다. 제네릭 타입은 컴파일 단계에서 소거되므로 제네릭 타입으로 비교할 수 없다.

ITEM 27 비검사 경고를 제거하라

비검사 경고를 제거할 수록 타입 안정성이 높아진다. 만약 타입 안정성이 확실한데 컴파일러의 경고를 없애고 싶다면
@SuppressWarnings("unchecked")를 사용한다.
@SuppressWarnings("unchecked")의 범위는 최대한 줄여서 달아야 한다. 메소드 레벨, 클래스 레벨보다는
비검사 경고가 뜨는 지역변수 레벨에 다는 것이 가장 좋다. 또한 타입에 안전한 이유를 주석으로 추가해주자.

ITEM 28 배열보다는 리스트를 사용하라

배열은 공변(covariant)이다. class Sub extends Super라면 Sub[]는 Super[]의 하위 타입이다. 그러나 리스트는 불공변이다. List<Sub>List<Super>는 상하위 관계가 아니다.

Object[] objectArray = new Integer[1]; 
objectArray[0] = "Hello world"; // 런타임에 ArrayStoreException 발생 

List<Object> objectList = new ArrayList<Integer>; // 컴파일 실패 
objectList.add("Hello world"); // 위에서 이미 컴파일에 실패했으며, 타입이 달라 넣을 수도 없다

배열이나 리스트나 Integer용 저장소에 String을 넣을 순 없으나, 전자는 런타임에 실수를 알 수 있고 후자는 컴파일타임에 알 수 있다. 후자가 당연히 좋으니 배열보다는 리스트를 사용하는 것이좋다. 다만 성능적인 측면에선 배열이 앞설 수 있다.

ITEM 29 이왕이면 제네릭 타입으로 만들라

일반 클래스를 제네릭 클래스로 만드는 방법

  1. 클래스 선언에 타입 매개변수를 추가
  2. 일반 타입(ex. Object)를 타입 매개변수로 교체
  3. 비검사(unchecked) 경고 해결해주기

다음은 일반 클래스 Stack을 제네릭 클래스로 바꿔본 예제이다.

public class Stack { // => Stack<E>
    private Object[] elements; // => E[] elements; 
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16; 
    
    // => @SuppressWarnings("unchecked")
    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY]; // => (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) { // => push(E e)
        ensureCapacity();
        elements[size++] = e;
    }

    private void ensureCapacity() {
        if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1);
    }

    public Object pop() { // => E pop()
        if (size == 0) throw new EmptyStackException();
        Object result = elements[--size]; // => E result = elements[--size];
        elements[size] = null;
        return result;
    }
    ...
}

추가적으로 기본 타입은 제네릭 타입List<int>으로 쓸 수 없으며 박싱된 기본타입을 써야한다.

ITEM 30 이왕이면 제네릭 메서드로 만들라

클래스와 마찬가지로 메소드도 제네릭이 가능하다면 사용을 권장한다. 사용자 측에서 형변환하는 것보다 안전하고 유연해진다.

public static Set union(Set s1, Set s2) { // => <E> Set<E> union(Set<E> s1, Set<E> s2) 
    Set result = new HashSet(s1); // => Set<E> result = new HashSet<>(s1); 
    result.addAll(s2);
    return result;
}

제네릭 싱글톤 팩토리
불변 객체를 여러 타입으로 활용할 수 있게 만들어야하는데, 이때는 제네릭 싱글톤 팩토리를 만들면 된다.

@SuppressWarnings("unchecked")
public static <T> Comparator<T> reverseOrder() {
    return (Comparator<T>) ReverseComparator.REVERSE_ORDER;
}

만약 제네릭을 쓰지 않았다면 요청 타입마다 형변환하는 정적 팩토리를 만들어야했을 것이다.

재귀적 타입 한정
자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정할 수 있다.
다음과 같이 타입 매개변수를 한정적으로 기술해주는 방식이다. 이를 통해 모든 타입 E는 자신과 비교할 수 있다라는 것을 나타낸다. max 메소드의 리턴값은 Comparable<E>을 구현했으므로, 다른 E와 비교할 수 있다.
public static <E extends Comparable<E>> E max(Collection<E> c)

제네릭 타입과 마찬가지로 클리아언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다 제네릭 메서드가 더 안전하며 사용하기 쉽다.

ITEM 31 한정적 와일드카드를 사용해 API 유연성을 높이라

매개변수화 타입은 불공변이기 때문에 때로는 유연한 처리방식이 필요하다.

public class Stack<E> {
    public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
}

// 코드 31-1 와일드카드 타입을 사용하지 않은 pushAll 메서드 - 결함이 있다!
public void pushAll(Iterable<E> src) {
    for (E e : src)
        push(e); //stack의 매개변수타입은 Number인데 e가 intVal이라면 불공변이기 때문에 오류가난다.
}

 // 코드 31-2 E 생산자(producer) 매개변수에 와일드카드 타입 적용
public void pushAll(Iterable<? extends E> src) {
    for (E e : src)
        push(e);
}

// 코드 31-3 와일드카드 타입을 사용하지 않은 popAll 메서드 - 결함이 있다!
public void popAll(Collection<E> dst) {
    while (!isEmpty())
        dst.add(pop());
}

// 코드 31-4 E 소비자(consumer) 매개변수에 와일드카드 타입 적용
public void popAll(Collection<? super E> dst) {
    while (!isEmpty())
        dst.add(pop());
}

유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라.
PESC : produce-extends, consumer-super

###제네릭 활용 사례
지금까지 사용해왔던 object mapper는 class단위로만 desirialize를 해왔는데 nested object에 대한 desirialize를 어떻게 수행 할 수 있을까에 대한 고민

jackson라이브러리의 TypeReference로부터 데이터를 읽는 과정에 대한 소개


//desirialize하려는 매개변수 타입을 인자로 넘겨준다.
response = RedisHelper.inquireListResponse(redisKey, redisField, new TypeReference<ListResponse<WebArticle>>() {});

public static <T> ListResponse<T> inquireListResponse(String key, String feild, TypeReference<ListResponse<T>> typeReference) {
    String cache = hget(key, feild);
    if (cache != null) {
        try {
            return JsonUtil.jsonToNestedObject(cache, typeReference);
        } catch (Exception e) {
            log.error(e.getClass().getSimpleName() + "! redis inquireList error : " + e.getLocalizedMessage() + ", key=" + key, e);
        }
    }
    return null;
}

//매개변수 타입을 이용하는 jackson mapper.read를 호출한다.
public static <T> ListResponse<T> jsonToNestedObject(String json,  TypeReference<ListResponse<T>> typeReference) {
    ObjectMapper mapper = new ObjectMapper();
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
    try {
        return mapper.readValue(json, typeReference);
    } catch (IOException e) {
        log.error(e.getClass().getSimpleName() + ": jsonToObject error : " + e.getLocalizedMessage());
    }
    return null;
}

//타입팩토리의 constructType메소드를 통해 바인딩된 JavaType을 가져오고 해당 타입을 통해 문서를 desirialize한다.
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T> T readValue(String content, TypeReference valueTypeRef) throws IOException, JsonParseException, JsonMappingException
{
    return (T) _readMapAndClose(_jsonFactory.createParser(content), _typeFactory.constructType(valueTypeRef));
}

이펙티브 자바 3판 - 6. 열거 타입과 애너테이션 (item37~41) ~ 7. 람다와 스트림(item42 ~ 43)

6. 열거 타입과 애너테이션 (item37~41)

Item37. ordinal 인덱싱 대신 EnumMap을 사용하라.

예제를 통해 살펴보자

  • Plant 클래스 정의
class Plant {
   enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }

   final String name;
   final LifeCycle lifeCycle;

   Plant(String name, LifeCycle lifeCycle) {
       this.name = name;
       this.lifeCycle = lifeCycle;
   }

   @Override public String toString() {
       return name;
   }
}
  • ordinal()을 배열 인덱스로 사용하는 예제의 문제점
    • 배열은 제네릭과 호환되지 않는 문제(비검사 형변환을 수행해야 하고 깔끔히 컴파일 되지 않음)
    • 정확한 정숫값을 사용한다는 것을 직접 보증해야 함.(열거타입과 달리 안전하지 않음)
      • 잘못된 정숫값을 사용하는 경우, 잘못된 동작을 수행하거나 ArrayIndexOutOfBoundException 발생할 수 있음)
    • 코드 자체도 장황하여 저런식으로는 개발하지 않을듯.
        // 코드 37-1 ordinal()을 배열 인덱스로 사용 - 따라 하지 말 것! (226쪽)
        Set<Plant>[] plantsByLifeCycleArr = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
        for (int i = 0; i < plantsByLifeCycleArr.length; i++) {
            plantsByLifeCycleArr[i] = new HashSet<>();
        }
            
        for (Plant p : garden) {
            plantsByLifeCycleArr[p.lifeCycle.ordinal()].add(p);   
        }
            
        // 결과 출력
        for (int i = 0; i < plantsByLifeCycleArr.length; i++) {
            System.out.printf("%s: %s%n", Plant.LifeCycle.values()[i], plantsByLifeCycleArr[i]);
        }
  • EnumMap을 사용한 매핑
    • 더 짧고 명료하고 안전하고 성능도 원래 버전과 비등함.
    • 배열 인덱스를 계산하는 과정에서 오류가 날 가능성도 원천봉쇄
    • EnumMap의 성능이 ordinal을 쓴 배열에 비견되는 이유는 그 내부에서 배열을 사용하기 때문
// 코드 37-2 EnumMap을 사용해 데이터와 열거 타입을 매핑한다. (227쪽)
        Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(Plant.LifeCycle.class);
        for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {
            plantsByLifeCycle.put(lc, new HashSet<>());
        }
        
        for (Plant p : garden) {
            plantsByLifeCycle.get(p.lifeCycle).add(p);
        }
            
        System.out.println(plantsByLifeCycle);
  • EnumMap 코드 일부
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
    implements java.io.Serializable, Cloneable
{
     /**
     * Array representation of this map.  The ith element is the value
     * to which universe[i] is currently mapped, or null if it isn't
     * mapped to anything, or NULL if it's mapped to null.
     */
    private transient Object[] vals;

     /**     
     * Creates an empty enum map with the specified key type.
     *
     * @param keyType the class object of the key type for this enum map
     * @throws NullPointerException if {@code keyType} is null
     */
    public EnumMap(Class<K> keyType) {
        this.keyType = keyType;
        keyUniverse = getKeyUniverse(keyType);
        vals = new Object[keyUniverse.length];
    }
}

EnumMap

  • EnumMap 생성자가 받는 키 타입의 Class 객체는 한정적 타입 토근으로 런타임 제네릭 타입 정보를 제공
  • 스트림 사용시 주의사항
    • 단순히 groupingBy에 람다만 사용하는 경우 EnumMap을 사용하지 않고 HashMap으로 사용될 수 있어서 MapFactory를 통해 Map구현체를 명시해주어야 함.
    • 명시하지 않으면 공간과 성능 이점이 사라질 수 있다.
    • 이왕이면 최적화를 하는 것이 좋은 것 같음.
// 코드 37-3 스트림을 사용한 코드 1 - EnumMap을 사용하지 않는다! (228쪽)
        System.out.println(Arrays.stream(garden)
                .collect(groupingBy(p -> p.lifeCycle))
        );

 // 코드 37-4 스트림을 사용한 코드 2 - EnumMap을 이용해 데이터와 열거 타입을 매핑했다. (228쪽)
        System.out.println(Arrays.stream(garden)
                .collect(groupingBy(p -> p.lifeCycle, () -> new EnumMap<>(LifeCycle.class), toSet()))
        );

Phase, Transition 예제

public enum Phase {
    SOLID, LIQUID, GAS;
   private static final Transition[][] = TRANSITIONS = {
       { null, MELT, SUBLINE },
       { FREEZE, null, BOIL },
       { DEPOSIT, CONDENSE, null }
   };

    public static Transition from(Phase from, Phase to) {
           return TRANSITIONS[from.ordinal()][to.ordinal()];
    }
  • ordinal을 이용한 맵핑의 문제

    • 컴파일러는 ordinal과 배열 인덱스의 관계를 알기가 어려움.
    • 열거타입을 수정하면서 연관된 열거타입을 함꼐 수정하지 않거나 실수로 잘못 수정하면 런타임 오류가 발생할 수 있음.
    • Enum Type이 추가되면 관리해야 하는 [][]의 크기가 제곱으로 커지고 null로 채워지는 칸도 늘어남.
  • EnumMap을 이용한 방식 적용

    • 새로운 상태가 추가되었을때 Phase, Transition에만 정의를 추가하면 된다.
public enum Phase {
    SOLID, LIQUID, GAS;
    public enum Transition {
        MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
        BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
        SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);

// 상전이 맵을 초기화한다.
        private static final Map<Phase, Map<Phase, Transition>> m = Stream.of(values())
                .collect(groupingBy(t -> t.from,
                        () -> new EnumMap<>(Phase.class),
                        // 이후 상태를 전이에 대응시키는 EnumMap 생성
                        toMap(t -> t.to,
                                t -> t,
                                (x, y) -> y,
                                () -> new EnumMap<>(Phase.class))
                        )
                );
        
        public static Transition from(Phase from, Phase to) {
            return m.get(from).get(to);
        }

핵심 정리

  • 열거타입에서 배열의 인덱스를 얻기 위해 ordinal을 쓰는 것은 일반적으로 좋지 않으니 대신 EnumMap을 사용하라.
  • 다차원 관계는 EnumMap<..., EnumMap<...>>으로 표현하라.
  • Enum.ordinal은 사용하지 말아야 한다.

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

타입 안전 열거 패턴(typesafe enum patttern)

  • 열거타입이 이 책 초판에서 소개한 타입 안전 열거 패턴보다 우수함.
  • 다만, 타입 안전 열거 패턴은 열거한 값들을 그대로 가져온 다음 값을 더 추가하여 다른 목적으로 쓸수 있지만, 열거타입은 그렇게 할 수 없다.

열거타입의 확장

  • 대부분 상황에서 열거타입을 확장하는 것은 좋지 않은 생각임.
  • 확장한 타입의 원소는 기반 타입의 원소로 취급하지만 그 반대는 성립하지 않는다면 이상함.
    • 기반 타입과 확장된 타입들의 원소 모두를 순회할 방법이 마땅치 않음.

열거타입의 확장이 어울리는 쓰임

  • 연산 코드(opration code)
  • Operation 인터페이스 정의
// 코드 38-1 인터페이스를 이용해 확장 가능 열거 타입을 흉내 냈다. (232쪽)
public interface Operation {
    double apply(double x, double y);
}
  • BasicOperation Enum 정의
// 코드 38-1 인터페이스를 이용해 확장 가능 열거 타입을 흉내 냈다. - 기본 구현 (233쪽)
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;
    }
}
  • 여기서 BasicOperation은 확장할 수 없지만 Operation interface를 확장할 수 있음.

    • 이 인터페이스를 연산의 타입으로 사용하면 됨.
  • ExtendedOperation Enum 정의

public enum ExtendedOperation implements Operation {
    EXP("^") {
        public double apply(double x, double y) {
            return Math.pow(x, y);
        }
    },
    REMAINDER("%") {
        public double apply(double x, double y) {
            return x % y;
        }
    };
    private final String symbol;
    ExtendedOperation(String symbol) {
        this.symbol = symbol;
    }
    @Override public String toString() {
        return symbol;
    }
}
  • 실행
public static void main(String[] args) {
        double x = 1;
        double y = 2;
        test(ExtendedOperation.class, x, y);
        test(BasicOperation.class, x, y);
    }
    private static <T extends Enum<T> & Operation> void test(Class<T> opEnumType, double x, double y) {
        for (Operation op : opEnumType.getEnumConstants()) {
            System.out.printf("%f %s %f = %f%n",x, op, y, op.apply(x, y));
        }
    }

// 주로 개발하는 형태로 만든다면
        List<Operation> operationList = new ArrayList<>();
        operationList.addAll(Arrays.asList(ExtendedOperation.values()));
        operationList.addAll(Arrays.asList(BasicOperation.values()));

        for (Operation op : operationList) {
            System.out.printf("%f %s %f = %f%n",x, op, y, op.apply(x, y));
        }


/* 실행결과
1.000000 ^ 2.000000 = 1.000000
1.000000 % 2.000000 = 1.000000
1.000000 + 2.000000 = 3.000000
1.000000 - 2.000000 = -1.000000
1.000000 * 2.000000 = 2.000000
1.000000 / 2.000000 = 0.500000
*/

인터페이스를 확장한 방식으로 했을떄의 문제

  • Operation은 인터페이스이기 떄문에 EnumSet, EnumMap 등을 사용할 수 없음.
  • 각 열거타입끼리 구현을 상속할 수 없는 점

핵심 정리

  • 열거 타입 자체는 확장할 수 없지만, 인터페이스와 그 인터페이스를 구현하는 기본 열거 타입을 함께 사용해 같은 효과를 낼 수 있음.
  • 인터페이스 기반으로 작성되었다면 기본 열거 타입의 인스턴스가 쓰이는 모든 곳을 새로 확장한 열거 타입의 인스턴스로 대체해 사용할 수 있음.

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

과거의 잔재 명명규칙

  • JUnit 버전 3까지 테스트 메서드 이름을 test로 prefix여야만 했음.

명명규칙의 단점

  • 1 오타가 나면 안됨.
  • 2 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없음.
    • TestSafetyMechanisms과 같은 클래스 이름(method 이름이 아닌)을 해서 처리하려는 경우
  • 3 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.
    • 특정 Exception던지는 것이 성공인 경우

애너테이션을 이용한 해결책

  • JUnit4의 Test 애너테이션 정릐
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Test {

    /**
     * Default empty exception
     */
    static class None extends Throwable {
        private static final long serialVersionUID = 1L;

        private None() {
        }
    }

    /**
     * Optionally specify <code>expected</code>, a Throwable, to cause a test method to succeed if
     * and only if an exception of the specified class is thrown by the method. If the Throwable's
     * message or one of its properties should be verified, the
     * {@link org.junit.rules.ExpectedException ExpectedException} rule can be used instead.
     */
    Class<? extends Throwable> expected() default None.class;

    /**
     * Optionally specify <code>timeout</code> in milliseconds to cause a test method to fail if it
     * takes longer than that number of milliseconds.
     * <p>
     * <b>THREAD SAFETY WARNING:</b> Test methods with a timeout parameter are run in a thread other than the
     * thread which runs the fixture's @Before and @After methods. This may yield different behavior for
     * code that is not thread safe when compared to the same test method without a timeout parameter.
     * <b>Consider using the {@link org.junit.rules.Timeout} rule instead</b>, which ensures a test method is run on the
     * same thread as the fixture's @Before and @After methods.
     * </p>
     */
    long timeout() default 0L;
}
  • 수행 로직은 책에 있는 코드보다 Junit의 Runner쪽을 보는게 실속 있을 것 같아서 생략합니다.

@repeatable 메타애너테이션

  • 자바8에서 여러 개의 값을 받는 애너테이션을 다른 방식으로도 만들 수 있음.
  • 배열 매개변수를 사용하는 대신 @repeatable 메타애너테이션을 다는 방식
  • @repeatable 사용시 주의 사항
    • 1 @repeatable을 단 애너테이션을 반환하는 '컨테이너 애너테이션'을 하나 더 정의하고, @repeatable에 이 컨에티어 애너테이션의 class 객체를 매개변수로 전달해야 함.
    • 2 컨테이너 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 vaue 메서드를 정의.
    • 3 적절한 보존 정책(@retention), 대상(@target)을 명시해야 함.
  • 반복 가능 애너테이션을 여러 개 달면 하나만 달았을 때와 구분하기 위해 해당 '컨테이너' 애너테이션 타입이 적용된다.
// 코드 39-8 반복 가능한 애너테이션 타입 (243-244쪽)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
    Class<? extends Throwable> value();
}

// 반복 가능한 애너테이션의 컨테이너 애너테이션 (244쪽)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
    ExceptionTest[] value();
}

// 코드 39-9 반복 가능 애너테이션을 두 번 단 코드 (244쪽)
    @ExceptionTest(IndexOutOfBoundsException.class)
    @ExceptionTest(NullPointerException.class)
    public static void doublyBad() {
        List<String> list = new ArrayList<>();

        // 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나
        // NullPointerException을 던질 수 있다.
        list.addAll(5, null);
    }

애너테이션 vs 명명 패턴

  • 애너테이션으로 할 수 있는 일을 명명 턴으로 처리할 이유는 없다.
  • 자바 프로그래머라면 예외 없이 자바가 제공하는 애너테이션 타입들을 사용해야 한다.

참조 링크

Item40 @OverRide 애너테이션을 일관되게 사용하라.

@OverRide 애너테이션

  • 기본 제공 애너테이션 중 가장 중요한 것
  • (다들아시겠지만) 상위 타입의 메서드를 재정의했음을 뜻함.
  • 일관되게 사용하면 여러 가지 악명 높은 버그들을 예방(IDE의 강력한 기능으로 문제된적은 아직까지 없음)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

method overloading 예제

  • @OverRide 어노테이션을 사용하면 컴팡ㄹ 단계에서 에러를 알 수 있음.
// equals 메소드는 매배견수가 Object임.
@Override public boolean equals(Bigram b) {
   return ..
}

@Override public boolean equals(Object o) {
   return ..
}
  • 상위 클래스의 메서드를 재정의하려는 모든 메서드에 @OverRide 애너테이션을 꼭 달자.

예외적인 케이스

  • 구체 클래스에서 상위 클래스의 추상 메소드를 재정의 하는 경우에는 굳이 @OverRide를 달지 않아도 됨.(IDE를 사용하면 자동으로 달아줌.)
  • 인터페이스의 default method를 재정의하는 경우에 검사하지 않는데 이떄 @OverRide를 임의로 달아주는것이 좋음.(달지 않아도 컴파일 에러 없음)

Set 인터페이스와 Collection 인터페이스 관련

  • Set이 Collection을 확장했지만 새로 추가한 메소드는 없고, 모든 메서드 선언에 @OverRide를 달아 실수로 추가한 메서드가 없음을 보장함.
    • 디컴파일 결과물에는 딱히 되어있지않은 것 같습니다...ㅋㅋㅋ

핵심 정리

  • 재정의한 모든 메서드에 @OverRide 애너테이션을 의식적으로 달면 실수했을 때 컴파일러가 알려줄 수 있음.
  • 구체 클래스에서 상위 클래스의 추상 메서드를 재정의한 경우엔 이 애너테이션을 달지 않아도 된다.(보통은 IDE가 달아줌)

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

마커 인터페이스(maker interface)

  • 아무 메서드도 담고 있지 않고, 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 인터페이스
  • 예시 : Serializable
    • Serializable은 자신을 구현한 클래스의 인스턴스는 ObjectOutputStream을 통해 쓸(Write)수 있다고, 즉 직렬화(serialization)할수 있다고 알려주는 역할을 함.
 * @author  unascribed
 * @see java.io.ObjectOutputStream
 * @see java.io.ObjectInputStream
 * @see java.io.ObjectOutput
 * @see java.io.ObjectInput
 * @see java.io.Externalizable
 * @since   1.1
 */
public interface Serializable {
}

마커 인터페이스가 마커 애너테이션보다 좋은 이유 ( 마커 인터페이스 vs 마커 애너테이션 )

  • 1 마커 인터페이스는 이를 구현한 클래스의 인스턴스들을 구분하는 타입으로 쓸수 있으나, 마커 애너테이션은 그렇지 않음.
    • 인터페이스는 어엿한 타입이라서 컴파일타임에 오류를 발견할 수 있음.
    • ObjectOutputStream.writeObject 메소드는 조금 예외적임
      • Object를 Arguments로 받기 때문에 직렬화할 수 없는 객체(Serializable을 구현하지 않은 객체)를 넘겨도 런타임에야 문제를 확인할 수 있음)
public final void writeObject(Object obj) throws IOException {
        if (enableOverride) {
            writeObjectOverride(obj);
            return;
        }
        try {
            writeObject0(obj, false);
        } catch (IOException ex) {
            if (depth == 0) {
                writeFatalException(ex);
            }
            throw ex;
        }
    }
  • 2 적용 대상을 더 정밀하게 지정할 수 있음.
    • 마커 애너테이션의 경우 적용 대상(@target)을 ElementType.TYPE - 모든 타입(클래스, 인터페이스, 열거타입, 애너테이션)에만 달수 있음.
    • 특정 인터페이스를 구현한 클래스에만 적용하고 싶은 마커가 있다고 했을때 애너테이션으로는 제한적임

마커 애너테이션이 마커 인터페이스보다 나은 점

  • 거대한 애너테이션 시스템의 지원을 받을 수 있다.
    • 애너테이션을 적극 활용하는 프레임워크에서는 이를 사용하는 것이 일관성을 지키는 데 유리할 수도 있음.

언제 마커 인터페이스를 쓰고, 언제 마커 애너테이션을 사용해야 하는가?

  • 클래스와 인터페이스 외의 프로그램 요소(모듈, 패키지, 필드, 지역면수 등)에 마킹해야 할때 애너테이션을 쓸수밖에 없음.
  • "마킹이 될 객체가 매개변수로 받는 메소드를 작성할 일이 있을까?" 했을떄 Yes이면 => 마커 인터페이스를 써야 함.
  • 이런 메소드를 작성할 일이 없다면 애너테이션이 나은 선택

핵심 정리

  • 마커 인터페이스와 마커 애너테이션은 각자의 쓰임이 있다.
  • 새로 추가하는 메서드 없이 타입 정의가 목적이라면 마커 인터페이스르 선택하자.
  • 적용 대상이 ElementType.TYPE인 마커 애너테이션을 작성하고 있다면, 잠시 여유를 갖고 정말 애너테이션으로 구현하는게 옳은지, 혹은 마커 인터페이스가 낫지는 않을지 곰곰히 생각해보자.

7. 람다와 스트림(item42 ~ 43)

  • 자바8에서 함수형 인터페이스, 람다, 메서드 참조라는 개념이 추가되면서 스트림 API도 추가됨. 이러한 기능들을 효과적으로 사용하는 방법에 대해 알아보자.

Item42. 익명 클래스보다는 람다를 사용하라

과거 함수 타입을 표현하는 방식 - 함수 객체(function object)

  • 추상 메서드를 하나만 담은 인터페이스(드믈게는 추상클래스) 사용
  • 함수 객체를 만드는 주요 수단은 익명 클래스
// 코드 42-1 익명 클래스의 인스턴스를 함수 객체로 사용 - 낡은 기법이다! (254쪽)
        Collections.sort(words, new Comparator<String>() {
            public int compare(String s1, String s2) {
                return Integer.compare(s1.length(), s2.length());
            }
        });
  • 과거에는 위와 같은 전략패턴이면 충분했음.

자바8 람다식(lambda expression)

  • 추상 메서드 하나 짜리 인터페이스는 특별한 의미리르 인정받아 특별한 대우를 받게 됨.
  • 함수형 인터페이스라 부르는 이 인터페이스들의 인스턴스를 람다식을 사용해 만들 수 있게 됨.
  • 람다는 함수나 익명 클래스와 개념은 비슷하지만 코드는 훨씬 간결해짐
// 코드 42-2 람다식을 함수 객체로 사용 - 익명 클래스 대체 (255쪽)
Collections.sort(words,
                (s1, s2) -> Integer.compare(s1.length(), s2.length()));

람다식 특징

  • 매개변수 타입과 반환값에 대한 언급이 생략됨.
  • 컴파일러가 문맥을 살펴 타입 추론을 해줌.
    • 상황에 따라 컴파일러가 타입을 결정하지 못하는 경우 직접 명시해야 함.
  • 타입을 명시해야 코드가 더 명확할 때만 제외하고는, 람다의 모든 매개변수 타입은 생략하자.

타입 추론

  • 제네릭의 로 타입 사용 금지, 제네릭을 사용하라, 제네릭 메서드를 사용하라 등 조언들은 람다와 함께 사용할 때 두 배로 중요해짐.
  • 컴파일러가 타입 추론을 하는데 필요한 타입 정보 대부분을 제네릭으로부터 얻음.
  • 우리가 이 정보를 제공하지 않으면 컴파일러는 람다의 타입을 추론할 수 없게 되어 일일이 명시해 줘야 함.
  • List이 아니라 List와 같은 로타입이었다면 컴파일 오류가 발생했을 것.
  • 자바 타입 추론 참고 : https://futurecreator.github.io/2018/07/20/java-lambda-type-inference-functional-interface/

더 간결한 처리 방식

  • Comparator static factory method, 메서드 참조를 사용
Collections.sort(words, Comparator.comparingInt(String::length));
  • 자바8에 추가된 List 인터페이스의 sort 메서드 사용
words.sort(Comparator.comparingInt(String::length));

람다의 활용 예제

  • FunctionalInterface을 사용하여 Enum의 abstract method를 재정의하는 방식을 더 쉽고 간결하게 구현할 수 있음.
  • Enum의 abstract method를 재정의하는 방식
public enum 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;
    public abstract double apply(double x, double y);
}
  • FunctionalInterface를 사용한 방식
public enum Operation {
    PLUS  ("+", (x, y) -> x + y),
    MINUS ("-", (x, y) -> x - y),
    TIMES ("*", (x, y) -> x * y),
    DIVIDE("/", (x, y) -> x / y);

    private final String symbol;
    private final DoubleBinaryOperator op;

    Operation(String symbol, DoubleBinaryOperator op) {
        this.symbol = symbol;
        this.op = op;
    }
  • DoubleBinaryOperator와 같은 Functional Interface는 이후 Item에서 설명이 나오므로 생략.

람다 사용시 주의 사항

  • 람다는 이름이 없고 문서화도 못한다. 따라서 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄수가 많아지면 람다를 사용해서는 안 된다.
  • 람다는 한 줄일 떄 가장 좋고, 아무리 길어도 세 줄 안에 끝내는게 좋음.
  • 열거타입 생성자 안의 람다를 사용할 때
    • 인스턴스가 런타임에 만들어지기 떄문에 멤버에 접근할 수 없음.
    • 상수별 동작을 단 몇줄로 구현하기 어렵거나, 인스턴스 필드나 메서드를 사용해야만 하는 상황이라면 상수별 클래스 몸체를 사용해야 함.
  • 람다를 직렬화하는 일은 극히 삼가야 한다.(익명 클래스도 마찬가지이기는 함)
    • 직렬화 형태태가 구현별로(가령 가상머신별로) 다를 수 있음.

익명 클래스를 람다로 대체할 수 없는 케이스

  • 람다의 시대가 열리면서 익명클래스를 쓸일이 거의 없게 됨.
  • 추상 클래스의 인스턴스를 만들 때는 람다를 사용할 수 없어 익명 클래스를 써야 함.(new 키워드를 통해 만드는 경우)
    • 추상 메서드가 여러 개인 인터페이스의 인스턴스를 만드는 경우에도 동일함.

익명클래스와 람다의 차이점

  • 람다는 자기 자신을 참조할 수 없음.
    • 람다 내에서 this는 자신의 바깥 인스턴스를 가리킨다.
  • 익명클래스는 자기 자신을 참조할 수 있음(this == 자기 자신)

핵심 정리

  • 작은 함수 객체를 구현하는 데 적합한 람다 도입
  • 익명 클래스는(함수형 인터페이스가 아닌) 타입의 인스턴스를 만들때만 사용하라

Item43. 람다보다는 메서드 참조를 사용하라.

메서드 참조 > 람다 > 익명 클래스

  • 람다가 익명 클래스보다 나은 점 중 가장 큰 특징은 간결함
    • 그러나 메서드 참조(method reference)가 더욱 간결함.

Map.merge

  • 자바8에 추가된 메소드
    • 키, 값, 함수를 인자로 받고, 주어진 키가 맵 안에 아직 없다면 주어진 {키, 값} 쌍을 그대로 저장
    • 반대로 키가 이미 있다면 세번째 인수로 받은 함수를 현재 값과 주어진 값에 적용한 뒤 결과를 현재 값에 덮어쓴다.
  • 키가 맵 안에 없다면 키와 숫자 1을 매핑, 이미 있다면 기존 매핑값을 증가시키는 코드 예제
    • 깔끔해보이지만 함수 인자가 거슬림.
map.merge(key, 1, (count, incr) -> count + incr);
  • Integer 클래스의 sum 함수의 메서드 참조 활용
map.merge(key, 1, Integer::sum);

    /**
     * Adds two integers together as per the + operator.
     *
     * @param a the first operand
     * @param b the second operand
     * @return the sum of {@code a} and {@code b}
     * @see java.util.function.BinaryOperator
     * @since 1.8
     */
    public static int sum(int a, int b) {
        return a + b;
    }
  • 어떤 람다에서는 매개변수의 이름 자체가 가독성에 도움을 줄수도 있음.
    • 이런 람다는 길이는 더 길어도 메서드 참조보다 유지보수하기 쉬울 수 있음.

람다 vs 메서드 참조

  • 람다로 할 수 없는 일이라면 메서드 참조로도 할 수 없다.
  • 람다로 구현했을 때 너무 길거나 복잡하다면 메서드 참조가 좋은 대안
  • 메서드 참조에 기능을 잘 드러내는 이름을 지어줄수 있고, 친절한 설명을 문서로 남길 수도 있음.(메소드 주석 등)
  • 항상 메서드 참조가 명확한 것은 아닐수도 있음.
    • 아래같은 케이스라면 람다가 나을수도 있음.
service.execute(GoshThisClassNameIsHumongous::action);

service.execute(() -> action());
  • 예외 케이스(람다로는 불가능하지만, 메소드 참조로는 가능한 것) - 제네릭 함수 타입(generic function type) 구현
    • 함수형 인터페이스를 위한 제네릭 함수 타입은 메서드 참조 표현식으로 구현 가능하지만, 람다로는 불가(제네릭 람다식이라는 문법이 존재하지 않기 때문)
interface G1 {
    <E extends Exception> Object m() throws E;
}

interface G2 {
    <F extends Exception> String m() throws Exception;
}

interface G extends G1, G2 {}

// 인터페이스 G를 함수 타입으로 표현하면
<F extends Exception> () -> String throws F

메소드 참조 유형 5가지

  • 1 정적 메서드를 가리키는 메소드 참조
service.execute(GoshThisClassNameIsHumongous::action);
Integer::parseInt
str -> Interge.parseInt(str)
  • 2 수신 객체(receiving object; 참조 대상 인스턴스)를 특정하는 한정적(bound) 인스턴스 메소드 참조
Instant.now()::isAfter

Instant than = Instant.now();
t -> then.isAfter(t)
  • 3 수신 객체를 특정하지 않는 비한정적(unbound) 인스턴스 메소드 참조
String::toLowerCase

str -> str.toLowerCase()
  • 4 클래스 생성자
TreeMap<K,V>::new

() -> new TreeMap<K,V>()
  • 5 배열 생성자
int[]::new

len -> new int[len]

핵심 정리

  • 메서드 참조 쪽이 짧고 명확하다면 메소드 참조를 쓰고, 그렇지 않을때만 람다를 사용하라.

이펙티브 자바 3판 - 5. 제네릭 (item32~33) ~ 6. 열거 타입과 애너테이션 (item34~36)

Item 32: 제네릭과 가변인수를 함께 쓸 때는 신중하라

제네릭과 가변인수를 함께 쓰면 안전하지 않은 이유

  • 가변인수의 목적은 클라이언트 측에서 가변 수의 인수를 전달할 수 있도록 하는 것. 하지만 구현 방식의 허점(leaky abstraction)이 있다.
    • varargs 메서드를 호출하면 가변인수를 담는 배열이 만들어지고, 클라이언트에 노출됨. 그 결과 varargs 매개변수에 제네릭이나 매개변수화 타입이 포함되면 혼란스러운 컴파일 경고 발생
  • 메서드에 varargs를 실체화 불가(non-reifiable) 타입으로 선언하면 컴파일 경고 발생
    • 실체화 불가 타입으로 추론되는 varargs 메서드 호출에 대해서도 컴파일 경고 발생
warning: [unchecked] Possible heap pollution from
    parameterized vararg type List<String>
  • 힙 오염(heap pollution)은 제네릭 varargs 매개변수가 타입이 다른 객체 참조시 발생
    • 컴파일러가 생성한 형변환의 실패로 제네릭 시스템의 타입 안전성이 위반
// 제네릭과 varargs를 혼용하면 타입 안전성이 깨진다!
// signature ([Ljava/util/List<Ljava/lang/String;>;)V
// declaration: void dangerous(java.util.List<java.lang.String>[])
static void dangerous(List<String>... stringLists) {
    List<Integer> intList = List.of(42);
    Object[] objects = stringLists;
    // 제네릭 varargs 매개변수가 타입이 다른 객체 참조시 힙 오염(heap pollution) 발생
    objects[0] = intList;
    // ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    // INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object; (itf)
    // CHECKCAST java/lang/String
    String s = stringLists[0].get(0);
}

public static void main(String[] args) {
    // INVOKESTATIC Dangerous.dangerous ([Ljava/util/List;)V
    dangerous(List.of("There be dragons!"));
}
  • 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 위험
    • dangerous 메서드에 형변환 코드는 없지만 ClassCastException 발생

@SafeVarargs

  • 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 메서드는 매우 유용함
    • 자바 라이브러리는 이러한 메서드를 여럿 제공 - 타입 안전함
      • Arrays.asList(T... a)
      • Collections.addAll(Collection<? super T> c, T... elements)
      • Enums.of(E first, E... rest)
  • 자바 7 전에는 제네릭 가변인수 메서드 작성자가 호출자 쪽에서 발생하는 경고에 대해 해줄 수 있는 일이 없었음
    • 자바 7에서는 @SafeVarargs 애너테이션이 추가되어 클라이언트 측에서 발생하는 경고를 숨길 수 있음
    • @SafeVarargs는 작성자가 타입 안전을 보증하는 것
  • 다음의 경우 @SafeVarargs 을 달 수 있다.
    • 메서드가 배열에 아무것도 저장하지 않는 경우
    • 메서드가 배열의 참조를 외부로 노출하지 않는 경우
    • 즉, varargs 매개변수 배열이 메서드에 가변 수의 인수를 전달하는 용도로만 사용하는 경우 - varargs의 본래 목적
  • @SafeVarargs는 오버라이드 할 수 없는 메서드에서만 유효함. 오버라이드 가능한 메서드가 안전하다는 것을 보장 할 수 없기 때문
    • 자바 8에서는 정적 메서드와 final 메서드에서만 유효
    • 자바 9에서는 private 인스턴스 메서드에서도 유효

제네릭 varargs 매개변수의 참조를 공개하는 것은 위험

// 자신의 제네릭 매개변수 배열의 참조를 노출한다. - 안전하지 않다!
// signature <T:Ljava/lang/Object;>([TT;)[TT;
// declaration: T[] toArray<T>(T[])
static <T> T[] toArray(T... args) {
    return args;
}

// signature <T:Ljava/lang/Object;>(TT;TT;TT;)[TT;
// declaration: T[] pickTwo<T>(T, T, T)
static <T> T[] pickTwo(T a, T b, T c) {
    switch(ThreadLocalRandom.current().nextInt(3)) {
        // INVOKESTATIC PickTwo.toArray ([Ljava/lang/Object;)[Ljava/lang/Object;
        case 0: return toArray(a, b);
        case 1: return toArray(a, c);
        case 2: return toArray(b, c);
    }
    throw new AssertionError(); // 도달할 수 없다.
}
  • pickTow 메서드는 제네릭 가변인수를 받는 toArray 를 호출한다는 점만 빼면 위험하지 않고 경고도 내지 않음
    • 컴파일시에 컴파일러는 pickTwo로 전달되는 객체 타입에 상관 없이 Object[] 타입의 varargs 매개변수 배열을 만드는 코드를 생성
    • toArray 메서드는 단순히 배열을 pickTwo에 반환하고 다시 호출한 클라이언트까지 반환되므로 pickTwo는 항상 Object[] 타입을 반환
public static void main(String[] args) {
    // ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
    // INVOKESTATIC PickTwo.pickTwo (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)[Ljava/lang/Object;
    // CHECKCAST [Ljava/lang/String;
    String[] attributes = pickTwo("좋은", "빠른", "저렴한");
}
  • pickTwo 호출시 형변환 코드는 없지만 ClassCastException 발생
    • Object[]String[] 하위 타입이 아니기 때문
  • 제네릭 varargs 매개변수 배열에 다른 메서드가 접근하도록 허용하는 것은 안전하지 않다.
    • 예외1: @SafeVarargs로 제대로 애노테이트된 또 다른 varargs 메서드에 넘기는 것은 안전함
    • 예외2: 배열 내용의 일부 함수를 호출만 하는 일반 메서드에 넘기는 것도 안전 ???
// 제네릭 varargs 매개변수를 안전하게 사용하는 메서드
@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
    List<T> result = new ArrayList<>();
    for (List<? extends T> list : lists)
        result.addAll(list);
    return result;
}

@SafeVarargs의 대안

// 제네릭 varargs 매개변수를 List로 대체한 예 - 타입 안전하다.
static <T> List<T> flatten(List<List<? extends T>> lists) {
    List<T> result = new ArrayList<>();
    for (List<? extends T> list : lists)
        result.addAll(list);
    return result;
}
  • varargs를 List로 변경하면 메서드가 타입 안전함을 컴파일러에 의해 증명됨
  • 단점은 클라이언트 코드가 좀 더 장황하고 조금 느려질 수 있음

Item 33: 타입 안전 이종 컨테이너를 고려하라

개요

  • Set<E>, Map<K, V>, ThreadLocal<T>, AtomicReference<T> 등은 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한됨. 일반적인 용도에 맞게 설계된 것이니 문제되지 않음
  • 하지만 더 유용한 수단이 필요함. 예를 들어 데이터베이스의 행은 임의 개수의 열을 가질 수 있음. 모든 열에 타입 안전하게 접근 할 수 있다면 좋겠다
    • 해법은 컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공
    • 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장

예제

  • 타입별로 즐겨 찾는 인스턴스를 저장하고 검색할 수 있는 Favorites 클래스를 생각해보자
    • class의 클래스가 제네릭이기 때문에 각 타입의 Class 객체를 매개변수화한 키 역할로 사용할 수 있음
      • 컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고받는 class 리터럴을 타입 토큰(type token)이라 함
public class Favorites {
    public <T> void putFavorite(Class<T> type, T instance);
    public <T> T getFavorite(Class<T> type);
}

// 타입 안전 이종 컨테이너 패턴 - 클라이언트
public static void main(String[] args) {
    Favorites f = new Favorites();

    f.putFavorite(String.class, "Java");
    f.putFavorite(Integer.class, 0xcafebabe);
    f.putFavorite(Class.class, Favorites.class);

    String favoriteString = f.getFavorite(String.class);
    int favoriteInteger = f.getFavorite(Integer.class);
    Class<?> favoriteClass = f.getFavorite(Class.class);

    System.out.printf("%s %x %s%n", favoriteString, favoriteInteger, favoriteClass.getName());
}
  • Favorites 인스턴스는 타입 안전
    • String을 요청했는데 Integer를 반환하는 일은 절대 없음. 따라서 Favorites는 타입 안전 이종(heterogeneous) 컨테이너라 부름
public class Favorites {
    private Map<Class<?>, Object> favorites = new HashMap<>();

    public <T> void putFavorite(Class<T> type, T instance) {
        favorites.put(Objects.requireNonNull(type), instance);
    }

    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}
  • 변수 favorites 맵의 키가 Class<?> 타입이기 때문에 모든 키가 서로 다른 매개변수화 타입일 수 있음 (Class<String>, Class<Integer> 등)
    • 다양한 타입을 지원하는 힘이 여기서 나온다
  • favorites 맵의 값 타입은 단순히 Object
    • 맵은 키와 값 사이의 타입 관계를 보증하지 않음
    • 자바의 타입 시스템에서는 이 관계를 명시할 방법이 없지만, 우리는 이 관계가 성립함을 알고 있음
  • putFavorite 메서드는 키와 값 사이의 '타입 링크(type linkage)' 정보는 버려짐
    • 값이 키 타입의 인스턴스라는 정보는 버려지지만, getFavorite 메서드에서 이 관계를 되살릴 수 있음
  • getFavorite 메서드는 맵에서 Object 타입을 꺼내서 동적 형변환
    • cast 메서드는 형변환 연산자의 동적 버전
    • cast 메서드는 Class 클래스가 제네릭이라는 이점을 완벽히 활용
      • T로 비검사 형변환하는 손실 없이도 Favorites를 타입 안전하게 만듬
  • 두 가지 제약 사항
    • 악의적인 클라이언트가 Class 객체를 로 타입으로 넘기면 Favorites 인스턴스의 타입 안전성이 쉽게 깨짐
      • 하지만 클라이언트 코드에서 컴파일타임에 비검사 경고 발생 - 동적 형변환으로 런타임 타입 안전성 확보 가능
      • 이 박식을 적용한 컬렉션 래퍼 checkedSet, checkedList, checkedMap 등이 있음
    • 실체화 불가 타입(제네릭 또는 매개변수화 타입)에서 사용할 수 없음
      • 이 제약에 대한 완벽히 만족스러운 우회로는 없음 - 한계는 있지만 슈퍼 타입 토큰으로 어느 정도 우회 가능
// 동적 형변환으로 런타임 타입 안전성 확보
public <T> void putFavorite(Class<T> type, T instance) {
    favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
  • Favorites가 사용하는 타입 토큰은 비한정적. 즉 getFavorite, putFavorite은 어떤 Class 객체든 받아들임
    • 타입을 제한하고 싶으면 한정적 타입 토큰 활용 가능
    • 애너테이션 API는 한정적 타입 토큰을 적극적으로 사용
      • annotationType 인수는 애너테이션 타입을 뜻하는 한정적 타입 토큰
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
  • Class<?> 타입의 객체가 있고, getAnnotation 처럼 한정적 타입 토큰을 받는 메서드에 넘긴다고 가정하자
    • Class<? extends Annotaion>으로 형변환하면 비검사이므로 컴파일 경고 발생
    • Class 클래스는 이런 형변환을 안전하고 동적으로 수행하는 메서드를 제공
      • asSubclass 메서드로 호출된 인스턴스 자신의 Class 객체를 인수가 명시한 클래스로 형 변환하고 실패하면 ClassCastException 발생
// asSubclass를 사용해 한정적 타입 토큰을 안전하게 형변환한다.
static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) {
    Class<?> annotationType = null; // 비한정적 타입 토큰
    try {
        annotationType = Class.forName(annotationTypeName);
    } catch (Exception ex) {
        throw new IllegalArgumentException(ex);
    }
    return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}

6. 열거 타입과 애너테이션

Item 34: int 상수 대신 열거 타입을 사용하라

정수 열거 패턴의 단점

// 정수 열거 패턴 - 상당히 취약하다!
public static final int APPLE_FUJI         = 0;
public static final int APPLE_PIPPIN       = 1;
public static final int APPLE_GRANNY_SMITH = 2;

public static final int ORANGE_NAVEL  = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD  = 2;
  • 타입 안전을 보장할 방법이 없으며 표현력도 좋지 않음
    • 오렌지를 건네야할 메서드에 사과를 보내고 동등 연산자(==)로 비교하더라도 컴파일러는 아무런 경고를 하지 않음
// 향긋한 오렌지 향의 사과 소스!
int i = (APPLE_FUJI - ORANGE_TEMPLE) / APPLE_PIPPIN;
  • 정수 열거 패턴은 평범한 상수 변수를 나열하며, 상수 값은 이 값을 사용하는 클라이언트로 컴파일됨
    • 상수 값이 바뀌면 클라이언트도 반드시 다시 컴파일해야 함. 다시 컴파일하지 않은 클라이언트는 실행이 되더라도 엉뚱하게 동작함
  • 상수 값을 출력하거나 디버거로 살펴보면 숫자로만 보여 도움 되지 않음
  • 같은 정수 열거 그룹에 속한 모든 상수를 순회하거나 그 그룹의 크기를 알 수도 없음
  • 문자열 열거 패턴은 정수 열거 패턴보다 더 나빠
    • 상수의 의미를 출력할 수는 있지만 상수의 이름 대신 문자열 상수 값을 하드코딩할 수 있음
    • 하드코딩한 문자열에 오타가 있으면 컴파일타임이 아닌 런타임 버그 발생
    • 문자열 비교에 따른 성능 저하

자바 열거 타입

// 가장 단순한 열거 타입
public enum Apple  { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }
  • 자바의 열거 타입은 완전한 형태의 클래스라서 단순 정수 값인 다른 언어의 열거 타입보다 강력함
    • 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개는 클래스
    • 외부 접근 가능 생성자를 제공하지 않으므로 사실상 final
      • 클라이언트가 직접 인스턴스를 생성하거나 확장할 수 없음
      • 열거 타입으로 만들어진 인스턴스들은 딱 하나씩만 존재
    • 인스턴스 통제됨
      • 싱글턴은 원소가 하나뿐인 열거 타입
      • 싱글턴을 일반화한 형태
  • 컴파일타임 타입 안전성을 제공
  • 각자의 이름공간이 있어 이름이 같은 상수도 평화롭게 공존
  • 필드 이름만 공개되므로, 상수를 추가하거나 순서를 바꿔도 다시 컴파일하지 않아도 됨
    • 정수 열거 패턴과 달리 상수 값이 클라이언트로 컴파일되어 각인되지 않음
  • toString 메서드는 출력하기 적합한 문자열을 내어줌
  • Object 메서드들의 고품질 구현을 제공하며 ComparableSerializable을 구현함
데이터와 메서드를 갖는 열거 타입
// 데이터와 메서드를 갖는 열거 타입
public enum Planet {
    MERCURY(3.302e+23, 2.439e6),
    VENUS  (4.869e+24, 6.052e6),
    ...;

    private final double mass;           // 질량(단위: 킬로그램)
    private final double radius;         // 반지름(단위: 미터)
    private final double surfaceGravity; // 표면중력(단위: m / s^2)

    // 중력상수(단위: m^3 / kg s^2)
    private static final double G = 6.67300E-11;

    // 생성자
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        surfaceGravity = G * mass / (radius * radius);
    }

    public double mass()           { return mass; }
    public double radius()         { return radius; }
    public double surfaceGravity() { return surfaceGravity; }

    public double surfaceWeight(double mass) {
        return mass * surfaceGravity;  // F = ma
    }
}
  • 열거 타입 상수와 데이터와 연결하려면 인스턴스 필드를 선언하고 데이터를 받아 필드에 저장하는 생성자를 작성
    • 필드는 공개해도 되지만 비공개로 두고 접근자 메서드를 제공하는게 낫다.
public static void main(String[] args) {
    double earthWeight = Double.parseDouble(args[0]);
    double mass = earthWeight / Planet.EARTH.surfaceGravity();
    for (Planet p : Planet.values())
        System.out.printf("%s에서의 무게는 %f이다.%n", p, p.surfaceWeight(mass));
}
공개 범위
  • 열거 타입에서 상수를 하나 제거하면
    • 제거한 상수를 참조하지 않는 클라이언트에는 아무 영향이 없음
    • 제거된 상수를 참조하는 클라이언트는 다시 컴파일하면 유용한 오류 메시지가 발생
  • 가능하다면 private 또는 package-private으로 선언하라
  • 특정 톱레벨 클래스에서만 쓰인다면 해당 클래스의 멤버 클래스로 만들어라

상수별 메서드 구현

// 값에 따라 분기하는 열거 타입 - 이대로 만족하는가?
public enum Operation {
    PLUS, MINUS, TIMES, DIVIDE;

    // 상수가 뜻하는 연산을 수행한다.
    public double apply(double x, double y) {
        switch(this) {
            case PLUS:   return x + y;
            case MINUS:  return x - y;
            case TIMES:  return x * y;
            case DIVIDE: return x / y;
        }
        throw new AssertionError("알 수 없는 연산: " + this);
    }
}
  • 동작하지만 아름답지 않다
    • throw 문이 없으면 컴파일되지 않음
    • 새로운 상수를 추가하면 해당 case 문 추가 필요. 빠져도 컴파일되지만 새로 추가한 연산 수행시 런타임 오류 발생
// 상수별 메서드 구현을 활용한 열거 타입
public enum 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; } };

    public abstract double apply(double x, double y);
}
  • apply 메서드가 상수 선언 바로 옆에 붙어 있으니 메서드 재정의를 깜빡하기 어려움
    • apply 메서드가 추상 메서드이므로 누락시 컴파일 오류
// 상수별 클래스 몸체(class body)와 데이터를 사용한 열거 타입
public enum 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;

    Operation(String symbol) { this.symbol = symbol; }

    @Override public String toString() { return symbol; }

    public abstract double apply(double x, double y);
}
  • 열거 타입에는 상수 이름을 입력받아 해당 상수를 반환하는 valueOf(String) 메서드가 있음
    • toString 메서드를 재정의하는 경우 fromString 메서드를 함께 제공하는 걸 고려하자
// 열거 타입용 fromString 메서드 구현하기
private static final Map<String, Operation> stringToEnum =
        Stream.of(values()).collect(toMap(Object::toString, e -> e));

// 지정한 문자열에 해당하는 Operation을 (존재한다면) 반환한다.
public static Optional<Operation> fromString(String symbol) {
    return Optional.ofNullable(stringToEnum.get(symbol));
}
  • 상수별 메서드 구현의 단점은 열거 타입 상수간 코드를 공유하기 어렵다는 것
// 값에 따라 분기하여 코드를 공유하는 열거 타입 - 좋은 방법인가?
enum PayrollDay {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;

    private static final int MINS_PER_SHIFT = 8 * 60;

    int pay(int minutesWorked, int payRate) {
        int basePay = minutesWorked * payRate;

        int overtimePay;
        switch(this) {
            case SATURDAY: case SUNDAY: // 주말
                overtimePay = basePay / 2;
                break;
            default: // 주중
                overtimePay = minutesWorked <= MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
        }
        return basePay + overtimePay;
    }
}
  • 간결하지만, 관리 관점에서 위험한 코드
    • 휴가와 같은 새로운 값을 추가한다고 가정할 때, 해당 case 문을 넣지 않는다면 (컴파일은 되지만) 휴가 기간에도 평일과 같은 임금을 받게 됨
    • 상수별 메서드 구현으로 정확히 계산하는 방법 두 가지
      • 잔업수당을 계산하는 코드를 모든 상수에 중복해서 넣거나
      • 계산 코드를 평일용/주말용 나눠 각각을 도우미 메서드로 작성 후 적절히 호출
      • 두 방식 모두 코드가 장황해져 가독성이 떨어지고 오류 발생 가능성이 커짐
    • PayrollDay에 평일 잔업수당 계산용 메서드인 overtimePay를 구현해 놓고 주말 상수에만 재정의해 쓰면 장황한 부분을 줄일 수 있지만, switch 문과 동일한 단점을 가짐
      • 새로운 상수를 추가하면서 overtimePay 메서드를 재정의하지 않으면 평일용 코드를 그대로 물려받게 됨
    • 가장 깔끔한 방법은 새로운 상수 추가시 잔업수당 '전략'을 선택하도록 하는 것
      • 아래와 같이 private 중첩 열거 타입을 정의하고 생성자 매개변수로 전달
// 전략 열거 타입 패턴
enum PayrollDay {
    MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY), THURSDAY(WEEKDAY), FRIDAY(WEEKDAY),
    SATURDAY(WEEKEND), SUNDAY(WEEKEND);

    private final PayType payType;

    PayrollDay(PayType payType) { this.payType = payType; }
    
    int pay(int minutesWorked, int payRate) {
        return payType.pay(minutesWorked, payRate);
    }

    // 전략 열거 타입
    enum PayType {
        WEEKDAY {
            int overtimePay(int minsWorked, int payRate) {
                return minsWorked <= MINS_PER_SHIFT ? 0 : (minsWorked - MINS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND {
            int overtimePay(int minsWorked, int payRate) {
                return minsWorked * payRate / 2;
            }
        };

        abstract int overtimePay(int mins, int payRate);
        private static final int MINS_PER_SHIFT = 8 * 60;

        int pay(int minsWorked, int payRate) {
            int basePay = minsWorked * payRate;
            return basePay + overtimePay(minsWorked, payRate);
        }
    }
}

switch 문은 안좋은가?

  • 기존 열거 타입에 상수별 동작을 혼합해 넣을 때는 switch 문이 좋은 선택이 될 수 있음
    • Operation 열거 타입이 있는데 각 연산의 반대 연산을 반환하는 메서드가 필요하다면
// switch 문을 이용해 원래 열거 타입에 없는 기능을 수행한다.
public static Operation inverse(Operation op) {
    switch(op) {
        case PLUS:   return Operation.MINUS;
        case MINUS:  return Operation.PLUS;
        case TIMES:  return Operation.DIVIDE;
        case DIVIDE: return Operation.TIMES;
        default:  throw new AssertionError("알 수 없는 연산: " + op);
    }
}
  • 추가하려는 메서드가 의미상 열거 타입에 속하지 않는다면 직접 만든 열거 타입이라도 이 방식을 적용하는 게 좋다
    • 종종 쓰이지만 열거 타입 안에 포함할 만큼 유용하지 않은 경우도 마찬가지

Item 35: ordinal 메서드 대신 인스턴스 필드를 사용하라

개요

  • 모든 열거 타입은 해당 상수가 그 열거 타입에서 몇 번째 위치인지 반환하는 ordinal이라는 메서드를 제공
// ordinal을 잘못 사용한 예 - 따라하지 말 것!
public enum Ensemble {
    SOLO, DUET, TRIO, QUARTET, QUINTET,
    SEXTET, SEPTET, OCTET, NONET, DECTET;

    public int numberOfMusicians() { return ordinal() + 1; }
}
  • 동작은 하지만 유지보수가 끔찍

해결방법

// 인스턴스 필드에 정수 데이터를 저장하는 열거 타입
public enum Ensemble {
    SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
    SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
    NONET(9), DECTET(10), TRIPLE_QUARTET(12);

    private final int numberOfMusicians;
    Ensemble(int size) { this.numberOfMusicians = size; }
    public int numberOfMusicians() { return numberOfMusicians; }
}
  • Enum의 API 문서를 보면 ordinal에 대해
    • 대부분 프로그래머는 이 메서드를 쓸 일이 없다. 이 메서드는 EnumSetEnumMap 같이 열거 타입 기반의 범용 자료구조에 쓸 목적으로 설계되었다.

Item 36: 비트 필드 대신 EnumSet을 사용하라

개요

// 비트 필드 열거 상수 - 구닥다리 기법!
public class Text {
    public static final int STYLE_BOLD          = 1 << 0; // 1
    public static final int STYLE_ITALIC        = 1 << 1; // 2
    public static final int STYLE_UNDERLINE     = 1 << 2; // 4
    public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8

    // 매개변수 styles는 0개 이상의 STYLES_ 상수를 비트별 OR한 값이다.
    public void applyStyles(int styles) { ... }
}
  • 위 표현은 비트 필드라고 하며, 비트별 OR를 사용해 여러 상수를 하나의 집합으로 결합할수 있다.
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
  • 비트 필드를 사용하면 비트별 연산을 사용해 합집합과 교집합 같은 집합 연산 수행 가능
    • 정수 열거 상수의 단점을 그대로 지닐뿐 아니라 더 많은 문제를 안고 있음

해결방법

  • Set 인터페이스를 구현한 EnumSet은 타입 안전하고 다른 Set 구현체와도 함께 사용 가능
// EnumSet - 비트 필드를 대체하는 현대적 기법
public class Text {
    public enum Style {BOLD, ITALIC, UNDERLINE, STRIKETHROUGH}

    // 어떤 Set을 넘겨도 되나, EnumSet이 가장 좋다.
    public void applyStyles(Set<Style> styles) { ... }
}
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

public interface Code<T> {
    T getCode();
}
public enum MemberStatus implements Code<String> {
    ACTIVATED("ACT"),
    PENDING  ("PEN");

    @Getter private final String code;

    MemberStatus(String code) { this.code = code; }
}
public abstract class AbstractCodeConverter<E extends Enum<E> & Code<T>, T>
            implements AttributeConverter<E, T> {
    private final Class<E> type;

    public AbstractCodeConverter(Class<E> type) { this.type = type; }

    @Override public final T convertToDatabaseColumn(E attribute) {
        if(attribute == null) return null;
        return attribute.getCode();
    }

    @Override public final E convertToEntityAttribute(T dbData) {
        return EnumUtils.lookupFromCode(type, dbData);
    }
}
@Converter(autoApply = true)
public class MemberStatusConverter
            extends AbstractCodeConverter<MemberStatus, String> {

    public MemberStatusConverter() { super(MemberStatus.class); }
}
public class EnumUtils {
    public static <E extends Enum<E> & Code<T>, T> E lookupFromCode(
                Class<E> type, T code) {
        for (E e : EnumSet.allOf(type))
            if (e.getCode().equals(code)) return e;
        return null;
    }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.