diff --git "a/docs/\354\225\204\354\235\264\355\205\234 34. int \354\203\201\354\210\230 \353\214\200\354\213\240 \354\227\264\352\261\260 \355\203\200\354\236\205\354\235\204 \354\202\254\354\232\251\355\225\230\353\235\274.md" "b/docs/\354\225\204\354\235\264\355\205\234 34. int \354\203\201\354\210\230 \353\214\200\354\213\240 \354\227\264\352\261\260 \355\203\200\354\236\205\354\235\204 \354\202\254\354\232\251\355\225\230\353\235\274.md" new file mode 100644 index 0000000..04a0f09 --- /dev/null +++ "b/docs/\354\225\204\354\235\264\355\205\234 34. int \354\203\201\354\210\230 \353\214\200\354\213\240 \354\227\264\352\261\260 \355\203\200\354\236\205\354\235\204 \354\202\254\354\232\251\355\225\230\353\235\274.md" @@ -0,0 +1,316 @@ +# 아이템 34. int 상수 대신 열거 타입을 사용하라 + + +```java +// 코드 34-1 정수 열거 패턴 - 상당히 취약 +public class IntEnumFruit { + 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; +} +``` + +- 타입 안전을 보장하지 못한다. +- 표현력이 떨어진다. +- 값이 바뀌면 다시 컴파일해야 한다. +- 정수 열거 그룹에 속한 모든 상수를 순회하는 방법, 상수가 몇 개인지 파악하기 어렵다. + +```java +// 코드 34-2 가장 단순한 열거 타입 +public class EnumFruit { + enum Apple { + FUJI, PIPPIN, GRANNY_SMITH + } + enum Orange { + NAVEL, TEMPLE, BLOOD + } +} +``` + +- 위 단점들은 **열거 타입(enum type)**을 사용하면 말끔히 해결해준다. +- 열거 타입의 상수 하나당 자신의 인스턴스를 만들어 public static final 필드로 공개한다. +- 열거 타입은 외부에서 접근할 수 있는 생성자를 제공하지 않아 사실상 final이다. +- 열거 타입은 컴파일 타임 타입 안전성을 제공한다. +- 열거 타입에는 임의의 메서드나 필드 추가가 가능하다. +- 인터페이스를 구현할 수 있다. + +```java +// 코드 34-3 데이터와 메서드를 갖는 열거 타입 (211쪽) +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 + } +} +``` + +```java +// 어떤 객체의 지구에서의 무게를 입력받아 여덟 행성에서의 무게를 출력한다. (212쪽) +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.printf("%s에서의 무게는 %f이다.%n", + p, p.surfaceWeight(mass)); + } +} +``` + +- 열거 타입을 선언한 클래스 혹은 그 패키지에서만 유용한 기능은 private이나 package-private 메서드로 구현한다. + - 널리 쓰이는 열거 타입은 톱레벨 클래스로 만든다. + - java.math.RoundingMode + +```java +package java.math; + +@SuppressWarnings("deprecation") // Legacy rounding mode constants in BigDecimal +public enum RoundingMode { + UP(BigDecimal.ROUND_UP), + DOWN(BigDecimal.ROUND_DOWN), + CEILING(BigDecimal.ROUND_CEILING), + FLOOR(BigDecimal.ROUND_FLOOR), + HALF_UP(BigDecimal.ROUND_HALF_UP), + HALF_DOWN(BigDecimal.ROUND_HALF_DOWN), + HALF_EVEN(BigDecimal.ROUND_HALF_EVEN), + UNNECESSARY(BigDecimal.ROUND_UNNECESSARY); + + final int oldMode; + + private RoundingMode(int oldMode) { + this.oldMode = oldMode; + } + + public static RoundingMode valueOf(int rm) { + switch(rm) { + + case BigDecimal.ROUND_UP: + return UP; + + case BigDecimal.ROUND_DOWN: + return DOWN; + + case BigDecimal.ROUND_CEILING: + return CEILING; + + case BigDecimal.ROUND_FLOOR: + return FLOOR; + + case BigDecimal.ROUND_HALF_UP: + return HALF_UP; + + case BigDecimal.ROUND_HALF_DOWN: + return HALF_DOWN; + + case BigDecimal.ROUND_HALF_EVEN: + return HALF_EVEN; + + case BigDecimal.ROUND_UNNECESSARY: + return UNNECESSARY; + + default: + throw new IllegalArgumentException("argument out of range"); + } + } +} +``` + +**상수마다 동작이 달라져야 하는 상황 처리 방법** + +```java +// 코드 34-10 switch 문을 이용해 원래 열거 타입에 없는 기능을 수행한다. (219쪽) +public class Inverse { + 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("Unknown op: " + op); + } + } + + public static void main(String[] args) { + double x = Double.parseDouble(args[0]); + double y = Double.parseDouble(args[1]); + for (Operation op : Operation.values()) { + Operation invOp = inverse(op); + System.out.printf("%f %s %f %s %f = %f%n", + x, op, y, invOp, y, invOp.apply(op.apply(x, y), y)); + } + } +} +``` + +- 사칙연산 계산기를 예를 들면, switch 구문으로 활용해 볼 수 있다. +- 그러나 새로운 상수가 추가되면 case 문이 추가돼야 한다는 단점이 있다. +- 위의 문제는 **상수별 메서드 구현**(constant-specific method implementation)으로 해결할 수 있다. + +```java +// 코드 34-6 상수별 클래스 몸체(class body)와 데이터를 사용한 열거 타입 (215-216쪽) +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; + } + }; + + private final String symbol; + + Operation(String symbol) { + this.symbol = symbol; + } + + @Override + public String toString() { + return symbol; + } + + public abstract double apply(double x, double y); + + // 코드 34-7 열거 타입용 fromString 메서드 구현하기 (216쪽) + private static final Map stringToEnum = + Stream.of(values()).collect( + toMap(Object::toString, e -> e)); + + // 지정한 문자열에 해당하는 Operation을 (존재한다면) 반환한다. + public static Optional fromString(String symbol) { + return Optional.ofNullable(stringToEnum.get(symbol)); + } + + public static void main(String[] args) { + double x = Double.parseDouble(args[0]); + double y = Double.parseDouble(args[1]); + for (Operation op : Operation.values()) + System.out.printf("%f %s %f = %f%n", + x, op, y, op.apply(x, y)); + } +} +``` + +- Operation 상수가 stringToEnum 맵에 추가되는 시점은 열거 타입 상수 생성 후 정적 필드가 초기화 될 때다. + - java 8이전에는 values()를 순회하면서 빈 해시맵에 {문자열: 열거 타입 상수} 형태로 추가했을 것이다. 그러나, 열거 타입 상수는 생성자에서 자신의 인스턴스를 맵에 추가할 수 없다.(컴파일 에러나 런타임에 NPE 발생할 수 있다.) +- 열거 타입의 정적 필드 중 열거 타입의 생성자에 접근할 수 있는 것은 상수 변수 뿐이다. + - 열거 타입 생성자가 실행되는 시점에는 정적 필드들이 아직 초기화되기 전이라, 자기 자신을 추가하지 못하게 하는 제약이 꼭 필요하다. + - 열거 타입 생성자에서 같은 열거 타입의 다른 상수에 접근할 수 없다. + - 열거 상수가 public static final 로 선언된 것인데 생성자에서 정적 필드에 접근할 수 없는 제약이 적용된 것이다. +- 상수별 메서드 구현에는 열거 타입 상수끼리 코드 공유하기 어렵다. + +```java +// 코드 34-9 전략 열거 타입 패턴 (218-219쪽) +enum PayrollDay { + MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY), + THURSDAY(WEEKDAY), FRIDAY(WEEKDAY), + SATURDAY(WEEKEND), SUNDAY(WEEKEND); + // (역자 노트) 원서 1~3쇄와 한국어판 1쇄에는 위의 3줄이 아래처럼 인쇄돼 있습니다. + // + // MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, + // SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND); + // + // 저자가 코드를 간결하게 하기 위해 매개변수 없는 기본 생성자를 추가했기 때문인데, + // 열거 타입에 새로운 값을 추가할 때마다 적절한 전략 열거 타입을 선택하도록 프로그래머에게 강제하겠다는 + // 이 패턴의 의도를 잘못 전달할 수 있어서 원서 4쇄부터 코드를 수정할 계획입니다. + + private final PayType payType; + + PayrollDay(PayType payType) { + this.payType = payType; + } + // PayrollDay() { this(PayType.WEEKDAY); } // (역자 노트) 원서 4쇄부터 삭제 + + 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); + } + } + + public static void main(String[] args) { + for (PayrollDay day : values()) + System.out.printf("%-10s%d%n", day, day.pay(8 * 60, 1)); + } +} +``` + +### 열거 타입은 언제 쓰는게 좋을까? + +필요한 원소를 컴파일타임에 다 알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자. \ No newline at end of file diff --git "a/docs/\354\225\204\354\235\264\355\205\234 35. ordinal \353\251\224\354\204\234\353\223\234 \353\214\200\354\213\240 \354\235\270\354\212\244\355\204\264\354\212\244 \355\225\204\353\223\234\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274.md" "b/docs/\354\225\204\354\235\264\355\205\234 35. ordinal \353\251\224\354\204\234\353\223\234 \353\214\200\354\213\240 \354\235\270\354\212\244\355\204\264\354\212\244 \355\225\204\353\223\234\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274.md" new file mode 100644 index 0000000..03fb08b --- /dev/null +++ "b/docs/\354\225\204\354\235\264\355\205\234 35. ordinal \353\251\224\354\204\234\353\223\234 \353\214\200\354\213\240 \354\235\270\354\212\244\355\204\264\354\212\244 \355\225\204\353\223\234\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274.md" @@ -0,0 +1,34 @@ +# 아이템 35. ordinal 메서드 대신 인스턴스 필드를 사용하라 + + +```java +// 인스턴스 필드에 정수 데이터를 저장하는 열거 타입 (222쪽) +public enum BadEnsemble { + SOLO, DUET, TRIO, QUARTET, QUINTET, + SEXTET, SEPTET, OCTET, DOUBLE_QUARTET, + NONET, DECTET, TRIPLE_QUARTET; + + public int numberOfMusicians() { return ordinal() + 1; } +} +``` + +- 열거 타입에서 몇 번째 위치인지 반환하는 ordinal 이라는 메서드를 제공한다. +- 상수 선언 순서를 바꾸는 순간 ordinal()를 사용하는 메서드는 오동작하게 될 것이다. + +```java +// 인스턴스 필드에 정수 데이터를 저장하는 열거 타입 (222쪽) +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; } +} +``` + +- 열거 타입 상수에 연결된 값은 ordinal 메서드로 얻지 말고 인스턴스 필드에 저장한다. + +> Enum API 문서 내용 중, “ordinal()은 EnumSet, EnumMap 같이 연거 타입 기반의 범용 자료구조에 쓸 목적으로 설계됐다.” +> \ No newline at end of file diff --git "a/docs/\354\225\204\354\235\264\355\205\234 36. \353\271\204\355\212\270 \355\225\204\353\223\234 \353\214\200\354\213\240 EnumSet\354\235\204 \354\202\254\354\232\251\355\225\230\353\235\274.md" "b/docs/\354\225\204\354\235\264\355\205\234 36. \353\271\204\355\212\270 \355\225\204\353\223\234 \353\214\200\354\213\240 EnumSet\354\235\204 \354\202\254\354\232\251\355\225\230\353\235\274.md" new file mode 100644 index 0000000..928a1bf --- /dev/null +++ "b/docs/\354\225\204\354\235\264\355\205\234 36. \353\271\204\355\212\270 \355\225\204\353\223\234 \353\214\200\354\213\240 EnumSet\354\235\204 \354\202\254\354\232\251\355\225\230\353\235\274.md" @@ -0,0 +1,45 @@ +# 아이템 36. 비트 필드 대신 EnumSet을 사용하라 + + +```java +public class BitText { + 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 + + public void applyStyles(int styles) { + } +} +``` + +- 비트 필드 값이 그대로 출력되면 단순한 정수 열거 상수를 출력할 때보다 해석하기가 훨씬 어렵다. +- 비트 필드 하나에 녹아있는 모든 원소를 순회하기 까다롭다. +- java.util 패키지의 **EnumSet** 클래스는 열거 타입 상수의 값으로 구성된 집합을 효과적으로 표현해준다. + - Set 인터페이스를 완벽히 구현 + - 타입 안전 + - EnumSet의 내부는 비트 백터로 구현 + - 원소가 64개 이하라면, EnumSet 전체를 long 변수 하나로 표현하여 비트 필드에 비견되는 성능을 보여준다. + +```java +// 코드 36-2 EnumSet - 비트 필드를 대체하는 현대적 기법 (224쪽) +public class Text { + public enum Style {BOLD, ITALIC, UNDERLINE, STRIKETHROUGH} + + // 어떤 Set을 넘겨도 되나, EnumSet이 가장 좋다. + public void applyStyles(Set