[item-34] int 상수 대신 열거 타입을 사용하라
- enum type 나오기 전에는 정수 열거 패턴(int enum pattern)을 사용해 왔습니다.
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int ORANGE_NEVEL = 0;
public static final int ORANGE_TEMPLE = 1;
- 위와 같은 정수 열거 패턴은 타입 안정성을 보장하기 어렵습니다.
- 위의 대안으로 나온 것이 열거 타입입니다.
열거 타입
public enum Apple {
FUJI, PIPPIN, GRANNY_SMITH
}
public enum Orange {
NAVEL, TEMPLE, BLOOD
}
장점
- 완전완 형태의 클래스입니다.
- 밖에서 접근할 수 있는 생성자를 제공하지 않으므로 사실상 final로 볼 수 있습니다.
- 인스턴스가 하나만 존재합니다.
- 열거 타입은 컴파일 시점에서 타입 안정성을 제공합니다.
- 열거 타입의 toString 메서드는 출력하기에 적합한 문자열을 제공합니다.
- 임의의 메서드나 필드를 추가할 수 있고 임의의 인터페이스를 구현할 수 있습니다.
Example
enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS(4.869e+24, 6.052e6),
EARTH(5.975e+24, 6.378e6);
// ...
private final double mass; // 질량
private final double radius; // 반지름
private final double surfaceGravity; // 표면중력
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
}
}
- 열거 타입 상수 각각을 특정 데이터와 연결 지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장합니다.
- 열거 타입은 근본적으로 불변이므로 모든 필드는 final 이어야 합니다.
- 열거 타입은 자신 안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메서드 values를 제공합니다.
[item-35] ordinal 메서드 대신 인스턴스 필드를 사용하라
- ordinal 메서드: 해당 상수가 열거 타입에서 몇 번째인지 반환하는 메서드
- 가장 첫 번째 상수는 0을 반환합니다.
ex) ordinal 메서드 예시
- 상수의 선언을 바꾸는 순간 오동작을 할 수 있으며, 이미 사용 중인 정수와 값이 같은 상수는 추가할 수도 없습니다.
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET,
SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() { return ordinal() + 1; }
}
해결책
- 열거 타입 상수에 연결된 값은 ordinal 메서드로 얻지 말고, 인스턴스 필드에 저장
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), NONET(9), DECTET(10),
DOUBLE_QUARTET(8), TRIPLE_QUARTET(12);
private final int int numberOfMusicians;
Ensemble(int size) { this.numberOfMusicians = size; }
public int numberOfMusicians() { return numberOfMusicians; }
}
[item-36] 비트 필드 대신 EnumSet을 사용하라
예전에는 정수 열거 패턴에 비트 필드 표시
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.applyStyles(STYLE_BOLD | STYLE_ITALIC);
- 비트 필드: 비트별 OR연산을 이용하여 여러 상수를 하나의 집합으로 모으기 위한 필드
단점
- 정수 열거 상수의 단점을 그대로 가져옵니다.
- 비트 필드에 포함된 모든 의미상의 원소를 순회하기도 어렵고 최대 몇 비트가 필요한지 미리 예측한 후 타입을 선택해야 합니다.
해결책: EnumSet 활용
- Google의 Guava Library인 Collections.unmodifiableSet을 사용하면 불변 상태로 만들 수 있습니다.
public class Text {
public enum Style { BOLD, ITALIC, INDERLINE, STRIKETHROUGH }
// 깔끔하고 안전하다. 어떤 Set을 넘겨도 되나, EnumSet이 가장 좋다.
// 보통 인터페이스를 전달 받는 것이 좋은 습관이다.
public void applyStyles(Set<Style> styles) { ... }
}
// Guava 라이브러리 사용
Set immutableEnumSet = Collections.unmodifiableSet(EnumSet.of(Text.Style.BOLD, Text.Style.ITALIC));
immutableEnumSet.add(Text.Style.INDERLINE); // java.lang.UnsupportedOperationException
[item-37] ordinal 인덱싱 대신 EnumMap을 사용하라
- ordinal 메서드를 배열 인덱스로 사용하면 위험합니다.
// 배열은 제네릭과 호환되지 않으니 비검사 형변환도 필요
Set<Plant>[] plantByLifeCycle =
(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); -> 문제 부분
}
// 결과 출력
for (int i = 0; i < plantsByLifeCycle.length; i++) {
System.out.printf("%s: %s%n", Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
}
- 배열은 각 인덱스의 의미를 모르기 때문에, 위 코드에서의 %s %s\n과 같은 출력 결과를 포맷팅 해야 합니다.
해결책
1. EnumMap 사용
// EnumMap을 사용하여 데이터와 열거 타입을 매핑한다.
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);
2. 스트림(Stream) 사용
// Map을 이용해 데이터와 열거 타입 매핑
Arrays.stream(garden)
.collect(groupingBy(p -> p.lifeCycle))
// EnumMap을 이용해 데이터와 열거 타입 매핑
Arrays.stream(garden)
.collect(groupingBy(
p -> p.lifeCycle,
() -> new EnumMap<>(LifeCycle.class),
toSet())
);
[item-38] 확장할 수 있는 열거타입이 필요하면 인터페이스를 사용하라
- 열거 타입을 확장하는 것은 대부분 좋지 않습니다.
- 하지만, 연산 코드(operation code)를 구현할 때는 어울릴 수 있습니다.
- 이때는 열거 타입 enum이 인터페이스를 구현(implements)할 수 있다는 점을 이용하면 됩니다.
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;
}
}
- 타입 수준에서도 기본 열거 타입 대신에 확장한 열거 타입을 넘겨서 열거 타입의 모든 원소를 순회할 수 있게 합니다.
[item-39] 명명 패턴보다 애너테이션을 사용하라
- Junit3 version까지는 테스트 메서드의 이름이 test로 시작해야 했습니다.
- test라는 이름이 없거나, 오타가 났다면 테스트 코드는 실행조차 되지 않았습니다.
- Junit4부터 애너테이션을 도입하여 이러한 문제들을 해결할 수 있었습니다.
- 애너테이션이 할 수 있는 일들을 명명 패턴으로 처리할 필요는 없습니다.
[item-40] @Override 애너테이션을 일관되게 사용하라
- @Override: 상위 타입의 메서드를 재정의 하기 위한 애너테이션
- 이러한 메서드를 일관되게 사용하면 발생할 수 있는 실수나 버그들을 줄일 수 있습니다.
- 추상 메서드를 재정의 할 때는 제외하고는 상위 클래스의 메서드를 재정의하려는 모든 메서드에 @Override 애너테이션을 다는 습관을 가져야 합니다. (위의 이유 때문에)
[item-41] 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라
- Marker Interface: 아무 메서드도 갖고 있지 않고 단지 자신을 구현하는 클래스가 특정 속성을 갖는 것을 표현해 주는 인터페이스
- ex) Serializable, Cloneable, @Target(ElementType.TYPE)
- 직렬화: 객체를 바이트 스트림으로 변환하여 파일이나 네트워크 전송 등을 가능하게 하는 기능입니다.
마커 인터페이스의 장점
- 클래스의 인스턴스를 구분하는 타입으로 사용할 수 있습니다.
- 적용 대상을 더 정밀하게 지정할 수 있습니다.
- 거대한 애너테이션 시스템의 자원을 받습니다.
'Java > Effective Java' 카테고리의 다른 글
[Effective Java] 5장. 제네릭 (2) | 2025.05.17 |
---|---|
[Effective Java] 4장. 클래스와 인터페이스 (0) | 2024.08.08 |
[Effective Java] 3장. 모든 객체의 공통 메서드 (0) | 2024.08.08 |
[Effective Java] 2장. 객체 생성과 파괴 (0) | 2024.08.08 |