참고. Enum Method
ordinal() 은 열거형 상수의 순서에 따라 변경되므로, 열거형 인스턴스의 상수 필드를 사용하는 것을 권장 (아이템 35)
아이템34. 상수 대신 열거 타입을 사용하라
정수 열거 패턴 (안티 패턴)
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;
- 타입 안전을 보장할 방법이 없음
- 표현력도 좋지 않음
- 만약 오렌지(ORANGE_NAVEL)를 건네야 하는데 사과(APPLE_FUJI)를 보내고 동등 연산자(==) 비교 하더라도 컴파일러는 아무런 경고 메시지를 출력하지 않음 -> (primitive type 이기 때문에)
- 상수 값 변경될 경우, 의도한데로 동작하지 않을 수 있음 (버그 발생 가능)
단순한 열거 타입 (p209 ~ 210)
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orage { NAVEL, TEMPLE, BLOOD }
- 열거 타입(enum) 은 클래스이다 (=열거 타입의 각 인스턴스는 Reference Type, 주소 참조형 자료이다)
- 열거 타입 내에 상수 하나당 인스턴스를 만들어 public static final 필드로 공개된다
- 열거 타입 생성자는 private 이므로 사실상 final이다
- 따라서 client가 인스턴스를 직접 생성하거나 확장 할 수 없으니 열거 타입 선언으로 만들어진 인스턴스들은 딱 하나씩만 존재한다
- 열거 타입은 싱글턴을 일반화한 형태라고 볼 수 있다
- 열거 타입은 컴파일 타임에 타입 안전성(type safe)을 제공한다
- 열거 타입에는 임의 메서드/필드 추가할 수 있고, 인터페이스를 구현하게 할 수 있다
상수 하나당 아래와 같은 인스턴스 형태로 표현될 수 있다
public enum Apple {
public static final Apple FUJI = new Apple();
public static final Apple PIPPIN = new Apple();
// ..
}
public enum Orange {
public static final Orange NAVEL = new Orange();
public static final Orange TEMPLE = new Orange();
// ..
}
데이터와 메서드를 갖는 열거 타입 (p211)
열거 타입 상수 각각을 특정 데이터와 연결 지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다
enum Planet {
MERCURY (3.303e+23, 2.4397e6),
VENUS (4.869e+24, 6.0518e6),
EARTH (5.976e+24, 6.37814e6),
MARS (6.421e+23, 3.3972e6),
JUPITER (1.9e+27, 7.1492e7),
SATURN (5.688e+26, 6.0268e7),
URANUS (8.686e+25, 2.5559e7),
NEPTUNE (1.024e+26, 2.4746e7),
PLUTO (1.27e+22, 1.137e6);
private final double mass; // 질량 (단위: 킬로그램)
private final double radius; // 반지름 (단위: 미터)
private Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
}
public double mass() { return mass; }
public double radius() { return radius; }
// 중력 상수 (m^3 / kg s^2)
public static final double G = 6.67300E-11;
public double surfaceGravity() {
return G * mass / (radius * radius);
}
public double surfaceWeight(double otherMass) {
return otherMass * surfaceGravity(); // F = ma
}
}
인스턴스 별로 상수를 사용하기 위해서는 맴버 변수를 선언하고 아래와 같이 생성자 정의를 해줘야 한다. enum 클래스에 정의한 인스턴스 각각은 아래와 같이 맵핑이 된다.
열거 타입은 근복적으로 불변이라 모든 필드는 final이어야 한다. 필드를 public 으로 선언해도 되지만, private로 두고 별도의 public 접근자 메서드를 두는게 낫다. (getter method인 mass()와 radius() 참고)
public class Main {
// 지구에서 무게가 다른 행성에서 어떠한가
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
double earthWeight = Double.parseDouble(br.readLine());
double mass = earthWeight/Planet.EARTH.surfaceGravity();
StringBuilder sb = new StringBuilder();
for(Planet p : Planet.values()) {
sb.append(String.format("%s 에서의 무게는 %f%n", p, p.surfaceWeight(mass)));
}
System.out.println(sb);
}
}
[출력 결과]
MERCURY 에서의 무게는 69.885159
VENUS 에서의 무게는 167.424833
EARTH 에서의 무게는 185.000000
MARS 에서의 무게는 70.066379
JUPITER 에서의 무게는 468.153142
SATURN 에서의 무게는 197.212875
URANUS 에서의 무게는 167.448532
NEPTUNE 에서의 무게는 210.590693
PLUTO 에서의 무게는 12.371775
상수별 클래스 몸체(class body)와 데이터를 사용한 열거 타입 (p215)
enum 클래스 추상 메서드(eval) 선언 후 각 상수별 클래스 몸체(constant-specific class body), 즉 각 상수에서 자신에 맞게 재정의 해야 한다. 아래의 경우 추상 메서드를 재정의 하지 않아 컴파일 에러(compile error)가 발생하였다.
열거 타입 상수별로 필드와 메서드를 아래와 같이 정의하여 사용할 수 있다
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;
private Operation(String symbol) {
this.symbol = symbol;
}
public String getSymbol() {
return symbol;
}
// 재정의 하지 않을 경우 인스턴스명이 기본적으로 출력됨 (예 PLUS)
@Override public String toString() { return symbol; }
abstract double apply(double x, double y);
}
public class Main {
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringBuilder sb = new StringBuilder();
double x = Double.parseDouble(br.readLine());
double y = Double.parseDouble(br.readLine());
for(Operation op : Operation.values()) {
sb.append(String.format("%f %s %f = %f%n", x, op, y, op.apply(x, y)));
}
System.out.println(sb);
}
}
[출력 결과]
4.000000 + 5.000000 = 9.000000
4.000000 - 5.000000 = -1.000000
4.000000 * 5.000000 = 20.000000
4.000000 / 5.000000 = 0.800000
열거 타입용 fromString 메서드 구현하기 (p216)
- Operation 상수가 stringToEnum맵에 추가되는 시점은 열거 타입 상수 생성 후 정적 필드가 초기화 될 때이다
- 열거 타입 상수는 생성자에서 자신의 인스턴스를 맵에 추가할 수 없다. (NullPointException 발생)
- 열거 타입 생성자가 실행되는 시점에는 정적 필드들이 아직 초기화 되기 전이라, 자기 자신을 추가하지 못하게 하는 제약이 꼭 필요하다.
enum Operation {
// ..
private static final Map<String, Operation> stringToEnum = Stream.of(values()).collect(Collectors.toMap(Objects::toString, e -> e));
// 지정한 문자열에 해당하는 Operation 을 반환한다
public static Optional<Operation> fromString(String symbol) {
return Optional.ofNullable(stringToEnum.get(symbol));
}
}
전략 열거 타입 패턴 (p218)
급여명세서에 쓸 요일을 표현하는 열거타입이 있을 때, 이 열거 타입은 직원의 (시간당) 기본 임금과 그날 일한 시간(분 단위)이 주어지면 일단을 계산해주는 메서드를 갖고 있다.
이때 주중에 오버타임이 발생하면 잔업수당이 발생하고, 주말에는 무조건 잔업수당이 주어진다.
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: // 주말, waterfall 방식
overtimePay = basePay / 2;
break;
default : // 주중
overtimePay = minutesWorked <= MINS_PER_SHIFT
? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
}
return basePay + overtimePay;
}
}
- 관리 관점에서 신규 열거 타입 추가시 switch case문을 수정해야하고, 휴먼 버그 발생할 가능성이 있는 코드이다.
전략 열거 타입 패턴을 사용하게 되면 switch 문보다 복잡하지만 더 안전하고 유연하게 할 수 있다
import static basic.function.PayrollDay.PayType.*;
public 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() {
return 0;
}
// 전략 열거 타입
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;
}
};
private static final int MINS_PER_SHIFT = 8 * 60;
abstract int overtimePay(int minsWorked, int payRate);
int pay(int minsWorked, int payRate) {
int basePay = minsWorked * payRate;
return basePay * overtimePay(minsWorked, payRate);
}
}
}
- PayType에 기본 급여와 추가 수당을 옮기고 상수 값에 따라 추가 수당 계산 방식을 다르게 하였음
- 그리고 PayrollDay에서 PayType 필드 추가하여 지정함으로써 각 요일별로 전략적으로 임금 계산이 가능해짐
아이템35. ordinal 메서드 대신 인스턴스 필드를 사용하라
ordinal() : 해당 상수가 그 열거 타입에서 몇 번째 위치인지를 반환 (0부터 시작)
아래 Enum API 문서를 보면 enum을 base한 자료 구조 EnumSet, EnumMap 외에는 사용할 일이 없다고 설명하고 있다
ordinal() 순서 값을 사용하여 코드를 작성할 경우, enum 상수 선언 순서 변경시 의도치 않은 동작과 함께 유지보수를 어렵게 할 위험이 있으므로 사용하지 않을 것을 권장하고 있다
필요한 상수 값이 있다면 ordinal() 을 사용하지 말고, 상수 인스턴스 필드에 저장하여 사용하도록 하자 (명시적)
public enum Ensemble {
SOLE(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 numberOfMusicians) {
this.numberOfMusicians = numberOfMusicians;
}
public int getNumberOfMusicians() {
return numberOfMusicians;
}
}
아이템36. 비트 필드 대신 EnumSet을 사용하라
- 비트 연산을 사용해 합집합(OR)과 교집합(AND)과 같은 집합 연산을 효율적으로 수행할 수 있다
- 하지만 비트 필드는 정수 열거 상수의 단점을 그대로 지니며, 무엇보다 해석하기 훨씬 어렵다 (아래 참고)
- 그리고 API 설계시 최대 몇 비트 필요한지 파악하고 적절한 타입(보통 int나 long)을 사용해야 한다
비트 필드 열거 상수 - 구닥다리 기법 ! (p223)
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
// 매개 변수 styles는 0개이상의 STYLE_ 상수를 OR 연산한 결과값이다
public void applyStyles(int styles) { ... }
}
text.applyStyle(STYLE_BOLD | STYLE_ITALIC); // 인자로 0011(2진수) => 3 (10진수) 이 전달된다
bit shift
STYLE_BOLD = 0001(2진수)
STYLE_ITALIC = 0010(2진수)
OR ( | ) 연산시
0011(2진수) = 3
java.util.EnumSet 클래스는 열거 타입 상수의 값으로 구성된 집합을 효과적으로 표현해준다 (비트 필드를 사용할 이유 x)
public class Text {
public enum Style {BOLD, ITALIC, UNDERLINE, STRIKETHROUGH}
// 인터페이스 Set<>을 매개변수로 할당하여, 인자 전달시 구현체를 다른 걸 사용할 수 있다
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)); // 집합 전달
}
}
[출력 결과]
Applying styles [BOLD, ITALIC] to text
개인적인 해석으로는 집합을 다룰 때 비트 연산보다 EnumSet을 활용한 형태가 좀 더 현대적이라는 듯 하다.
참고
https://incheol-jung.gitbook.io/docs/study/effective-java/undefined-4/undefined-11
아이템37. ordinal 인덱싱 대신 EnumMap을 사용하라
앞서 살펴봤지만 ordinal() 메서드는 열거 상수의 순서에 따르기 때문에, 상수 선언 순서만 변경에 취약한 문제가 있어 사용하지 않을 것을 권장하고 있었다.
대안으로 Enum 상수를 키 값으로 가지는 EnumMap을 사용할 것은 권장하고 있다
public class Plant {
enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }
final String name;
final LifeCycle lifeCycle;
public Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
public String getName() {
return name;
}
public LifeCycle getLifeCycle() {
return lifeCycle;
}
@Override
public String toString() {
return name;
}
private static List<Plant> garden = new ArrayList<>();
static {
garden.add(new Plant("장미", ANNUAL));
garden.add(new Plant("튤립", ANNUAL));
garden.add(new Plant("해바라기", ANNUAL));
garden.add(new Plant("백합", PERENNIAL));
}
// {ANNUAL=[해바라기, 장미, 튤립], PERENNIAL=[백합], BIENNIAL=[]}
private static Map<LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(LifeCycle.class);
static {
for(LifeCycle lc : LifeCycle.values()) {
plantsByLifeCycle.put(lc, new HashSet<>());
}
for(Plant plant : garden) {
plantsByLifeCycle.get(plant.lifeCycle).add(plant);
}
}
}
- stream API 로 EnumMap 구현하는 방법의 경우 생략
- 중첩 EnumMap 선언 생략 (p230)
아이템38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라
- enum 클래스의 경우 추상 클래스 enum을 확장하고 있기 때문에 추가로 상속을 할 수 없다 (자바는 다중상속 지원 x)
-단, 인터페이스 구현은 지원함
공통 인터페이스 정의
public interface CommonOperationMethod {
double apply(double x, double y);
}
enum BasicOperation 클래스 정의 및 인터페이스 구현
public enum BasicOperation implements CommonOperationMethod {
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;
private BasicOperation(String symbol) {
this.symbol = symbol;
}
public String getSymbol() {
return symbol;
}
@Override
public String toString() {
return symbol;
}
}
enum ExtendedOperation 클래스 정의 및 인터페이스 구현
public enum ExtendedOperation implements CommonOperationMethod {
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;
}
}
테스트
static <T extends Enum<T> & CommonOperationMethod> void test(Class<T> classType, double x, double y) {
for(CommonOperationMethod op : classType.getEnumConstants()) {
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
}
public static void main(String[] args) {
test(BasicOperation.class, 3, 4);
test(ExtendedOperation.class, 3, 4);
}
[출력 결과]
3.000000 + 4.000000 = 7.000000
3.000000 - 4.000000 = -1.000000
3.000000 * 4.000000 = 12.000000
3.000000 / 4.000000 = 0.750000
3.000000 ^ 4.000000 = 81.000000
3.000000 % 4.000000 = 3.000000
참고 & 살펴보기
https://techblog.woowahan.com/2527/
https://jojoldu.tistory.com/122
'공부 > Java' 카테고리의 다른 글
[Java] String 클래스 메서드 (자바의 정석) (0) | 2023.07.31 |
---|---|
[Java] FunctionalInterface와 Lambda Expression (0) | 2023.07.28 |
[Java] Enum values 조회 (Baeldung) (0) | 2023.07.26 |
[Java] Enum values 배열을 리스트 변환 (Baeldung, Enum values to List) (0) | 2023.07.21 |
[Java] Generics (제네릭) - 공변/무공변/반공변, PECS (0) | 2023.07.21 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!