// 34-1 정수 열거 패턴 - 상당히 취약하다!
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;
// 향긋한 오렌지 향의 사과 소스!
int i = (APPLE_FUJI - ORANGE_TEMPLE) / APPLE_PIPPIN;
// 34-2 가장 단순한 열거 타입
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }
// 34-3 데이터와 메서드를 갖는 열거 타입
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS(4.869e+24, 6.052e6),
EARTH(5.975e+24, 6.378e6),
MARS(6.419e+23, 3.393e6),
JUPITER(1.899e+27, 7.149e7),
SATURN(5.685e+26, 6.027e7),
URANUS(8.683e+25, 2.556e7),
NEPTUNE(1.024e+26, 2.477e7);
private final double mass; // 질량(단위 : 킬로그램)
private final double radius; // 반지름(단위 : 미터)
private final double surfaceGravity; // 표면중력(단위 : m / s^2)
// 중력상수 (단위 m^3 / kg s^2)
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
}
}
public class WeightTable {
public static void main(String[] args) {
double earthWeight = Double.parseDouble(args[0]);
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values()) {
System.out.println("Weight on %s is %f\\n", p, p.surfaceWeight(mass));
}
}
}
// 34-4 값에 따라 분기하는 열거 타입 - 이대로 만족하는가?
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);
}
}
// 34-5 상수별 메서드 구현을 활용한 열거 타입
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);
}
// 34-6 상수별 클래스 몸체(class body)와 데이터를 사용한 열거 타입
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; }
};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
@Override
public String toString() { return symbol; }
public abstract double apply(double x, double y);
}
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (OperationWithToString op : OperationWithToString.values()) {
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
}
// 34-7 열거 타입용 fromString 메서드 구현하기
private static final Map<String, Operation> stringToEnum =
Stream.of(values()).collect(
toMap(Object::toString, e -> e));
// 지정한 문자열에 해당하는 Operation을 (존재한다면) 반환한다.
public static Optional<Operation> fromString(String symbol) {
return Optional.ofNullable(stringToEnum.get(symbol));
}
// 34-8 값에 따라 분기하여 코드를 공유하는 열거 타입 - 좋은 방법인가?
enum PayrollDay {
MONDAY, TUSEDAY, 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;
}
}
// 34-9 전략 열거 타입 패턴
enum PayrollDay {
MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY),
THURSDAY(WEEKDAY), FRIDAY(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 / 2;
}
};
abstract int overtimePay(int mins, 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);
}
}
}
// 34-10 switch 문을 이용해 원래 열거 타입에 없는 기능을 수행한다.
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("알 수 없는 연산: " + op);
}
}
35. ordinal 메서드 대신 인스턴스 필드를 사용하라
// 35-1 ordinal 잘못 사용한 예 - 따라 하지 말것
public enum Ensemble {
SOLO, DUET, TRIO, QUARTER,
SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() {
return ordinal()+1;
}
}
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; }
}
36. 비트 필드 대신 EnumSet을 사용하라
// 36-1 비트 필드 열거 상수 - 구닥다리 기법
public class Text {
public static final int STYLE_BOLD = 1 << 0; // 이진수 1 =1
public static final int STYLE_ITALIC = 1 << 1; // 이진수 10 = 2
public static final int STYLE_UNDERLINE = 1 << 2; // 이진수 100 = 4
public static final int STYLE_STRIKETHROUGH = 1 << 3; // 이진수 1000 = 8
// 매개변수 styles는 0개 이상의 STYLE_ 상수를 비트별 OR한 값이다
public void applyStyle(int styles) {...}
}
text.applyStyle(STYLE_BOLD | STYLE_ITALIC);
// 36-2 EnumSet 비트 필드를 대체하는 현대적 기법
public class Text {
public enum Style {
BOLD, ITALIC, UNDERLINE, STRIKETHROUGH
}
// 어떤 Set을 넘겨도 되나, EnumSet이 가장 좋다
public void applyStyles(Set<Style> styles){
System.out.printf("Applying styles %s to text%n", Objects.requireNonNull(styles));
}
}
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 this.name;
}
}
// 37-1 ordinal()을 배열 인덱스로 사용 - 따라하지 말것
Set<Plant>[] plantByLifeCycle = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
for (int i = 0; i < plantByLifeCycle.length; i++) {
plantByLifeCycle[i] = new HashSet<>();
}
for (Plant p : garden) { // garden은 없는 구현체, Plant를 담은 객체
plantByLifeCycle[p.lifeCycle.ordinal()].add(p);
}
// 결과 출력
for (int i = 0; i < plantByLifeCycle.length; i++) {
System.out.printf("%s: %s %n", Plant.LifeCycle.values()[i], plantByLifeCycle[i]);
}
// 37-2 EnumMap을 사용해 데이터와 열거 타입을 매핑한다. (227쪽)
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);
// {ANNUAL=[바질, 딜], PERENNIAL=[로즈마리, 라벤더], BIENNIAL=[캐러웨이, 파슬리]}
```java
// 37-3 스트림을 사용한 코드 1- EnumMap을 사용하지 않는다!
System.out.println(Arrays.stream(garden)
.collect(groupingBy(p -> p.lifeCycle)));
// 37-4 스트림을 사용한 코드 2- EnumMap을 이용해 데이터와 열거 타입을 매핑했다.
System.out.println(Arrays.stream(garden)
.collect(groupingBy(p -> p.lifeCycle,
() -> new EnumMap<>(LifeCycle.class), toSet())));
// 37-5 배열들의 배열의 인덱스에 ordinal()을 사용 - 따라하지 말것!
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
// 행은 from의 ordinal을, 열은 to의 ordinal을 인덱스로 쓴다.
private static final Transition[][] TRANSITIONS = {
{null, MELT, SUBLIME},
{FREEZE, null, BOIL},
{DEPOSIT, CONDENSE, null}
};
// 한 상태에서 다른 상태로의 전이를 반환한다.
public static Transition from(Phase from, Phase to) {
return TRANSITIONS[from.ordinal()][to.ordinal()];
}
}
}
// 37-6 중첩 EnumMap으로 데이터와 열거 타입 쌍을 연결했다.
import java.util.EnumMap;
import java.util.Map;
import java.util.stream.Stream;
import static java.util.stream.Collectors.groupingBy;
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;
}
// 상전이 맵을 초기화한다.
public static final Map<Phase, Map<Phase, Transition>>
m = Stream.of(values()).collect(groupingBy(t -> t.from,
() -> new EnumMap<>(Phase.class),
toMap(t -> t.to, //key-mapper
t -> t, //value-mapper
(x, y) -> y, //merge-function
() -> new EnumMap<>(Phase.class))));
public static Transition from(Phase from, Phase to) {
return m.get(from).get(to);
}
}
// 37-7 EnumMap 버전에 새로운 상태 추가하기
public enum Phase {
SOLID, LIQUID, GAS, PLASMA;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS);
... // 나머지 코드는 그대로다.
}
38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라
// 38-1 인터페이스를 이용해 확장 가능 열거 타입을 흉내 냈다.
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;
}
}
// 38-2 확장 가능 열거 타입
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;
}
}
//38-2 확장 가능 열거 타입
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(ExtendedOperation.class,x,y);
}
// 1. class 리터럴은 열거타입이면서 Operation 하위 타입 (opEnumType)
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));
}
39. 명명 패턴보다 애너테이션을 사용하라
// 39-1 마커(marker) 애너테이션 타입 선언
import java.lang.annotation.*;
/**
* 테스트 메서드임을 선언하는 애너테이션이다.
* 매개변수 없는 정적 메서드 전용이다.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
// 39-2 마커 애너테이션을 사용한 프로그램 예
public class Sample {
@Test
public static void m1() { } // 성공해야 한다.
public static void m2() { }
@Test public static void m3() { // 실패해야 한다.
throw new RuntimeException("실패");
}
public static void m4() { }
@Test public void m5() { } // 잘못 사용한 예: 정적 메서드가 아니다.
public static void m6() { }
@Test public static void m7() { // 실패해야 한다.
throw new RuntimeException("실패");
}
public static void m8() { }
}
// 39-3 마커 애너테이션을 처리하는 프로그램
import java.lang.reflect.*;
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName(args[0]);
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
m.invoke(null);
passed++;
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + " 실패: " + exc);
} catch (Exception exc) {
System.out.println("잘못 사용한 @Test: " + m);
}
}
}
System.out.printf("성공: %d, 실패: %d%n",
passed, tests - passed);
}
}
// 39-4 매개변수 하나를 받는 애너테이션 타입
import java.lang.annotation.*;
/**
* 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
**Class<? extends Throwable> value();
}**
// 39-5 매개변수 하나짜리 애너테이션을 사용한 프로그램 (241쪽)
public class Sample2 {
**@ExceptionTest(ArithmeticException.class)**
public static void m1() { // 성공해야 한다.
int i = 0;
i = i / i;
}
**@ExceptionTest(ArithmeticException.class)**
public static void m2() { // 실패해야 한다. (다른 예외 발생)
int[] a = new int[0];
int i = a[1];
}
**@ExceptionTest(ArithmeticException.class)**
public static void m3() { } // 실패해야 한다. (예외가 발생하지 않음)
}
// 39-6 배열 매개변수를 받는 애너테이션 타입 (242쪽)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception>**[]** value();
}
// 39-7 배열 매개변수를 받는 애너테이션을 사용하는 코드 (242-243쪽)
@ExceptionTest(**{ IndexOutOfBoundsException.class,
NullPointerException.class }**)
public static void doublyBad() { // 성공해야 한다.
List<String> list = new ArrayList<>();
// 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나
// NullPointerException을 던질 수 있다.
list.addAll(5, null);
}
// 배열 매개변수를 받는 애너테이션을 처리하는 코드 (243쪽)
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
**int oldPassed = passed;
Class<? extends Throwable>[] excTypes =
m.getAnnotation(ExceptionTest.class).value();
for (Class<? extends Throwable> excType : excTypes) {
if (excType.isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed)
System.out.printf("테스트 %s 실패: %s %n", m, exc);**
}
}
// 39-8 반복 가능한 애너테이션 타입 (243-244쪽)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
// 컨테이너 애너테이션 (244쪽)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value();
}
// 39-9 반복 가능 애너테이션을 두 번 단 코드 (244쪽)
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() {
...
}
// 40-1 영어 알파벳 2개로 구성된 문자열(바이그램)을 표현하는 클래스 - 버그를 찾아보자. (246쪽)
public class Bigram {
private final char first;
private final char second;
public Bigram(char first, char second) {
this.first = first;
this.second = second;
}
public boolean equals(Bigram b) {
return b.first == first && b.second == second;
}
public int hashCode() {
return 31 * first + second;
}
public static void main(String[] args) {
Set<Bigram> s = new HashSet<>();
for (int i = 0; i < 10; i++)
for (char ch = 'a'; ch <= 'z'; ch++)
s.add(new Bigram(ch, ch));
System.out.println(s.size());
}
}
@Override
public boolean equals(Bigram b) {
return b.first == first && b.second == second;
}
@Override public boolean equals(Object o) {
if (!(o instanceof Bigram2))
return false;
Bigram2 b = (Bigram2) o;
return b.first == first && b.second == second;
}
41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라
public interface Serializable{
}
else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
}
Class<?> testClass = Class.forName(args[0]);
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {...}
}