본문 바로가기

Java

6. Enums and Annotations - Effective Java 3th

자바에는 enum과 annotation이라는 것이 있다. 이번 챕터에서는 이 두가지를 잘 사용하는 방법에 대해 살펴보자.

 

Item 34 : int 상수 대신 enum을 사용하자

기본적인 상수만 있는 enum 말고 동작이 필요한 Enum을 만든다면?

가장 기본적인 구현방법은 아래와 같을 것이다. 

// Enum type that switches on its own value - questionable
public enum Operation {
  PLUS, MINUS, TIMES, DIVIDE;
  // Do the arithmetic operation represented by this constant
  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("Unknown op: " + this);
  }
}

하지만 해당 코드는 enum에 타입이 추가되었을 때 switch 문에 해당되는 액션을 추가하지 않아도 컴파일 에러가 나지 않는다! 상당히 위험한 코드다.

 

Constant-specific method implementation ( 상수별 메서드 구현 )

추상 메서드로 이렇게 선언해놓으면 강제화 할 수 있다.

// Enum type with constant-specific method implementations
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);
}
// Implementing a fromString method on an enum type
private static final Map<String, Operation> stringToEnum =
  Stream.of(values()).collect(
  	toMap(Object::toString, e -> e));
    
// Returns Operation for string, if any
public static Optional<Operation> fromString(String symbol) {
  return Optional.ofNullable(stringToEnum.get(symbol));
}
급여 계산 enum

평일과 주말간의 급여를 계산해주는 코드를 작성해보자. 기본적인 구현은 switch를 통해서 아래와 같이 계산을 할 수 있을 것이다.

// Enum that switches on its value to share code - questionable
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: // Weekend
        	overtimePay = basePay / 2;
        	break;
        default: // Weekday
        	overtimePay = minutesWorked <= MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
      }
    return basePay + overtimePay;
  }
}
  • 장점 : 코드가 간결하고 읽기 좋다
  • 단점 : 유지보수에 좋지 않다
    • Vacataion day가 추가된다면? 개발자가 실수로 switch 문에 추가를 하지 않아도 컴파일 에러가 나지 않기 때문에 Vacation day 급여도 평일 급여로 계산이 될 것이다.
    • 결국 가독성은 점점떨어지고 실수할 확률은 점점 늘어날 것이다.
constant-specific method 대신 abstract method 사용하기

우리가 위에서 생겼던 문제는 enum constant ( 날짜 종류 ) 가 추가되어도 overtime pay 계산하는 함수를 강제하지 못했기 때문에 발생하였다. 계산과 관련된 부분은 PayType inner enum에게 책임을 준다면 된다.

// The strategy enum pattern
enum PayrollDay {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY(PayType.WEEKEND),
    SUNDAY(PayType.WEEKEND);
    private final PayType payType;


    PayrollDay(PayType payType) { this.payType = payType; }


    PayrollDay() { this(PayType.WEEKDAY); } // Default


    int pay(int minutesWorked, int payRate) {
        return payType.pay(minutesWorked, payRate);
    }


    // The strategy enum type
    private 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;
            }
        }
    }
}
enum에 있는 constant-specific한 행동을 할 때는 switch를 사용하는게 더 좋다. 

 

// Switch on an enum to simulate a missing method
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("Unknown op: " + op);
  }
}

위의 코드도 필요할 수도 있지만 enum에 담을만한 가치가 있지는 않다.

 

Enum 언제 사용하는게 좋은데?
  • 개발자가 compile 시점에 constants에 대해 알아야 할 때

Item35 : ordinal 함수 대신 instance field를 사용하라

// Abuse of ordinal to derive an associated value - DON'T DO THIS
public enum Ensemble {
  SOLO, DUET, TRIO, QUARTET, QUINTET,
  SEXTET, SEPTET, OCTET, NONET, DECTET;
  public int numberOfMusicians() { return ordinal() + 1; }
}
  • 문제점 1 : 12명인 enum을 추가하려면 의미 없는 11명 enum도 생성해야 함
  • 문제점 2 : 만약에 순서를 실수로 바꾼다면??? 컴파일 에러 없이 프로그램이 다 깨짐
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; }
}

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

// Bit field enumeration constants - OBSOLETE!
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
  
  // Parameter is bitwise OR of zero or more STYLE_ constants
  public void applyStyles(int styles) { ... }
}

비트를 이용하여 처리하면 합집합으로 사용하기 좋다

text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
  • 해석하기 어려움
  • 비트 수도 미리 예측 해야함
// EnumSet - a modern replacement for bit fields
public class Text {
  public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
 
  // Any Set could be passed in, but EnumSet is clearly best
  public void applyStyles(Set<Style> styles) { ... }
}
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

EnumSet으로 받아도 되지만 일반적으로 interface로 받는게 좋은 습관이다. 그리고 EnumSet의 내부는 비트 벡터로 구현되어 있다. 원소가 총 64개 이하라면, long 변수 하나로 표현이 가능하며 비트 필드에 비견되는 성능을 보여준다.

 

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

- 방법 1 : 생애주기별로 for문을 돌면서 식물을 해당 집합에 넣어서 구현 & ordinal() 사용

// Using ordinal() to index into an array - DON'T DO THIS!
Set<Plant>[] plantsByLifeCycle =
	(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.lifeCycle.ordinal()].add(p);
    
// Print the results
for (int i = 0; i < plantsByLifeCycle.length; i++) {
  System.out.printf("%s: %s%n",
  Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
}
  • 문제점 1 : 배열은 generic과 호환되지 않으므로 비검사 형변환을 수행해야 함
  • 문제점 2 : i 가 정확한 정수인지 모른다!

- 방법 2 : EnumMap을 이용

 

생애주기는 이미 정해진 상수 값이므로 Map을 사용할 수 있다. 그리고 열거 타입을 key로 사용하도록 설계한 아주 빠른 EnumMap이 있다!

// Using an EnumMap to associate data with an enum
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);
  • 좋은점 1 : 짧고, 명료하고, 안전함
  • 좋은점 2 : ordinal() 과 비교해도 떨어지지 않는 성능
    -> EnumMap 내부에서도 결국 vals 라는 배열을 이용하기 때문에 성능이 비견 된다.
    -> 결국 타입 안정성 & 성능을 모두 얻어냄
 public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
    implements java.io.Serializable, Cloneable
{
	// ...
    
	 public boolean containsKey(Object key) {
        return isValidKey(key) && vals[((Enum<?>)key).ordinal()] != null;
    }
    
    // ...
}
  • 좋은점 3 : EnumMap의 생성자가 받는 키 타입의 Class 객체는 한정적 타입 토큰으로, 런타임 제네릭 타입 정보를 제공한다. ( Item 33 참고 )

- 방법 3 : Stream을 사용 ( Item 45 )

// Naive stream-based approach - unlikely to produce an EnumMap!
System.out.println(Arrays.stream(garden)
	.collect(groupingBy(p -> p.lifeCycle)));
  • 좋은점 1 : 코드가 짧아짐
  • 단점 1 : EnumMap이 아닌 고유 맵 구현체 사용 -> 공간 성능 이점 사라짐

- 방법 4 : Stream + mapFactory 사용

매개변수 3개짜리 groupingBy에서는 원하는 맵 구현체를 명시해 호출할 수 있다.

// Using a stream and an EnumMap to associate data with an enum
System.out.println(Arrays.stream(garden)
	.collect(groupingBy(p -> p.lifeCycle,
	() -> new EnumMap<>(LifeCycle.class), toSet())));
구현 상황 : 두 열거 타입 값들을 매핑해야 하는 경우 ( 상태 / 전이 )

예를 들어, 액체(LIQUID)에서 고체(SOLID)로의 전이는 응고(FREEZE)가 된다.

// Using ordinal() to index array of arrays - DON'T DO THIS!
public enum Phase {
  SOLID, LIQUID, GAS;
  
  public enum Transition {
    MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
   
    // Rows indexed by from-ordinal, cols by to-ordinal
    private static final Transition[][] TRANSITIONS = {
      { null, MELT, SUBLIME },
      { FREEZE, null, BOIL },
      { DEPOSIT, CONDENSE, null }
    };
    
    // Returns the phase transition from one phase to another
    public static Transition from(Phase from, Phase to) {
    	return TRANSITIONS[from.ordinal()][to.ordinal()];
    }
  }
}
  • 좋은점 1 : 그냥 멋저보임
  • 단점 1 : 컴파일러는 oridinal과 배열 인덱스의 관계를 알 도리가 없음
  • 단점 2 : 상태가 추가되면 null 값과 2차원 배열이 계속 늘어남

- 개선 된 방법 : EnumMap을 사용

// Using a nested EnumMap to associate data with enum pairs
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;
    }
    
    // Initialize the phase transition map
    private static final Map<Phase, Map<Phase, Transition>>
    	m = Stream.of(values()).collect(groupingBy(t -> t.from,
    		() -> new EnumMap<>(Phase.class),
    		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);
    }
  }
}
Item 38 : 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라
// Emulated extensible enum using an interface
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; }
  };
  
  ITEM 38: EMULATE EXTENSIBLE ENUMS WITH INTERFACES 177
  private final String symbol;
  
  BasicOperation(String symbol) {
  	this.symbol = symbol;
  }
  
  @Override 
  public String toString() {
 	 return symbol;
  }
}

enum 타입은 상속이 불가능 하지만, 그래도 Operation interface를 통해 API의 동작을 나타낼 수 있고, 다른 enum타입을 정의 할 수 있다!

// Emulated extension 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 = Double.parseDouble(args[0]);
  double y = Double.parseDouble(args[1]);
  test(ExtendedOperation.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));
}
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));
}
Item 39 : 명명 패턴보다 annotation을 사용하라

명명패턴의 단점

  • JUnit3때처럼 함수 이름 앞에 test가 붙어야 동작하던 시절 : 오타나면? 확인할 방법 없음
  • 별도로 보장을 해주지 않음