Dev Book Review/Effective Java

[Effective Java] Chapter6: 열거 타입과 애너테이션

item 34. int 상수 대신 열거 타입을 사용하라 열거 타입은 확실히 정수 상수보다 뛰어나다. 더 알기쉽고 강력하다 대다수 열거 타입이 명시적 생성자나 메서드 없이 쓰이지만, 각 상수를 특정 데이터와 연결짓거나 상수마다 다르게 동작하게 할 때는 필요하다 하나의 메서드가 상수별로 다르게 동작해야할 때에는 switch문 대신 상수별 메서드 구현을 사용하자 열거 타입 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자. Link : jyami.tistory.com/102 [Effective Java] item 34. int 상수 대신 열거 타입을 사용하라 열거타입 (enum) : 일정 개수의 상수 값을 정의한 다음 그외의 값은 허용하지 않는 타입 정수 열거 패턴 (int enum patt..

[Effective Java] Chapter6: 열거 타입과 애너테이션

728x90

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

  • 열거 타입은 확실히 정수 상수보다 뛰어나다. 더 알기쉽고 강력하다
  • 대다수 열거 타입이 명시적 생성자나 메서드 없이 쓰이지만, 각 상수를 특정 데이터와 연결짓거나 상수마다 다르게 동작하게 할 때는 필요하다
  • 하나의 메서드가 상수별로 다르게 동작해야할 때에는 switch문 대신 상수별 메서드 구현을 사용하자
  • 열거 타입 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자.
  • Link : jyami.tistory.com/102
 

[Effective Java] item 34. int 상수 대신 열거 타입을 사용하라

열거타입 (enum) : 일정 개수의 상수 값을 정의한 다음 그외의 값은 허용하지 않는 타입 정수 열거 패턴 (int enum pattern) : 이전까지 사용하던 패턴 1. 정수 열거 패턴 (int enum pattern)의 단점 public stati..

jyami.tistory.com

 

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

  • ordinal 메서드는 EnumSet과 EnumMap과 같이 열거 타입 기반의 범용 자료 구조에 쓸 목적으로 설계 되었다. 이 경우가 아니면 사용하지 말자
  • 열거 타입 상수에 연결 된 값을 ordinal 메서드로 얻지 말고 인스턴스 필드에 저장하라.
  • Link : https://jyami.tistory.com/103
 

[Effective Java] item 35. ordinal 메서드 대신 인스턴스 필드를 사용하라

1. ordinal 메서드를 잘못쓸 때 ordinal 메서드 : 해당 상수가 그 열거 타입에서 몇 번째 위치인지 반환하는 메서드 상수 선언 순서를 바꾸면 오동작한다. 이미 사용중인 정수와 값이 같은 상수는 추��

jyami.tistory.com

 

item 36. 비트 필드 대신 EnumSet을 사용하라

  • 열거할 수 있는 타입을 한데 모아 집합 형태로 사용한다고 해도 비트 필드를 사용할 이유는 없다
  • EnumSet 클래스가 비트 필드 수준의 명료함과 성능을 제공하고 아이템 34에서 설명한 열거 타입의 장점까지 선사한다
  • EnumSet의 유일한 단점은 (자바 11까지는 아직) 불변 EnumSet을 만들 수 없다는 것이다.
  • 향후 릴리즈에서 수정될 때 까지는 (명확성과 성능이 조금 희생되지만) Collections.unmodifiableSet으로 EnumSet을 감싸 사용할 수 있다.
  • Link : https://jyami.tistory.com/104
 

[Effective Java] item 36. 비트 필드 대신 EnumSet을 사용하라

1. 비트 필드란? 비트 필드 : 비트별 OR를 사용해 여러 상수를 하나의 집합으로 모을 수 있는 집합 public class Text{ public static final int STYLE_BOLD = 1 << 0; // 1 public static final int STYLE_ITALIC..

jyami.tistory.com

 

 

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

  • 배열의 인덱스를 위해 ordinal을 쓰는 것은 일반적으로 좋지 않으니 EnumMap을 사용하라
  • 다차원 관계는 EnumMap<..., EnumMap<...>> 으로 표현하라
  • 애플리케이션 프로그래머는 Enum.ordinal을 (왠만해서는) 사용하지 말아야한다 (아이템 35)는 일반원칙의 특수한 사례다
  • Link : https://jyami.tistory.com/105
 

[Effective Java] item 37. ordinal 인덱싱 대신 EnumMap을 사용하라

1. 올바르지 않은 방법 : ordinal()을 배열 인덱스로 사용 Set [] plant ByLisfeCycle = (Set []) new Set[Plant.LifeCycle.values().length]; for(int i =0; i < plantsByLifeCycle.length; i++) plantsByLifeCyc..

jyami.tistory.com

 

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

  • 열거 타입 자체는 확장할 수 없지만, 인터페이스와 그 인터페이스를 구현하는 기본 열거 타입을 함께 사용해 같은 효과를 낼 수 있다.
  • 이렇게 하면 클라이언트는 이 인터페이스를 구현해 자신만의 열거 타입(혹은 다른 타입)을 만들 수 있다.
  • API가 (기본 열거 타입을 직접 명시하지 않고) 인터페이스 기반으로 작성되었다면 기본 열거 타입의 인스턴스가 쓰이는 모든 곳을 새로 확장한 열거 타입의 인스턴스로 대체해 사용 할 수 있다.
  • Link : https://jyami.tistory.com/106
 

[Effective Java] item 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라

1. 열거타입 확장은 하지 말자 열거 타입은 거의 모든 상황에서 타입 안전 열거 패턴(typesafe enum pattern)보다 우수하다. 단점 : 타입 안전 열거 패턴은 확장할 수 있으나 열거 타입은 그렇지 못하다

jyami.tistory.com

 

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

  • 애너테이션으로 할 수 있는 일을 명명패턴으로 처리할 이유는 없다.
  • 자바 프로그래머라면 예외 없이 자바가 제공하는 애너테이션 타입들은 사용해야한다.
  • 마커애너테이션을 이용하여 애너테이션에 관심있는 프로그램에 추가 정보를 제공한다.
  • 매개변수를 받는 애너테이션 타입, 반복가능한 애너테이션 등을 사용하여 소스 코드에 추가 정보를 제공할 수 있는 도구를 제공하자.
  • Link : https://jyami.tistory.com/107
 

[Effective Java] item 39. 명명 패턴보다 애너테이션을 사용하라

1. 명명 패턴의 단점 ex) junit3 : 테스트 메서드의 시작을 test로 시작하게 하였다. 오타가 나면 안된다. 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다. 메서드가 아닌 클래스 명을 Tes

jyami.tistory.com

 

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

  • 재정의한 모든 메서드에 @Override 애너테이션을 의식적으로 달면 실수했을 때 컴파일러가 바로 알려준다.
  • 예외는 하나다. 구체 클래스에서 상위 클래스의 추상 메서드를 재정의한 경우엔 이 애너테이션을 달지 않아도 된다. (단다고 해서 해롭진 않다.)
  • Link : https://jyami.tistory.com/108
 

[Effective Java] item 40. @Override 애너테이션을 일관되게 사용하라

1. @Override를 사용했을 때 장점 @Override : 메서드 선언에만 달 수 있다. 상위 타입의 메서드를 재정의했음을 뜻한다. Overriding을 Overloading로 잘못 작성할 수 있는 오류를 방지 할 수 있다. 잘못 작성 �

jyami.tistory.com

 

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

  • 마커 인터페이스와 마커 애너테이션은 각자의 쓰임이 있다.
  • 새로 추가하는 메서드 없이 단지 타입 정의가 목적이라면 마커 인터페이스를 선택하자
  • 클래스나 인터페이스의 프로그램요소에 마킹해야하거나 애너테이션을 적극 활용하는 프레임워크의 일부로 그 마커를 편입시키고자 한다면 마커 애너테이션이 올바른 선택이다.
  • 적용 대상이 ElementType.TYPE인 마커 애너테이션을 작성하고 있다면, 잠시 여유를 갖고 정말 애너테이션으로 구현하는게 옳은지, 혹은 마커 인터페이스가 낫지는 않을지 곰곰히 생각해보자
  • Link : https://jyami.tistory.com/109
 

[Effective Java] item 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라

1. 마커 인터페이스 (marker interface) 마커 인터페이스(marker interface) : 아무 메서드도 담고 있지 않고, 단지 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 것 실제로 아무런 메서드도 담

jyami.tistory.com

 

댓글

Comments

Dev Book Review/Effective Java

[Effective Java] item 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라

1. 마커 인터페이스 (marker interface) 마커 인터페이스(marker interface) : 아무 메서드도 담고 있지 않고, 단지 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 것 실제로 아무런 메서드도 담고있지 않다 : Serializable을 구현한 클래스의 인스턴스는 ObjectOutputStream 을 통해 쓸 수 있다고, 즉 직렬화 할 수 있다고 알려준다. ObjectOutputStream의 writeObject0 메서드안에는 Serializable의 인스턴스인지 확인하는 검증 로직이 들어가있으며, 마킹이 안되어있다고 판단되는 경우에 NotSerializableException이 발생한다. 2. 마커 인터페이스의 장점 마커 인터페이스는 이를 구현한 클래스의 인스턴스들을 구..

[Effective Java] item 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라

728x90

1. 마커 인터페이스 (marker interface)

마커 인터페이스(marker interface) : 아무 메서드도 담고 있지 않고, 단지 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 것

item41-1

실제로 아무런 메서드도 담고있지 않다 : Serializable을 구현한 클래스의 인스턴스는 ObjectOutputStream 을 통해 쓸 수 있다고, 즉 직렬화 할 수 있다고 알려준다.

item41-2

ObjectOutputStream의 writeObject0 메서드안에는 Serializable의 인스턴스인지 확인하는 검증 로직이 들어가있으며, 마킹이 안되어있다고 판단되는 경우에 NotSerializableException이 발생한다.

 

2. 마커 인터페이스의 장점

  • 마커 인터페이스는 이를 구현한 클래스의 인스턴스들을 구분하는 타입으로 쓸 수 있으나, 마커 애너테이션은 그렇지 않다.
    인터페이스가 어엿한 타입이므로 런타임에야 발견될 오류를 컴파일타임에 잡을 수 있다
    ex ) 애너테이션 : 직렬화 할 수 없는 객체를 넘겨도 런타임에야 발견 가능함
  • 적용 대상을 더 정밀하게 지정할 수 있다 : 특정 인터페이스를 구현한 클래스에만 적용하게!
    @Target(ElementType.TYPE) : 모든 타입에 달 수 있다. (클래스, 인터페이스, 열거타입, 애너테이션)

Set 인터페이스 : 일종의 마커 인터페이스

  • Set은 Collection의 하위 타입에만 적용할 수 있다.
  • Collection이 정의한 메서드 외에는 새로 추가한 것이 없다.
  • 특정 인터페이스의 하위타입에만 적용할 수 있으며, 아무 규약에도 손대지 않은 마커 인터페이스는 충분히 있음직 하다
    • 객체의 특정 부분을 불변식으로 규정
    • 그 타입의 인스턴스는 다른 클래스의 특정 메서드가 처리할 수 있다는 사실을 명시하는 용도로 사용

 

3. 마커 애너테이션의 장점

  • 거대한 애너테이션 시스템의 자원을 받는다.
  • 애너테이션을 적극 활용하는 프레임워크에서는 애너테이션을 사용하는게 일관성에 좋다.

 

4. 사용해야할 때

1. 클래스와 인터페이스 외의 프로그램 요소 (모듈, 패키지, 필드, 지역변수)
이 요소들에 마킹해야할 때 애너테이션을 쓸 수 밖에 없다.

2. 클래스와 인터페이스 요소
"이 마킹이 된 객체를 매개변수로 받는 매서드를 작성할 일이 있을 때" : 인터페이스
마커 인터페이스를 메서드의 매개변수 타입으로 사용하여 컴파일타임에 오류를 잡을 수 있다. > 아닐 때 : 애너테이션

3. 애너테이션을 활발히 활용하는 프레임워크
마커 애너테이션을 사용하는 편이 좋다.

댓글

Comments

Dev Book Review/Effective Java

[Effective Java] item 40. @Override 애너테이션을 일관되게 사용하라

1. @Override를 사용했을 때 장점 @Override : 메서드 선언에만 달 수 있다. 상위 타입의 메서드를 재정의했음을 뜻한다. Overriding을 Overloading로 잘못 작성할 수 있는 오류를 방지 할 수 있다. 잘못 작성 했을 경우, 컴파일시 컴파일러가 잘못된 부분을 명확히 알려준다. 대부분의 IDE에서 의도한 재정의를 상위 클래스의 메서드를 재정의하려는 모든 메서드에 @Override 애너테이션을 달자 컴파일 오류의 보완재 역할이다. @Override를 다는 습관을 들이자! 시그니처가 올바른지 재차 확신할 수 있다. 2. @Override를 작성하지 않아도 되는 예외 경우 구체 클래스에서 상위 클래스의 추상 메서드를 재정의할 때 (이때 밖에 없다.) 구체 클래스인데 구현하지 않은 메..

[Effective Java] item 40. @Override 애너테이션을 일관되게 사용하라

728x90

1. @Override를 사용했을 때 장점

@Override : 메서드 선언에만 달 수 있다. 상위 타입의 메서드를 재정의했음을 뜻한다.

  • Overriding을 Overloading로 잘못 작성할 수 있는 오류를 방지 할 수 있다.
  • 잘못 작성 했을 경우, 컴파일시 컴파일러가 잘못된 부분을 명확히 알려준다.
  • 대부분의 IDE에서 의도한 재정의를
  • 상위 클래스의 메서드를 재정의하려는 모든 메서드에 @Override 애너테이션을 달자

컴파일 오류의 보완재 역할이다.

@Override를 다는 습관을 들이자! 시그니처가 올바른지 재차 확신할 수 있다.

 

2. @Override를 작성하지 않아도 되는 예외 경우

구체 클래스에서 상위 클래스의 추상 메서드를 재정의할 때 (이때 밖에 없다.)

  • 구체 클래스인데 구현하지 않은 메서드가 있다면 컴파일러가 알려주기 때문이다.

그러나 재정의 메서드 모두에 @Override를 일괄로 붙여두어도 상관없다.

댓글

Comments

Dev Book Review/Effective Java

[Effective Java] item 39. 명명 패턴보다 애너테이션을 사용하라

1. 명명 패턴의 단점 ex) junit3 : 테스트 메서드의 시작을 test로 시작하게 하였다. 오타가 나면 안된다. 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다. 메서드가 아닌 클래스 명을 TestSafetyMechanisms으로 지었을 때 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다. 예외를 던져야 성공하는 테스트 : 방법이 없다. 2. 마커(marker) 애너테이션 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Test { } 메타 애너테이션 (meta-annotation) : 애너테이션 선언에 다는 애너테이션 @Retention(RetentionPolicy.RUNTIME) :..

[Effective Java] item 39. 명명 패턴보다 애너테이션을 사용하라

728x90

1. 명명 패턴의 단점

ex) junit3 : 테스트 메서드의 시작을 test로 시작하게 하였다.

  • 오타가 나면 안된다.
  • 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다.
    메서드가 아닌 클래스 명을 TestSafetyMechanisms으로 지었을 때
  • 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.
    예외를 던져야 성공하는 테스트 : 방법이 없다.

 

2. 마커(marker) 애너테이션

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

메타 애너테이션 (meta-annotation) : 애너테이션 선언에 다는 애너테이션

@Retention(RetentionPolicy.RUNTIME) : @Test가 런타임에도 유지되어야한다 - 보존 정책

@Target(ElementType.METHOD) : @Test가 메서드 선언에서만 사용되어야 한다 - 적용 대상

마커 애너테이션 : 아무 매개변수 없이 단순히 대상에 마킹을 한다.

  • 대상 코드의 의미는 그대로 둔채 애너테이션에 관심 있는 도구에서 특별한 처리를 할 기회를 준다.
  • 실제 클래스에 영향은 주지 않지만, 애너테이션에 관심있는 프로그램에 추가 정보를 제공한다.
import java.lang.reflect.*;

public class RunTests { // 코드 39-3 마커 애너테이션을 처리하는 프로그램
  public static void main(String[] args) throws Exception {
    int tests = 0;
    int passed = 0;
    Class<?> testClass = Class.forName(args[0]);
    for (Method m : testClass.getDeclaredMethods()) {
      if (m.isAnnotationPresent(Test.class)) {
        tests++;
        try {
          m.invoke(null);
          passed++;
        } catch (InvocationTargetException wrappedExc) {
          Throwable exc = wrappedExc.getCause();
          System.out.println(m + " 실패: " + exc);
        } catch (Exception exc) {
          System.out.println("잘못 사용한 @Test: " + m);
        }
      }
    }
    System.out.printf("성공: %d, 실패: %d%n",
                      passed, tests - passed);
  }
}

리플렉션을 사용하여, @Test 애너테이션이 달린 메서드 찾기, 원래 예외에 담긴 실패 정보 추출하여 출력한다.

 

3. 매개 변수를 가진 애너테이션

예외를 던져야하는 테스트 애너테이션 만들기

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
  Class<? extends Throwable> value();
}

애터네이션의 필드 : 애터네이션에서 사용하는 옵션값을 의미한다 @Test(value = "RuntimeException.class")

Class<? extends Throwable> : Throwable을 확장한 클래스의 Class 객체 - 모든 예외와 오류 타입을 수용함

if (m.isAnnotationPresent(ExceptionTest.class)) {
  tests++;
  try {
    m.invoke(null);
    System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
  } catch (InvocationTargetException wrappedEx) {
    Throwable exc = wrappedEx.getCause();
    Class<? extends Throwable> excType =
      m.getAnnotation(ExceptionTest.class).value();
    if (excType.isInstance(exc)) {
      passed++;
    } else {
      System.out.printf(
        "테스트 %s 실패: 기대한 예외 %s, 발생한 예외 %s%n",
        m, excType.getName(), exc);
    }
  } catch (Exception exc) {
    System.out.println("잘못 사용한 @ExceptionTest: " + m);
  }
}

위의 메서드를 위와 같이 수정한다.

애너테이션 매개변수의 값을 추출하여 테스트 메서드가 올바른 예외를 던지는지 확인하는데 사용한다.

  • 테스트 프로그램이 문제 없이 컴파일 되면 애터네이션 매개변수가 가리키는 예외가 올바른 타입일것.
  • 컴파일타임에는 존재했으나 런타임에는 존재하지 않는 경우 : TypeNotPresentException

 

4. 배열 매개변수를 받는 애너테이션 타입

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Exception>[] value();
}
배열 매개변수 애너테이션의 사용
@ExceptionTest({IndexOutOfBoundsException.class, NullPointerException.class})
if (m.isAnnotationPresent(ExceptionTest.class)) {
  tests++;
  try {
    m.invoke(null);
    System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
  } catch (Throwable wrappedExc) {
    Throwable exc = wrappedExc.getCause();
    int oldPassed = passed;
    Class<? extends Throwable>[] excTypes =
      m.getAnnotation(ExceptionTest.class).value();
    for (Class<? extends Throwable> excType : excTypes) {
      if (excType.isInstance(exc)) {
        passed++;
        break;
      }
    }
    if (passed == oldPassed)
      System.out.printf("테스트 %s 실패: %s %n", m, exc);
  }
}

테스트 러너 수정 : for문을 추가하여 throwable 배열을 검증한다.

 

5. 반복 가능한 애너테이션

  • 자바 8부터 가능하다
  • 배열 매개변수 사용 대신 애너테이션에 @Repeatable메타 애너테이션을 단다.

@Repeatable : 하나의 프로그램 요소에 여러개의 애너테이션을 달 수 있다.

  • @Repeatable을 단 애너테이션을 반환하는 '컨테이션 애너테이션' 을 하나 더 정의한다
  • @Repeatable에 이 컨테이너 애너테이션의 class 객체를 매개변수로 전달해야 한다
  • 컨테이너 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 value 메서드를 정의해야한다
  • 컨테이너 애너테이션 타입에 적절한 @Retention@Target을 명시해야 한다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
  Class<? extends Throwable> value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
  ExceptionTest[] value();
}

적용 방법

@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() {

@Repeatable 처리시 주의점

  • 여러 개 달면 하나만 달았을 때와 구분하기 위해 해당 '컨테이너' 애너테이션 타입이 적용된다.
  • isAnnotationPresent : 반복 가능 애너테이션이 달렸는지 검사 (false : 컨테이너 애너테이션)
  • getAnnotationsByType : 반복 가능 애너테이션과 컨테이너 애너테이션을 모두 가져옴
  • 따로따로 확인하여 코딩한다
if (m.isAnnotationPresent(ExceptionTest.class)
    || m.isAnnotationPresent(ExceptionTestContainer.class)) {
  tests++;
  try {
    m.invoke(null);
    System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
  } catch (Throwable wrappedExc) {
    Throwable exc = wrappedExc.getCause();
    int oldPassed = passed;
    ExceptionTest[] excTests =
      m.getAnnotationsByType(ExceptionTest.class);
    for (ExceptionTest excTest : excTests) {
      if (excTest.value().isInstance(exc)) {
        passed++;
        break;
      }
    }
    if (passed == oldPassed)
      System.out.printf("테스트 %s 실패: %s %n", m, exc);
  }
}
  • 애너테이션을 여러번 달아 코드 가독성을 높였다.
  • 애너테이션을 선언하고 처리하는 부분에서 코드양이 늘어난다.
  • 처리 코드가 복잡해져 오류가 날 가능성이 커짐을 명시하자

 

6. 결론

애너테이션이 명명패턴보다 낫다

다른 프로그래머가 소스코드에 추가 정보를 제공할 수 있는 도구를 만드는 일을 한다면 적당한 애너테이션 타입을 제공하자

애너테이션으로 할 수 있는 일을 명명패턴으로 처리할 이유는 없다.

자바 프로그래머라면 예외 없이 자바가 제공하는 애너테이션 타입들은 사용하자

댓글

Comments

Dev Book Review/Effective Java

[Effective Java] item 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라

1. 열거타입 확장은 하지 말자 열거 타입은 거의 모든 상황에서 타입 안전 열거 패턴(typesafe enum pattern)보다 우수하다. 단점 : 타입 안전 열거 패턴은 확장할 수 있으나 열거 타입은 그렇지 못하다 타입 안전 열거 패턴의 예시 public class DSymbolType{ private final String type; private DSymbolType(String type){ this.type = type; } public String toString(){ return type; } public static final DSymbolType Terminal = new DSymbolType("Terminal"); public static final DSymbolType Process ..

[Effective Java] item 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라

728x90

1. 열거타입 확장은 하지 말자

열거 타입은 거의 모든 상황에서 타입 안전 열거 패턴(typesafe enum pattern)보다 우수하다.

단점 : 타입 안전 열거 패턴은 확장할 수 있으나 열거 타입은 그렇지 못하다

타입 안전 열거 패턴의 예시

public class DSymbolType{
  private final String type;
  private DSymbolType(String type){
    this.type = type;
  }
  public String toString(){
    return type;
  }
  public static final DSymbolType Terminal = new DSymbolType("Terminal");
  public static final DSymbolType Process = new DSymbolType("Process");
  public static final DSymbolType Decision = new DSymbolType("Decision");
}

열거 타입을 확장하는 건 좋지 않다.

  • 확장 타입의 원소는 기반 타입의 원소로 취급, 그러나 반대는 아니다 (이상함)
  • 기반 타입과 확장 타입의 원소 모두 순회할 방법도 마땅치 않다.
  • 확장성을 높이려면 고려할 요소가 늘어나 설계와 구현이 복잡하다.

어울리는 쓰임 : 연산 코드 (opcode)

 

2. 열거 타입 확장

아이디어 : 열거 타입이 임의의 인터페이스를 구현할 수 있다.

public interface Operation {
    double apply(double x, double y);
}
public enum BasicOperation implements Operation {
    PLUS("+") {
        public double apply(double x, double y) { return x + y; }
    },
    MINUS("-") {
        public double apply(double x, double y) { return x - y; }
    },
    TIMES("*") {
        public double apply(double x, double y) { return x * y; }
    },
    DIVIDE("/") {
        public double apply(double x, double y) { return x / y; }
    };

    private final String symbol;

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

    @Override public String toString() {
        return symbol;
    }
}
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;
    }
}

새로 작성한 연산은 기존 연산을 쓰던 곳이면 어디든 사용이 가능하다.

Operation 인터페이스를 사용하도록 작성되있기만 하면된다

// 열거 타입의 Class 객체를 이용해 확장된 열거 타입의 모든 원소를 사용하는 예
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));
}

<T extends Enum<T> & Operation> : Class 객체가 열거 타입인 동시에 Operation의 하위 타입이어야 한다.

// 컬렉션 인스턴스를 이용해 확장된 열거 타입의 모든 원소를 사용하는 예 (235쪽)
private static void test(Collection<? extends Operation> opSet,
                         double x, double y) {
  for (Operation op : opSet)
    System.out.printf("%f %s %f = %f%n",
                      x, op, y, op.apply(x, y));
}

확장 가능한 열거 타입 흉내내는 방식의 문제점

열거 타입끼리 구현을 상속할 수 없다.

  • 디폴트 구현을 이용해 인터페이스에 추가하는 방법을 사용할 수 없다.
  • 연산 기호를 저장하고 찾는 로직이 BasicOperation ,ExtendedOperation 모두에 들어가야한다.

댓글

Comments

Dev Book Review/Effective Java

[Effective Java] item 37. ordinal 인덱싱 대신 EnumMap을 사용하라

1. 올바르지 않은 방법 : ordinal()을 배열 인덱스로 사용 Set[] plant ByLisfeCycle = (Set[]) new Set[Plant.LifeCycle.values().length]; for(int i =0; i p.lifeCycl..

[Effective Java] item 37. ordinal 인덱싱 대신 EnumMap을 사용하라

728x90

1. 올바르지 않은 방법 : ordinal()을 배열 인덱스로 사용

Set<Plant>[] plant ByLisfeCycle = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
for(int i =0; i < plantsByLifeCycle.length; i++)
  plantsByLifeCycle[i] = new HashSet<>();
for(Plant p : garden){
  plantsByLifeCycle[p.lifeCycler.ordinal()].add(p);
}
for(int i =0; i<plantsByLifeCycle.length;i++)}{
  System.out.println("%s: %s%n", Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
}

문제점

  • 배열은 제네릭과 호환되지 않아 비검사 형변환을 수행해야한다. (컴파일이 깔끔하지 않다)
  • 정수 값을 정확히 사용해야한다 : 잘못하면 ArrayIndexOutOfBoundException

 

2. EnumMap : 열거 타입을 키로 사용

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]);
}

열거 타입을 키로 사용하도록 설계한 Map 구현체이다.

  • 안전하지 않은 형변환 사용하지 않는다.
  • 배열 인덱스 계산 과정 오류 가능성도 없다.
  • 내부에서 배열을 사용해서 ordinal 배열만큼 성능이 나온다 : 낭비 되는 공간과 시간이 거의 없어 명확하고 안전하고 유지보수하기 좋다.

EnumMap의 생성자가 받는 키타입의 Class 객체는 한정적 타입 토큰으로 런타입 제네릭 타입정보를 제공한다.

item37-1

 

3. EnumMap을 stream과 함께 사용할 때

스트림을 사용할 때 고유한 맵 구현체를 사용하기 때문에 EnumMap을 사용할 때 얻는 공간과 성능 이점이 사라진다는 문제가 있다.

System.out.println(Arrays.stream(garden)
                   .collect(groupingBy(p -> p.lifeCycle))); // 고유한 맵 구현체
System.out.println(Arrays.stream(garden)
                  .collect(groupingBy(p -> p.lifeCycle, // 원하는 맵 구현체 명시
                                     () -> new EnumMap<>(LifeCycle.class), toSet())));

stream을 사용하면 enumMap 만 사용했을 때와 생성되는 맵의 개수가 다르다.

// 코드 37-2 EnumMap을 사용해 데이터와 열거 타입을 매핑한다.
System.out.println("using EnumMap");
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);

System.out.println("using stream");
// 코드 37-3 스트림을 사용한 코드 1 - EnumMap을 사용하지 않는다!
Map<LifeCycle, List<Plant>> collect = Arrays.stream(garden)
  .collect(groupingBy(p -> p.lifeCycle));
System.out.println(collect);

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

 

item37-2

EnumMap을 이용해 put한 코드드에서는 PERENNIAL이 없어도 생성된다. 반면 stream 을 이용한 코드에서는 2개만 생성된다.

댓글

Comments

Dev Book Review/Effective Java

[Effective Java] item 36. 비트 필드 대신 EnumSet을 사용하라

1. 비트 필드란? 비트 필드 : 비트별 OR를 사용해 여러 상수를 하나의 집합으로 모을 수 있는 집합 public class Text{ public static final int STYLE_BOLD = 1

[Effective Java] item 36. 비트 필드 대신 EnumSet을 사용하라

728x90

1. 비트 필드란?

비트 필드 : 비트별 OR를 사용해 여러 상수를 하나의 집합으로 모을 수 있는 집합

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; // 5

  // 매개변수 styles는 0개 이상의 STYLE_ 상수를 비트별 OR 한 값
  public void applyStyle(int styles){...}
}

비트 필드의 문제점

  • 정수 열거 상수의 단점을 그대로 지님
  • 비트 필드 값이 그대로 출력되면 단순 정수 열거 상수 출력보다 해석이 어렵다
  • 비트 필드 하나에 녹아있는 모든 원소 순회도 까다롭다
  • 최대 몇 비트가 필요한지 API 작성시 미리 예측해야한다.

 

2. 비트 필드의 해결 : EnumSet

열거 타입 상수의 값으로 구성된 집합을 효과적으로 표현한다

내부에서 비트 벡터로 구현되어있어 성능도 비트 필드에 비견된다.

item36-1

EnumSet javadoc 내용

  • enum type을 구현할 때 특화된다.
  • Enum set들은 bit vector들로 표현된다 : 극도로 시간과 공간 복잡도에서 효율적이다 (bit flag)
  • 인자가 enum set이라면 containsAll, retainAll 같은 대형 연산에서도 매우 빠르다.
import java.util.*;

// 코드 36-2 EnumSet - 비트 필드를 대체하는 현대적 기법 (224쪽)
public class Text {
    public enum Style {BOLD, ITALIC, UNDERLINE, STRIKETHROUGH}

    // 어떤 Set을 넘겨도 되나, EnumSet이 가장 좋다.
    public void applyStyles(Set<Style> styles) {
        System.out.printf("Applying styles %s to text%n",
                Objects.requireNonNull(styles));
    }

    // 사용 예
    public static void main(String[] args) {
        Text text = new Text();
        text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
    }
}

EnumSet을 건네리라 짐작되는 상황이라도 이왕이면 인터페이스로 받는게 좋은 습관이다

댓글

Comments

Dev Book Review/Effective Java

[Effective Java] item 35. ordinal 메서드 대신 인스턴스 필드를 사용하라

1. ordinal 메서드를 잘못쓸 때 ordinal 메서드 : 해당 상수가 그 열거 타입에서 몇 번째 위치인지 반환하는 메서드 상수 선언 순서를 바꾸면 오동작한다. 이미 사용중인 정수와 값이 같은 상수는 추가할 수 없다. 중간에 값을 비울 수 없다 : 값을 비우기 위한 더미(dummy) 상수 추가 2. 해결 방법 열거 타입 상수에 연결 된 값을 ordinal 메서드로 얻지 말고 인스턴스 필드에 저장하라. 3. Enum API 문서 이 메서드는 EnumSet과 EnumMap과 같이 열거 타입 기반의 범용 자료 구조에 쓸 목적으로 설계 되었다.

[Effective Java] item 35. ordinal 메서드 대신 인스턴스 필드를 사용하라

728x90

1. ordinal 메서드를 잘못쓸 때

ordinal 메서드 : 해당 상수가 그 열거 타입에서 몇 번째 위치인지 반환하는 메서드

  • 상수 선언 순서를 바꾸면 오동작한다.
  • 이미 사용중인 정수와 값이 같은 상수는 추가할 수 없다.
  • 중간에 값을 비울 수 없다 : 값을 비우기 위한 더미(dummy) 상수 추가

2. 해결 방법

열거 타입 상수에 연결 된 값을 ordinal 메서드로 얻지 말고 인스턴스 필드에 저장하라.

3. Enum API 문서

이 메서드는 EnumSet과 EnumMap과 같이 열거 타입 기반의 범용 자료 구조에 쓸 목적으로 설계 되었다.

 

댓글

Comments