해당 포스트는 "Effective Java" 책의 내용을 요약한 것이다.
※ int 상수 대신 enum을 사용하라.
: enum은 열거 상수별로 하나의 객체를 public static final 필드 형태로 제공하는 것이다. enum은 자료형의 개체 수를 엄격히 통제된다. 싱글턴 패턴을 일반화 한것으로 싱글턴 패턴은 본질적으로 보면 열거 상수가 하나뿐인 enum과 같다. 다음은 enum의 한 예이다.
enum Operation { PLUS, MINUS, TIMES, DIVIDE; 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 문을 사용했다. switch문은 새로운 enum 상수를 추가하고 case문을 추가 안해도 컴파일이 된다. 이렇게 switch문은 깨지기 쉬운 오류이다. 따라서 다른 방법으로 enum 자료형에 추상 메서드를 선언하고 각 상수별 클래스 몸체 안에 실제 메서드를 재정의 하는 방식(상수별 메서드 구현)을 사용해야 한다.
enum Operation { PLUS("+"){ double apply(double x, double y){return x+y;} }, MINUS("-"){ double apply(double x, double y){return x-y;} }, TIMES("*"){ double apply(double x, double y){return x*y;} }, DIVIDE("/"){ 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;} abstract double apply(double x, double y); }
위와 같이 코딩하면 상수를 추가해도 apply 메서드 구현을 잊을 가능성도 없고 잊더라도 컴파일러가 오류를 낸다. 위와 같은 상수별 메서드 구현의 단점은 enum 상수끼리 공유하는 코드를 만들기가 힘들다. 다음은 급여 계산 코드인데 주중과 주말의 초과근무 수당을 주는 방식이 달라 switch 문으로 주말과 주중을 구분한다. 그리고 enum 상수끼리 코드를 공유해야 되서 switch문을 활용했다.
enum PayrollDay{ MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY; private static final int HOURS_PER_SHIFT=0; double pay(double hoursWorked, double payRate){ double basePay = hoursWorked*payRate; double overtimePay; switch(this){ case SATURDAY: case SUNDAY: overtimePay = hoursWorked*payRate/2; break; default: overtimePay = hoursWorked <= HOURS_PER_SHIFT ? 0 : (hoursWorked - HOURS_PER_SHIFT) * payRate /2; } return basePay+overtimePay; } }
위 코드는 비교적 간결하다. 하지만 초과근무수당 외에 휴가 수당 등 다른 급여 계산을 추가로 적용할 시 case문에 추가해야 하고 구현을 잊을 가능성 또한 높다. 이를 대안하는 방법으로 정책 enum 패턴이 있다. 정책 enum 패턴은 private로 선언된 중첩 enum 자료형을 넣고 중첩 enum을 enum 생성자의 인자로 받게 하는 것이다. 이 패턴을 사용할 경우 switch 구문을 없앨 수 있고 switch 구문보다는 복잡하지만 안전하고 유연성이 높다. 다음은 위 예제를 정책 enum 패턴으로 구현한 코드이다.
enum PayrollDay { MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WEDNESDAY(PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY), FRIDAY(PayType.WEEKDAY), SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND); private final PayType payType; PayrollDay(PayType payType){ this.payType = payType; } double pay(double hoursWorked, double payRate){ return payType.pay(hoursWorked, payRate); } private enum PayType{ WEEKDAY{ double overtimePay(double hours, double payRate){ return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT) * payRate / 2; } }, WEEKEND{ double overtimePay(double hours, double payRate){ return hours * payRate /2; } }; private static final int HOURS_PER_SHIFT = 8; abstract double overtimePay(double hrs, double payRate); double pay(double hoursWorked, double payRate){ double basePay = hoursWorked * payRate; return basePay + overtimePay(hoursWorked,payRate); } } }
enum은 int 상수와 성능 면에서 비슷하지만 enum 자료형은 메모리에 올리고 초기화하는 공간적/시간적 비용 때문에 약간 손해를 보긴 한다. 그러나 휴대전화처럼 시스템 자원 요구량에 민감한 기기에 돌릴 프로그램이 아니라면 실제로는 별 차이 없다. enum은 고정된 상수 집합이 필요할 때 사용해야 한다.
※ ordinal 대신 객체 필드를 사용하라.
public enum Ensemble { SOLO, DUET, TRIO, QUARTET, QUIINTET, SEXTET, SEPTET, OCTET, NONET, DECTET; public int numberOfMusicions() { return ordinal() + 1; } }
public enum Ensemble { SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUIINTET(5), SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8), NONET(9), DECTET(10), TRIPLE_QUARTET(12); private final int numberOfMusicions; Ensemble(int size) { this.numberOfMusicions = size; } public int numberOfMusicions() { return numberOfMusicions; } }
※ 비트 필드(bit field) 대신 EnumSet을 사용하라.
public class Text{ public static final int STYLE_BOLD = 1; public static final int STYLE_ITALIC = 2; public static final int STYLE_UNDERLINE = 4; public static final int STYLE_STRIKETHROUGH = 8; public void applyStyles(int styles){ ... } } text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
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));
※ ordinal을 배열 첨자로 사용하는 대신 EnumMap을 이용하라.
다음과 같은 클래스가 있다고 가정하자.
class Herb { enum Type { ANNUAL, PERENNIAL, BIENNIAL } final String name; final Type type; public Herb(String name, Type type) { this.name = name; this.type = type; } }
이 허브들을 품종별로 나열해야 한다고 해보자. 품종별로 Set 객체를 만든 후 각 객체에 맞는 허브를 넣으면 된다.
Herb[] garden = ... ; Set<Herb>[] herbsByType = (Set<Herb>[]) new Set[Herb.Type.values().length]; for(int i=0; i<herbsByType.length; i++) { herbsByType[i] = new HashSet<Herb>(); } for(Herb h : garden) { herbsByType[h.type.ordinal()].add(h); }
위에서 ordinal 인덱스를 이용해 배열을 참조하고 있다. orinal 값은 enum 상수가 변경될 시 변경될 가능성이 높다. 또한 잘못 참조하게 될 경우 배열 범위를 넘어서는 결과도 나올 수 있다. 이러한 단점을 해결하기 위해 EnumMap을 사용하면 된다. EnumMap은 enum 상수를 키로 사용할 목적으로 설계되어 성능이 아주 우수하다.
Map<Herb.Type, Set<Herb>> herbByType = new EnumMap<Herb.Type, Set<Herb>(Herb.Type.class); for (Herb.Type t : Herb.Type.values()) { herbByType.put(t, new HashSet<Herb>()); } for(Herb h : garden) { herbByType.get(h.type).add(h); }
참고로 EnumMap 생성자는 키의 자료형을 나타내는 Class 객체를 인자로 받는다.
또 다른 ordinal을 사용하는 코드를 보자. 다음 코드는 상태 변화 정보를 표현하는 예이다.
public enum Phase { SOLID, LIQUID, GAS; public enum Transition { MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT; //아래 배열의 행은 상전이 이전 enum, 열은 상전이 이후 enum를 나타냄 private static final Transition[][] Transitions = { {null, MELT, SUBLIME}, {FREEZE, null, BOIL}, {DEPOSIT, CONDENSE, null} }; public static Transition from(Phase src, Phase dst) { return Transitions[src.ordinal()][dst.ordinal()]; } } }
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 src; private final Phase dst; private Transition(Phase src, Phase dst) { this.src = src; this.dst = dst; } private static final Map<Phase, Map<Phase, Transition>> m = new EnumMap<Phase, Map<Phase, Transition>>(Phase.class); static { for (Phase p : Phase.values()) { m.put(p, new EnumMap<Phase, Transition>(Phase.class)); } for(Transition trans : Transition.values()) { m.get(trans.src).put(trans.dst, trans); } } public static Transition from(Phase src, Phase dst) { return m.get(src).get(dst); } } }
※ Override 어노테이션은 일관되게 사용하라.
Override 어노테이션을 사용하면 심각한 오류를 미리 막을 수 있다. 상위 클래스를 상속받아 재정의하는 모든 메서드에는 무조건 Override 어노테이션을 사용해야 한다. 단, abstract 추상 클래스를 상속 받아 재정의하는 메서드에 대해서는 Override 어노테이션을 꼭 붙일 필요는 없다.
'자바 > 자바 성능' 카테고리의 다른 글
원칙(유효범위, for-each, float/double) (0) | 2017.07.10 |
---|---|
메서드(인자 유효성, 방어적 복사본, 오버로딩, 주석) (0) | 2017.07.10 |
제네릭 주의할 점(무인자 자료형, 무점검 경고, 제네릭 자료형/메서드) (0) | 2017.07.08 |
함수 객체 (0) | 2017.07.07 |
상속(extends)와 구성(composition), 인터페이스 (0) | 2017.07.07 |