-
EnumBackEnd/자바 2021. 1. 31. 09:38
Enum 은 열거형(enumerated type)이라고 부른다. 열거형은 서로 연관된 상수들의 집합이라고 할 수 있다.
Enum 은 class 선언 자리에 enum 을 대신 넣어 아래와 같이 만들 수 있다.
public enum Fruits { APPLE,PEACH,BANANA; private Fruits(){ System.out.println("Fruits constructor"); } }
Enum 생성자는 private 으로만 선언할 수 있으며, 변수를 선언한 만큼 생성자가 돌아가 아래와 같은 결과가 나온다.
public class Test { public static void main(String[] args) { Fruits type = Fruits.APPLE; } } ---------------> //Fruits constructor //Fruits constructor //Fruits constructor
만약 BANANA 없이 APPLE, PEACH 만 선언하면 console 창에 Fruits constructor 가 두 번만 찍히는데 그 이유는 다음 코드가 아래와 동일하기 때문이다.
class Fruit{ public static final Fruit APPLE = new Fruit(); public static final Fruit PEACH = new Fruit(); public static final Fruit BANANA = new Fruit(); private Fruit(){} }
상수 하나 당 인스턴스 하나를 만들어 public static final 필드로 공개한다.
클라이언트가 직접 인스턴스를 생성하거나 확장할 수 없으니 열거 타입 선언으로 만들어진 인스턴스들은 딱 하나씩만 존재함이 보장된다. 싱글톤은 원소가 하나뿐인 열거타입이라 할 수 있고, 거꾸로 열거타입은 싱글톤을 일반화한 형태라고 볼 수 있다.
Fruit 이라는 클래스 타입의 변수 APPLE , PEACH, BANANA 를 만든다.
그래서 생성 후 APPLE 을 스트링 비교를 하면 false 가 나온다.
ordinal 은 변수의 순서로 APPLE 은 0이, BANANA 는 2가 나온다.
하지만 ordinal 은 쓸 일이 거의 없고 이 메서드를 함부로 이용하면, 상황에 따라 코드가 지저분해지고 실용성이 떨어진다. 이 메서드는 EnumSet 과 EnumMap 같이 열거 타입 기반의 범용 자료구조에 쓸 목적으로 설계되었다.
public class Test { public static void main(String[] args) { Fruits type = Fruits.APPLE; Fruits type2 = Fruits.BANANA; System.out.println(type.equals("APPLE")); //false System.out.println(type2.ordinal()); //2 } }
열거타입 상수 각각을 특정 데이터와 연결지으려면 생성자의 매개변수를 통해
데이터를 받아 인스턴스 필드에 저장하면 된다.
public enum Fruits { APPLE("red"),PEACH("peach"),BANANA("yellow"); public String color; private Fruits(String color){ this.color = color; System.out.println("Fruits constructor - " + this + ", color = " + this.color); } String getColor() { return this.color; } }
public class Test { public static void main(String[] args) { Fruits type = Fruits.APPLE; System.out.println(type.color); //red System.out.println(type.getColor()); //red } }
enum 안에 모든 내용을 배열로 가져오는 .values() 와 이름으로 상수를 찾는 valueOf(String arg)
public class Test { public static void main(String[] args) { for(Fruits f : Fruits.values() ) { System.out.println(f); //APPLE, PEACH , BANANA } Fruits type2 = Fruits.valueOf("PEACH"); System.out.println(type2); //PEACH System.out.println(type2.getColor()); //pink } }
※ 왜 이런 enum 타입을 써야하는 걸까? (이펙티브 자바 6장)
public static final int APPLE_FUJI = 0; public static final int APPLE_PIPPIN = 1; public static final int APPLE_GRANNY = 2; public static final int ORANGE_NAVEL = 0; public static final int ORANGE_TEMPLE = 1; public static final int ORANGE_BLOOD = 2;
이런 정수 열거 패턴 기법에는 1. 타입 안전을 보장할 방법이 없으며, 2. 표현력이 좋지 않다.
오렌지를 건네야 하는 메서드에 사과를 보내고 동등(==) 연산자로 비교해도 컴파일러는 아무 경고 메세지를 보내지 않는다.
또 3. 정수 열거 패턴을 사용한 프로그램은 깨지기 쉽다.
평범한 상수를 나열한 것 뿐이라 컴파일하면 그 값이 클라이언트에 그대로 새겨지고 값이 바뀌면 클라이언트도 반드시 다시 컴파일 해야한다.
정수 상수는 문자열로 출력하기 다소 까다롭고, 단지 숫자로만 보여 의미를 알기도 어려우며, 상수가 몇 개인지 알기도 어렵다.
문자열 열거 패턴은 의미를 알기 쉽지만, 문자열 값을 하드코딩해야하고, 오타가 있어도 컴파일러는 확인할 길이 없다.
문자열 비교에 따른 성능저하도 따라온다.
public enum Apple {FUJI, PIPIN, GRANNY} public enum Orange {NAVEL, TEMPLE, BLOOD}
열거 타입은 컴파일타임 타입 안정성을 보장한다.
Apple 열거타입을 매개변수로 받는 메서드를 선언하면, 건네받은 참조는 Apple 의 세가지 값 중 하나가 분명하다.
타입이 다른 열거 타입 변수에 할당하려하거나 다른 열거 타입끼리 == 연산자로 비교하려는 꼴이기 때문이다.
열거타입에는 각자의 이름공간이 있어 이름이 같은 상수도 평화롭게 공존하며, 열거타입에 새로운 상수를 추가하거나 순서를 바꿔도 다시 컴파일하지 않아도 된다.
열거타입에는 어떤 메서드도 추가 할 수 있다.
가장 단순하게는 상수 모음이지만, 실제로는 클래스이므로 고차원의 추상 개념 하나를 완벽히 표현해 낼 수도 있다.
위에 APPLE,PEACH,BANANA 에 color 는 각각의 상수에 다른 데이터를 연결하는 예시였다.
더 나아가 상수마다 동작이 달라져야 하는 상황도 있을 것이다.
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); } }
위 코드에서는 만약 상수가 추가되면 apply 메소드 내 case 문도 함께 추가해야한다.
만약 깜박한다면 런타임에 new AssertionError 가 발생할 것이다.
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); }
상수 선언 바로 옆에 각각 메서드를 선언하면 잊기 어렵고, abstract apply 메소드로도 선언했기 때문에 만약 어떤 상수에서 apply 를 구현하지 않았다면 컴파일 에러가 난다.
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); private String symbol; private Operation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } }
위와 같이 데이터를 연결할 수도 있다.
toString 을 symbol 을 return 해 아래와 같이 쉽게 출력할 수도 있다. (op 가 symbol 이 출력됨)
public class Test1 { public static void main(String[] args) { Operation plus = Operation.PLUS; double result = plus.apply(30, 40); System.out.println(result); //70 double x = 10.5; double y = 20.5; for(Operation op : Operation.values()) { System.out.printf("%f %s %f = %f%n ",x,op,y,op.apply(x, y)); //10.500000 + 20.500000 = 31.000000 } } }
반대로 String 을 받아 enum 으로 바꿀 수도 있다.
이렇게 String symbol 을 받아 enum 안에 있는 상수를 Optional 로 감싸 돌려준다. 없더라도 NPE 이 나지 않도록 Optional 로 감싸준 것이다.
private static final Map<String,Operation> stringToEnum = Stream.of(values()).collect(Collectors.toMap(Object::toString,e->e)); //{*=*, +=+, -=-, /=/} //key : * = value : * public static Optional<Operation> fromString(String symbol){ return Optional.ofNullable(stringToEnum.get(symbol)); }
자바 8 이전에는 빈 해시맵을 만든 다음 values 가 반환한 배열을 순회하며 {문자열, 열거타입상수} 쌍을 맵에 추가했을 것이다. 물론 지금도 이렇게 해도 된다.
하지만 열거 타입 상수는 생성자에서 자신의 인스턴스를 맵에 추가할 수 없다. 이렇게 하려면 컴파일 오류가 나는데 ,
만약 이를 허용했다면 런타임에 NPE 가 발생했을 것이다.
열거 타입의 정적 필드 중 열거 타입의 생성자에서 접근할 수 있는 것은 상수 변수뿐이다.
(아이템24: 멤버클래스는 되도록 static 으로 만들어라.)
열거 타입 생성자가 실행되는 시점에는 정적 필드들이 아직 초기화 되기 전이라, 자기 자신을 추가하지 못하게 하는 제약이 꼭 필요하다. 이 제약의 특수한 예로 , 열거타입 생성자에서 다른 타입의 다른 상수에도 접근 할 수 없다.
1. 빈 해시맵에 {문자열, 열거타입상수} 추가하기
public Map<String,Operation> map = new HashMap<String,Operation>(); private Operation(String symbol) { this.symbol = symbol; map.put(symbol, this); } public Map<String,Operation> getMap(){ return map; }
Operation plus = Operation.PLUS; System.out.println(plus.getMap());
2. 생성자에서 자신의 인스턴스를 맵에 추가할 수 없다
열거 타입의 정적 필드 중 열거 타입의 생성자에서 접근할 수 있는 것은 상수 변수뿐이다.
만약 map 이 static 이라면 생성자에서 접근할 수 없다.
열거 타입 생성자가 실행되는 시점에는 정적 필드들이 아직 초기화 되기 전이라,
자기 자신을 추가하지 못하게 하는 제약이 꼭 필요하다.
(열거타입이 상수들을 먼저 생성하고난 다음 정적 필드를 초기화하기 때문에.)
컴파일 에러
public static Map<String,Operation> map = new HashMap<String,Operation>(); private Operation(String symbol) { this.symbol = symbol; map.put(symbol, this); //Cannot refer to the static enum field Operation.map within an initializer }
NPE 발생
public static Map<String,Operation> map = new HashMap<String,Operation>(); private Operation(String symbol) { this.symbol = symbol; putString(symbol,this); } public void putString(String symbol, Operation operation) { map.put(symbol,operation); }
Operation plus = Operation.PLUS;
상수별 메서드 구현에는 열거 타입 상수끼리 코드를 공유하기 어렵다는 단점이 있다.
예) 급여명세서 요일 열거 타입
직원의 시간당 기본 임금,
분 단위 일당 계산 메서드,
주중 오버타임 잔업 수당,
주말 잔업 수당;
public 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: overTimePay = basePay / 2; break; default : overTimePay = minutesWorked <= MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2; } return basePay + overTimePay; } }
이 코드는 휴가와 같은 새로운 값을 열거 타입에 추가하려면 그 값을 처리하는 case 문을 넣어야한다.
상수별 메서드 구현으로 급여를 정확히 계산하기 위해서는
1. 잔업 수당을 계산하는 코드를 모든 상수에 중복해서 넣거나,
2. 계산 코드를 평일용과 주말용으로 나눠 각 상수에서 필요한 메서드를 호출한다.
하지만 두 방식 모두 가독성이 떨어지고 오류가 발생할 가능성이 높아진다.
가장 깔끔한 방법은 새로운 상수를 추가할 때 잔업수당 '전략'을 선택하도록 하는 것이다.
잔업수당 계산을 private 중첩 열거 타입으로 옮기고 PayrollDay 열거 타입 생성자에서 이 중 적절한 것을 선택한다.
그러면 PayrollDay 열거 타입은 잔업수당 계산을 전략 열거 타입에 위임하고, switch 문이나 상수별 메서드 구현이 필요없어진다. 이 패턴은 switch 보다 복잡하지만 더 안전하고 유연하다.
public 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; } 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; } }; abstract int overtimePay(int minsWorked,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); } } }
대부분의 경우 열거 타입의 성능은 정수 상수와 별반 다르지 않다.
열거 타입을 메모리에 올리는 공간과 초기화하는 시간이 들긴 하지만 체감될 정도는 아니다.
필요한 원소를 컴파일 타임에 다 알 수 있는 상수 집합이라는 항상 열거 타입을 사용하자.
ordinal 인덱싱 대신 EnumMap 을 사용하라
public 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; } }
public class Test1 { public static void main(String[] args) { Plant[] garden = { new Plant("바질", LifeCycle.ANNUAL), new Plant("캐러웨이", LifeCycle.BIENNIAL), new Plant("딜", LifeCycle.ANNUAL), new Plant("라벤더", LifeCycle.PERENNIAL), new Plant("파슬리", LifeCycle.BIENNIAL), new Plant("로즈마리", LifeCycle.PERENNIAL) }; Set<Plant>[] plantsLife = new Set[Plant.LifeCycle.values().length]; for(int i=0;i<plantsLife.length;i++) { plantsLife[i] = new HashSet<>(); } for(Plant p : garden) { plantsLife[p.lifeCycle.ordinal()].add(p); } for(int i=0;i<plantsLife.length;i++) { System.out.printf("%s : %s%n",Plant.LifeCycle.values()[i] , plantsLife[i]); } //ANNUAL : [딜, 바질] //PERENNIAL : [라벤더, 로즈마리] //BIENNIAL : [파슬리, 캐러웨이] } }
위에 ordinal 메서드를 이용한 방식은
배열은 제네릭과 호환되지 않으니 비검사 형변환을 수행해야하고, 깔끔히 컴파일 되지 않을 것이다.
배열은 각 인덱스의 의미를 모르니 출력결과에 직접 레이블을 달아야 한다.
정확한 정수값을 사용한다는 것을 보장해야한다.
아래와 같이 EnumMap 으로 위 문제를 해결할 수 있다.
public class Test1 { public static void main(String[] args) { Plant[] garden = { new Plant("바질", LifeCycle.ANNUAL), new Plant("캐러웨이", LifeCycle.BIENNIAL), new Plant("딜", LifeCycle.ANNUAL), new Plant("라벤더", LifeCycle.PERENNIAL), new Plant("파슬리", LifeCycle.BIENNIAL), new Plant("로즈마리", LifeCycle.PERENNIAL) }; Map<Plant.LifeCycle, Set<Plant>> map = new EnumMap<>(Plant.LifeCycle.class); for(Plant.LifeCycle lc : Plant.LifeCycle.values()) { map.put(lc, new HashSet<>()); // key : ANNUAL, values : new * 3 } for(Plant p : garden) { map.get(p.lifeCycle).add(p); } System.out.println(map); //{ANNUAL=[딜, 바질], PERENNIAL=[로즈마리, 라벤더], BIENNIAL=[캐러웨이, 파슬리]} } }
비트 필드 대신 EnumSet 을 사용하라
public class Text { public static final int STYLE_BOLD = 1<<0; public static final int STYLE_ITALIC = 1<<1; public static final int STYLE_UNDERLINE = 1<<2; public static final int STYLE_STRIKETHROUGH = 1<<3; public void applyStyles(int styles) { ... } }
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
비트 필드를 사용하면 비트별 연산을 통해 합집합과 교집합같은 집합 연산을 효율적으로 수행 할 수 있다.
하지만 비트 필드는 정수 열거 상수의 단점을 그대로 지니며, 훤씬 해석하기 어렵다.
비트 필드 하나에 녹아있는 모든 원소를 순회하기도 까다롭다. 마지막으로, 최재 몇 비트가 필요한지 API 작성 시 미리 예측해 적절한 타입(int나 long) 을 선택해야한다.
public enum Style {BOLD,ITALIC,UNDERLINE,STRIKETHROUGH; } public void applyStyles(Set<Style> styles) { }
text.applyStyles(EnumSet.of(Style.BOLD,Style.ITALIC));
EnumSet 의 단점은 불변으로 만들 수 없다는 것이다.
불변으로 만들기 위해서는
명확성과 성능이 조금 떨어지지만 Collections.unmodifiableSet 으로 EnumSet 을 감싸 사용할 수 있다.
반응형'BackEnd > 자바' 카테고리의 다른 글
tdd (0) 2021.05.11 상속보다는 컴포지션을 사용하라 > 이펙티브 자바 아이템18 (0) 2021.04.26 예외처리 (0) 2021.01.16 인터페이스 (0) 2021.01.09 자바 기초 (패키지, import , classpath, 접근제어자) (0) 2021.01.01