EffectiveJava

[EffectiveJava] 6장 열거 타입과 애너테이션

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

// 34-1 정수 열거 패턴 - 상당히 취약하다!
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; 
// 34-2 가장 단순한 열거 타입
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }
// 34-3 데이터와 메서드를 갖는 열거 타입
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
	}
}
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.println("Weight on %s is %f\\n", p, p.surfaceWeight(mass));
		}
	}
}
// 34-4 값에 따라 분기하는 열거 타입 - 이대로 만족하는가?
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);
    }
}
// 34-5 상수별 메서드 구현을 활용한 열거 타입
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);
}
// 34-6 상수별 클래스 몸체(class body)와 데이터를 사용한 열거 타입
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);
}
public static void main(String[] args) {
      double x = Double.parseDouble(args[0]);
      double y = Double.parseDouble(args[1]);
      for (OperationWithToString op : OperationWithToString.values()) {
          System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
      }
}
// 34-7 열거 타입용 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));
}
// 34-8 값에 따라 분기하여 코드를 공유하는 열거 타입 - 좋은 방법인가?
enum PayrollDay {
	MONDAY, TUSEDAY, 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;
	}
}
// 34-9 전략 열거 타입 패턴
enum PayrollDay {
	MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY),
  THURSDAY(WEEKDAY), FRIDAY(WEEKDAY),
  SATURDAY(PayType.WEEKEND), SUNDAY(PayType.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);
		}
	}
}
// 34-10 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);
    }
}

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

// 35-1 ordinal 잘못 사용한 예 - 따라 하지 말것
public enum Ensemble {
	SOLO, DUET, TRIO, QUARTER, 
	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; }
}

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

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

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

text.applyStyle(STYLE_BOLD | STYLE_ITALIC);
// 36-2 EnumSet 비트 필드를 대체하는 현대적 기법
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));
	}
}
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

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

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 this.name;
  }
}
// 37-1 ordinal()을 배열 인덱스로 사용 - 따라하지 말것
Set<Plant>[] plantByLifeCycle = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
for (int i = 0; i < plantByLifeCycle.length; i++) {
    plantByLifeCycle[i] = new HashSet<>();
}
for (Plant p : garden) { // garden은 없는 구현체, Plant를 담은 객체
    plantByLifeCycle[p.lifeCycle.ordinal()].add(p);
}
// 결과 출력
for (int i = 0; i < plantByLifeCycle.length; i++) {
    System.out.printf("%s: %s %n", Plant.LifeCycle.values()[i], plantByLifeCycle[i]);
}
// 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);
// {ANNUAL=[바질, 딜], PERENNIAL=[로즈마리, 라벤더], BIENNIAL=[캐러웨이, 파슬리]}
```java
// 37-3 스트림을 사용한 코드 1- EnumMap을 사용하지 않는다!
System.out.println(Arrays.stream(garden)
                .collect(groupingBy(p -> p.lifeCycle)));
// 37-4 스트림을 사용한 코드 2- EnumMap을 이용해 데이터와 열거 타입을 매핑했다.
System.out.println(Arrays.stream(garden)
                .collect(groupingBy(p -> p.lifeCycle,
                        () -> new EnumMap<>(LifeCycle.class), toSet())));
// 37-5 배열들의 배열의 인덱스에 ordinal()을 사용 - 따라하지 말것!
public enum Phase {
    SOLID, LIQUID, GAS;

    public enum Transition {
        MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;

        // 행은 from의 ordinal을, 열은 to의 ordinal을 인덱스로 쓴다.
        private static final Transition[][] TRANSITIONS = {
                {null, MELT, SUBLIME},
                {FREEZE, null, BOIL},
                {DEPOSIT, CONDENSE, null}
        };
        
        // 한 상태에서 다른 상태로의 전이를 반환한다.
        public static Transition from(Phase from, Phase to) {
            return TRANSITIONS[from.ordinal()][to.ordinal()];
        }
    }
}
// 37-6 중첩 EnumMap으로 데이터와 열거 타입 쌍을 연결했다.
import java.util.EnumMap;
import java.util.Map;
import java.util.stream.Stream;

import static java.util.stream.Collectors.groupingBy;

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 final Phase from;
        private final Phase to;

        Transition(Phase from, Phase to) {
            this.from = from;
            this.to = to;
        }

        // 상전이 맵을 초기화한다.
        public static final Map<Phase, Map<Phase, Transition>> 
								m = Stream.of(values()).collect(groupingBy(t -> t.from, 
												() -> new EnumMap<>(Phase.class),
                        toMap(t -> t.to, //key-mapper
                                t -> t,  //value-mapper
                                (x, y) -> y, //merge-function
                                () -> new EnumMap<>(Phase.class))));

        public static Transition from(Phase from, Phase to) {
            return m.get(from).get(to);
        }
}
// 37-7 EnumMap 버전에 새로운 상태 추가하기

public enum Phase {
    SOLID, LIQUID, GAS, PLASMA;

    public enum Transition {
        MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
        BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
        SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
				IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS);

        ... // 나머지 코드는 그대로다.
}

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

// 38-1 인터페이스를 이용해 확장 가능 열거 타입을 흉내 냈다.
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;
    }
}
// 38-2 확장 가능 열거 타입
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;
    }
}
//38-2 확장 가능 열거 타입
public static void main(String[] args) {
		double x = Double.parseDouble(args[0]);
		double y = Double.parseDouble(args[1]);
		test(ExtendedOperation.class,x,y);
}

// 1. class 리터럴은 열거타입이면서 Operation 하위 타입 (opEnumType)
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));
}
public static void main(String[] args) {
		double x = Double.parseDouble(args[0]);
		double y = Double.parseDouble(args[1]);
		test(Arrays.asList(ExtendedOperation.values()),x,y);
}
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));
}

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

// 39-1 마커(marker) 애너테이션 타입 선언
import java.lang.annotation.*;

/**
* 테스트 메서드임을 선언하는 애너테이션이다.
* 매개변수 없는 정적 메서드 전용이다.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
// 39-2 마커 애너테이션을 사용한 프로그램 예
public class Sample {
    @Test
    public static void m1() { }        // 성공해야 한다.
    public static void m2() { }
    @Test public static void m3() {    // 실패해야 한다.
        throw new RuntimeException("실패");
    }
    public static void m4() { }  
    @Test public void m5() { }   // 잘못 사용한 예: 정적 메서드가 아니다.
    public static void m6() { }
    @Test public static void m7() {    // 실패해야 한다.
        throw new RuntimeException("실패");
    }
    public static void m8() { }
}
// 39-3 마커 애너테이션을 처리하는 프로그램
import java.lang.reflect.*;

public class RunTests {
    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);
    }
}
// 39-4 매개변수 하나를 받는 애너테이션 타입
import java.lang.annotation.*;

/**
 * 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    **Class<? extends Throwable> value();
}**
// 39-5 매개변수 하나짜리 애너테이션을 사용한 프로그램 (241쪽)
public class Sample2 {
    **@ExceptionTest(ArithmeticException.class)**
    public static void m1() {  // 성공해야 한다.
        int i = 0;
        i = i / i;
    }
    **@ExceptionTest(ArithmeticException.class)**
    public static void m2() {  // 실패해야 한다. (다른 예외 발생)
        int[] a = new int[0];
        int i = a[1];
    }
    **@ExceptionTest(ArithmeticException.class)**
    public static void m3() { }  // 실패해야 한다. (예외가 발생하지 않음)
}
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);
      }
  }
// 39-6 배열 매개변수를 받는 애너테이션 타입 (242쪽)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Exception>**[]** value();
}
// 39-7 배열 매개변수를 받는 애너테이션을 사용하는 코드 (242-243쪽)
@ExceptionTest(**{ IndexOutOfBoundsException.class,
                 NullPointerException.class }**)
public static void doublyBad() {   // 성공해야 한다.
    List<String> list = new ArrayList<>();

    // 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나
    // NullPointerException을 던질 수 있다.
    list.addAll(5, null);
}
// 배열 매개변수를 받는 애너테이션을 처리하는 코드 (243쪽)
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);**
    }
}
// 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() {
    ...
}
// 39-10 반복 가능 애너테이션 다루기 (244-245쪽)
**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);
    }
}

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

// 40-1 영어 알파벳 2개로 구성된 문자열(바이그램)을 표현하는 클래스 - 버그를 찾아보자. (246쪽)
public class Bigram {
    private final char first;
    private final char second;

    public Bigram(char first, char second) {
        this.first  = first;
        this.second = second;
    }

    public boolean equals(Bigram b) {
        return b.first == first && b.second == second;
    }

    public int hashCode() {
        return 31 * first + second;
    }

    public static void main(String[] args) {
        Set<Bigram> s = new HashSet<>();
        for (int i = 0; i < 10; i++)
            for (char ch = 'a'; ch <= 'z'; ch++)
                s.add(new Bigram(ch, ch));
        System.out.println(s.size());
    }
}
@Override
  public boolean equals(Bigram b) {
      return b.first == first && b.second == second;
  }
@Override public boolean equals(Object o) {
        if (!(o instanceof Bigram2))
            return false;
        Bigram2 b = (Bigram2) o;
        return b.first == first && b.second == second;
}

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

public interface Serializable{
}
else if (obj instanceof Serializable) {
  writeOrdinaryObject(obj, desc, unshared);
}
Class<?> testClass = Class.forName(args[0]);
for (Method m : testClass.getDeclaredMethods()) {
    if (m.isAnnotationPresent(Test.class)) {...}
}

Leave a Reply