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

Dev Book Review/Effective Java

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

열거타입 (enum) : 일정 개수의 상수 값을 정의한 다음 그외의 값은 허용하지 않는 타입 정수 열거 패턴 (int enum pattern) : 이전까지 사용하던 패턴 1. 정수 열거 패턴 (int enum pattern)의 단점 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; 타입 안전을 보장할 ..

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

728x90

열거타입 (enum) : 일정 개수의 상수 값을 정의한 다음 그외의 값은 허용하지 않는 타입

정수 열거 패턴 (int enum pattern) : 이전까지 사용하던 패턴

 

1. 정수 열거 패턴 (int enum pattern)의 단점

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;
  • 타입 안전을 보장할 방법이 없으며 표현력이 좋지 않다.
    Apple에서 Orange를 사용해도 컴파일러의 경고 메세지가 없다.
  • 접두어를 사용한 이름 충돌을 방지하는 방법을 사용한다.
  • 평범한 상수 나열이라, 컴파일 하면 그 값이 그대로 새겨지기 때문에 프로그램이 깨지기 쉽다.
  • 정수 열거 그룹에 속한 모든 상수를 한 바퀴 순회하는 방법도 마땅치 않으며 상수가 몇개인지도 알 수 없다.

 

2. 문자열 열거 패턴(string enum pattern)

정수 열거 패턴보다 더 나쁘다

private final String APPLE = "1";
private final String GRAPE = "2";
private final String ORANGE = "3";
  • 문자열에 오타가 있어도 컴파일러에서 확인할 길이 없어 런타임 버그
  • 문자열 비교에 따른 성능저하

 

3. 열거 타입 (enum)

public enum Apple {FUJI, PIPPIN, GRANNY_SMITH}
public enum Orange {NAVEL, TEMPLE, BLOOD}

완전한 형태의 클래스 (정수값 뿐인)라서 다른 언어의 열거 타입보다 훨씬 강력하다.

 

a. 열거 타입의 아이디어

  • 열거 타입은 클래스
  • 상수 하나당 자신의 인스턴스를 하나씩 만들어서 public static final 필드로 공개한다.
  • 열거 타입은 final이다 : 밖에서 접근가능한 생성자를 제공하지 않는다.
  • 열거 타입 선언으로 만들어진 인스턴스는 딱 1개만 존재한다.
 

b. 열거 타입과 싱글턴

  • 열거 타입은 인스턴스 통제클래스이다 : 언제 어느 인스턴스를 살아 있게 할지를 통제할 수 있음 (정적 팩터리 방식 클래스)
  • 싱글턴 = 원소가 하나뿐인 열거타입
  • 열거타입 = 싱글턴을 일반화한 형태
 

c. 열거타입의 장점

  • 컴파일 타입 안전성 제공 : Apple 열거타입 인수에 Orange를 넘길 수 없음
  • 이름 같은 상수 공존 : 각자의 이름공간이 있기 때문! 공개 되는 것이 필드의 이름이라 상수 값이 클라이언트에 컴파일 되어 각인되지 않기 때문이다.
  • 임의의 메서드나 필드를 추가할 수 있고 임의의 인터페이스를 구현하게 할 수 있다.
  • 상수를 하나 제거했을 때 : 제거한 상수를 참조하지 않는 클라이언트에 아무 영향이 없다.
    참조를 한 클라이언트에서는 컴파일(런타임-다시 컴파일 X일 때) 오류가 발생할 것! (정수 열거 패턴에서는 기대할 수 없는 대응)

 

4. 데이터와 메서드를 갖는 열거 타입

각 상수와 연관된 데이터를 해당 상수 자체에 내재시킨다.

고차원의 추상 개념 하나를 표현 할 수 있다.

public enum Planet {
    MERCURY(3.302e+23, 2.439e6),
    VENUS  (4.869e+24, 6.052e6),
    EARTH  (5.975e+24, 6.378e6),
    MARS   (6.419e+23, 3.393e6),
    JUPITER(1.899e+27, 7.149e7),
    SATURN (5.685e+26, 6.027e7),
    URANUS (8.683e+25, 2.556e7),
    NEPTUNE(1.024e+26, 2.477e7);

    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
    }
}

열거 타입 상수 각각을 특정 데이터와 연결지을 때 생성자에서 데이터를 받아 인스턴스 필드에 저장한다.

  • 열거 타입은 근본적으로 불변이라 모든 필드는 final이야 한다.
  • 필드를 private으로 두고 별도의 public 접근자 메서드를 두자
 

a. 열거타입의 배열 values()

자신 안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메서드 값들은 선언된 순서로 저장된다.

public class WeightTable {
   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));
   }
}
 

b. 열거타입을 올바르게 사용하기

  • 일반 클래스와 마찬가지로 기능을 클라이언트에게 노출해야할 합당한 이유가 없다면 private으로, 혹은 (필요하다면) package-private으로 선언하라
  • 널리 쓰이는 열거타입 = 톱레벨 클래스로 구현
  • 특정 톱레벨 클래스에서만 사용 = 해당 클래스의 멤버 클래스로 구현

 

5. 상수별 메서드 구현(constant-specific method implementation)

switch를 이용한 구현은 새로운 상수를 추가할 때마다 해당 case문도 추가해야해서 깨지기 쉽다.

상수별 메서드 구현

열거 타입에 추상 메서드를 선언하고, 각 상수별 클래스 몸체(constant-specific class body)를 각 상수에 맞게 재정의하는 방법

import java.util.*;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toMap;

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

    public abstract double apply(double x, double y);    
}

열거 타입의 valueOf(string)

상수 이름을 입력받아 이 이름에 해당하는 상수를 반환해 주는 메서드

fromString 메서드 제공

열거타입의 toString을 재정의 할 때 함께 제공하는 걸 고려해보자.

toString이 반환하는 문자열을 해당 열거 타입 상수로 변환해주는 메서드

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

// 지정한 문자열에 해당하는 Operation을 (존재한다면) 반환한다.
public static Optional<Operation> fromString(String symbol) {
    return Optional.ofNullable(stringToEnum.get(symbol));
}

열거타입 정적 필드의 생성 시점

private static final Map<String, Operation> stringToEnum =
            Stream.of(values()).collect(
                    toMap(Object::toString, e -> e));

Operation 상수가 stringToEnum 맵에 추가되는 시점 : 열거타입 생성 후 정적 필드가 초기화 될 때

열거 타입 상수는 생성자에서 자신의 인스턴스를 맵에 추가할 수 없다 : 컴파일 오류

  • 열거 타입의 정적 필드 중 열거 타입 생성자에서 접근 할 수 잇는 것은 상수 변수 뿐이다.
  • 열거 타입 생성자 실행 시점에는 정적 필드 초기화 전이다.
  • 열거 타입 생성자에서 같은 열거 타입의 다른 상수에도 접근 할 수 없다.
    (열거 타입의 인스턴스를 public static final으로 선언함. 다른 형제 상수도 static이므로 열거 타입 생성자에서 정적 필드에 접근할 수 없다는 제약이 적용된다.)

 

6. 상수별 동작 혼합 : 전략 열거 타입 패턴

열거 타입 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자.

package effectivejava.chapter6.item34;

import static effectivejava.chapter6.item34.PayrollDay.PayType.*;

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

    public static void main(String[] args) {
        for (PayrollDay day : values())
            System.out.printf("%-10s%d%n", day, day.pay(8 * 60, 1));
    }
}

추가하려는 메서드가 의미상 열거타입에 속하는 경우 다음과 같이 전략 열거 타입 패턴을 사용한다.

그렇지 않은 경우에는 switch를 적용해서 간단하게 만든다.

 

7. 열거타입을 사용해야 할 때

  • 필요한 원소를 컴파일 타임에 다 알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자

    Ex) 태양계 행성, 한 주의 요일, 체스말

  • 열거 타입에 정의된 상수 개수가 영원히 고정 불변일 필요는 없다.

    Ex) 메뉴 아이템, 연산 코드, 명령줄 플래그

  • 열거타입의 성능은 상수와 별반 다르지 않다

 

댓글

Comments

Dev Book Review/Effective Java

[Effective Java] Chapter 5: 제네릭

용어정리 한글 영문 예 매개변수화 타입 parameterized type List 실제 타입 매개변수 actual type parameter String 제네릭 타입 generic type List 정규 타입 매개변수 formal type parameter E 비한정적 와일드카드 타입 unbounded wildcard type List 로 타입 raw type List 한정적 타입 매개변수 bounded type parameter 재귀적 타입 한정 recursive type bound 한정적 와일드카드 타입 Bounded wildcard type 로타입 : 제네릭 타입 시스템에 속하지 않는다. Set Set, Set는. 안전하지만, 로타입인 Set은 안전하지 않다. Link : jyami.tistory...

[Effective Java] Chapter 5: 제네릭

728x90

용어정리

한글 영문
매개변수화 타입 parameterized type List<String>
실제 타입 매개변수 actual type parameter String
제네릭 타입 generic type List<E>
정규 타입 매개변수 formal type parameter E
비한정적 와일드카드 타입 unbounded wildcard type List<?>
로 타입 raw type List
한정적 타입 매개변수 bounded type parameter <E extends Number>
재귀적 타입 한정 recursive type bound <T extends Comparable<T>>
한정적 와일드카드 타입 Bounded wildcard type <? extends Number>
제네릭 메서드 generic method static <E> List<E> asList(E[] a)
타입 토큰 type token String.class

 

item26. 로 타입은 사용하지 말라

  • 로타입을 사용하면 런타임에 예외가 일어날 수 있으니 사용하면 안 된다.
  • 로 타입은 제네릭이 도입되기 이전 코드와의 호환성을 위해 제공될 뿐이다.
  • 매개변수화 타입 : 어떤 타입의 객체도 저장할 수 있다.Set<Object>
  • 와일드카드 타입 : 모종의 타입 객체만 저장할 수 있다. Set<?>
  • 로타입 : 제네릭 타입 시스템에 속하지 않는다. Set
  • Set<Object>, Set<?>는. 안전하지만, 로타입인 Set은 안전하지 않다.
  • Link : jyami.tistory.com/90
 

[Effective Java] item26. 로타입은 사용하지 말라

1. 용어정리 public class Example{ private T member; } 제네릭 클래스[인터페이스] : 클래스[인터페이스] 선언에 타입 매개변수(type parameter)가 쓰인다. Example.class 제네릭 타입(Generic Type) : 제네..

jyami.tistory.com

 

item27. 비검사 경고를 제거하라

  • 비검사 경고는 중요하지 무시하지 말자.
  • 모든 비검사 경고는 런타임에 ClassCastException을 일을킬 수 있는 잠재적 가능성을 뜻하니 최선을 다해 제거하라
  • 경고를 없앨 방법을 찾지 못하겠다면 그 코드가 타입 안전함을 증명하고 가능한 한 범위를 좁혀 @SuppressWarning("unchecked") 애너테이션으로 경고를 숨겨라
  • 그런다음 경고를 숨기기로 한 근거를 주석으로 남겨라
  • Link : jyami.tistory.com/91
 

[Effective Java] item 27. 비검사 경고를 제거하라

1. 할수있는 한 모든 비검사 경고를 제거하자 제네릭을 사용하기 시작했을 때 볼 수 있는 수많은 컴파일러 경고 비검사 형변환 경고 비검사 메서드 호출 경고 비검사 매개변수화 가변인수 타입 �

jyami.tistory.com

 

item28. 배열보다는 리스트를 사용하라

  • 배열과 제네릭에는 매우 다른 타입 규칙이 적용된다.
  • 배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입정보가 소거된다.
  • 그 결과 배열은 런타임에는 타입 안전하지만 컴파일타임에는 그렇지 않다.
  • 제네릭은 반대로, 컴파일 타임에는 타입 안전하지만 런타임에는 그렇지 않다.
  • 그래서 둘을 섞어쓰기란 쉽지 않으며, 둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대체하는 방법을 적용하자.
  • 제네릭은 컴파일타임단에서 TypeCasting을 잡아주므로 런타임에 ClassCastingException이 뜨지 않는다.
  • Link : jyami.tistory.com/92
 

[Effective Java] item28. 배열보다는 리스트를 사용하라

1. 배열과 제네릭의 차이 배열 공변 (convariant) - Sub 가 Super 의 하위타입이라면 배열 Sub[] 는 배열 Super[] 의 하위타입이다. (함께 변한다) 배열에서는 실수를 런타임에 타입 오류를 알 수 있다 Object[]

jyami.tistory.com

 

item29. 이왕이면 제네릭 타입으로 만들라

  • 클라언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다.
  • 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하라
  • 그렇게 하려면 제네릭 타입으로 만들어야 할 경우가 많다
  • 기존 타입중 제네릭이었어야 하는게 있다면 제네릭 타입으로 변경하자.
  • 기존 클라이언트에는 아무 영향을 주지 않으면서, 새로운 사용자를 훨씬 편하게 해준다. (raw 타입의 등장 이유)
  • Link : jyami.tistory.com/93
 

[Effective Java] item29. 이왕이면 제네릭 타입으로 만들라

1. 제네릭클래스로 만드는 방법 a. 클래스 선언에 타입매개변수를 추가한다. 보통 E를 많이 사용한다. 제네릭 필드를 쓴다는 것을 명시하는 것이다. b. 실체화 불가 타입으로는 배열을 만들 수 없�

jyami.tistory.com

 

item30. 이왕이면 제네릭 메서드로 만들라

  • 제네릭 타입과 마찬가지로, 클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다 제네릭 메서드가 더 안전하며 사용하기도 쉽다.
  • 타입과 마찬가지로, 메서드도 형변환 없이 사용할 수 있는 편이 좋으며, 많은 경우 그렇게 하려면 제네릭 메서드가 되어야한다.
  • 역시 타입과 마찬가지로 형변환을 해줘야하는 기존 메서드는 제네릭하게 만들자
  • 기존 클라이언트는 그대로 둔 채 새로운 사용자의 삶을 훨씬 편하게 만들어줄 것이다 (raw 타입의 등장이유)
  • Link : jyami.tistory.com/94
 

[Effective Java] item30. 이왕이면 제네릭 메서드로 만들라

1. 제네릭 메서드 만들기 메서드도 제네릭으로 만들 수 있다. ex ) Collections의 '알고리즘' 메서드 메서드 선언에서 원소타입을 타입 매개변수로 지정한다. 메서드 안에서 이 타입 매개변수를 사용�

jyami.tistory.com

 

Item31. 한정적 와일드 카드를 사용해 API 유연성을 높여라

  • 조금 복잡해도 와일드 카드 타입을 적용하면 API가 훨씬 유연해진다.
  • 널리쓰일 라이브러리를 작성한다면 반드시 와일드카드 타입을 적절히 사용해주자.
  • PECS 공식을 기억하자
  • producer는 extends를 consumer는 super를 사용한다.
  • Comparable과 Comparator는 모두 소비자이다.
  • Link : jyami.tistory.com/95
 

[Effective Java] item31. 한정적 와일드 카드를 사용해 API 유연성을 높여라

1. 매개변수화 타입의 불공변 매개변수화 타입은 불공변이다(invariant) : 서로 다른 타입 Type1 , Type2 가 있을 때 List 은 List 의 하위타입도 아니고 상위타입도 아니다. (리스코프 치환 원칙

jyami.tistory.com

 

item32. 제네릭과 가변인수를 함께 쓸 때는 신중하라

  • 가변인수와 제네릭은 궁합이 좋지 않다.
  • 가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못하고, 배열과 제네릭의 타입 규칙이 서로 다르기 때문이다.
  • 제네릭 varargs 매개변수는 타입 안전하지는 않지만, 허용된다.
  • 메서드에 제네릭(혹은 매개변수화된) varargs 매개변수를 사용하고자 한다면, 먼저 그 메서드가 타입 안전한지 확인한 다음 @SafeVarargs 애너테이션을 달아 사용하는데 불편함이 없게끔 하자.
  • Link : jyami.tistory.com/96
 

[Effective Java] item32. 제네릭과 가변인수를 함께 쓸 때는 신중하라

1. 가변인수와 제네릭을 함께 사용할 때의 헛점 가변인수 메서드를 호출하면 가변인수를 담기위한 배열이 자동으로 하나 만들어진다. 내부로 감춰야했을 배열을 클라이언트에 노출해서 문제가

jyami.tistory.com

 

item33. 타입 안전 이종 컨테이너를 고려하라

  • 컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다.
  • 하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다.
  • 타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런식으로 쓰이는 Class 객체를 타입 토큰이라 한다.
  • 또한, 직접 구현한 키 타입도 쓸 수 있다.
  • 데이터베이스 행(컨테이너)을 표현한 DatabaseRow 타입에는 제네릭타입인 Column<T>를 키로 쓸 수 있다.
  • Link : jyami.tistory.com/97
 

[Effective Java] item33. 타입 안전 이종 컨테이너를 고려하라

1. 타입 안전 이종 컨테이너 패턴 매개변수화 되는 대상은 원소가 아닌 컨테이너 자신이다 Set 가 있을 때 매개변수화 되는 것은 Integer 가 아니라 List 이다. 하나의 컨테이너에서 매��

jyami.tistory.com

 

댓글

Comments