diff --git "a/Ch04/item19/\354\203\201\354\206\215\354\235\204 \352\263\240\353\240\244\355\225\264 \354\204\244\352\263\204\355\225\230\352\263\240 \353\254\270\354\204\234\355\231\224 \355\225\230\353\235\274, \352\267\270\353\237\254\354\247\200 \354\225\212\354\225\230\353\213\244\353\251\264 \354\203\201\354\206\215\354\235\204 \352\270\210\354\247\200\355\225\230\353\235\274.md" "b/Ch04/item19/\354\203\201\354\206\215\354\235\204 \352\263\240\353\240\244\355\225\264 \354\204\244\352\263\204\355\225\230\352\263\240 \353\254\270\354\204\234\355\231\224 \355\225\230\353\235\274, \352\267\270\353\237\254\354\247\200 \354\225\212\354\225\230\353\213\244\353\251\264 \354\203\201\354\206\215\354\235\204 \352\270\210\354\247\200\355\225\230\353\235\274.md" new file mode 100644 index 0000000..8de3592 --- /dev/null +++ "b/Ch04/item19/\354\203\201\354\206\215\354\235\204 \352\263\240\353\240\244\355\225\264 \354\204\244\352\263\204\355\225\230\352\263\240 \353\254\270\354\204\234\355\231\224 \355\225\230\353\235\274, \352\267\270\353\237\254\354\247\200 \354\225\212\354\225\230\353\213\244\353\251\264 \354\203\201\354\206\215\354\235\204 \352\270\210\354\247\200\355\225\230\353\235\274.md" @@ -0,0 +1,63 @@ +# item19.상속을 고려해 설계하고 문서화 하라, 그러지 않았다면 상속을 금지하라 + +### 상속을 고려한 설계와 문서화 +상속용 클래스는 +- 재정의 할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야 한다. + +- 재정의 가능 = public 과 protected 메서드 중 final 이 아닌 모든 메서드 +### Implementation Requirements +API 문서의 메서드 설명 끝에서 Implementation Requirements 로 시작하는 절을 종종 볼 수 있다. + +- 메서드의 내부 동작 방식을 설명하는 곳 +- @implSpec 태그 → javadoc 도구가 생성해줌 +- 클래스를 안전하게 상속할 수 있도록 내부 구현 방식을 설명해야 한다. + +### protected 로 노출할 메서드의 결정 +- 수는 가능한 적어야 한다. +- 3개 정도 실제 하위 클래스를 만들어 보는 것이 유일하다. +- 제 3 자의 작성으로도 검증해야 한다. +- 꼭 필요한 protected 멤버를 놓쳤다면 하위 클래스를 작성할 때 그 빈자리가 확연히 드러난다. +- 하위 클래스를 여러 개 만들 때까지 전혀 쓰이지 않는 protected 멤버는 private 이어야 할 가능성이 크다. +- 너무 적게 노출해서 상속으로 얻는 이점마저 없애지 않도록 주의 한다 + +### 상속용 클래스의 생성자는 직/간접적으로 재정의 가능메서드를 호출하면 안된다. +private, final, static 메서드는 재정의가 불가능하니 생성자에서 안심하고 호출해도 된다. + +- 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행된다. +- 하위 클래스에서 재정의한 메서드가, 하위 클래스의 생성자에서 초기화 하는 값에 의존 → 오동작 +```java + +public class Super { +// 잘못된 예 - 생성자가 재정의 가능 메서드를 호출함. +public Super() { +overrideMe(); +} +public void overrideMe() { } +} + +public final class Sub extends Super { +// 초기화 되지 않은 final 필드, 생성자에서 초기화 한다. +private final Instant instant; + + Sub() { + instant = Instant.now(); + } + // 재정의 가능 메서드, 상위 클래스의 생성자가 호출한다. + @Override public void overrideMe() { + System.out.println(instant); // null 출력 + } + + public static void main(String[] args) { + Sub sub = new Sub(); + sub.overrideMe(); + } +} +``` +System.out.println(instant); 이 실행 될 때, NullPointerException 이 발생 하지만 println 이 null 입력도 받아 들이기 때문에 에러를 던지지 않았다. + +### 상속을 금지하는 방법 +1. final 클래스 선언 +2. 모든 생성자를 private 이나 package-private 으로 선언, public 정적 팩터리 +3. 래퍼 클래스 패턴 + +Set, List, Map 같이 핵심기능을 정의한 인터페이스가 있고, 클래스가 그 인터페이스를 구현했다면 상속을 금지해도 개발하는 데 아무런 어려움이 없을 것이다. \ No newline at end of file diff --git "a/Ch04/item22/\354\235\270\355\204\260\355\216\230\354\235\264\354\212\244\353\212\224_\352\265\254\355\230\204\355\225\230\353\212\224_\354\252\275\354\235\204_\354\203\235\352\260\201\355\225\264_\354\204\244\352\263\204\355\225\230\353\235\274.md" "b/Ch04/item21/\354\235\270\355\204\260\355\216\230\354\235\264\354\212\244\353\212\224_\352\265\254\355\230\204\355\225\230\353\212\224_\354\252\275\354\235\204_\354\203\235\352\260\201\355\225\264_\354\204\244\352\263\204\355\225\230\353\235\274.md" similarity index 100% rename from "Ch04/item22/\354\235\270\355\204\260\355\216\230\354\235\264\354\212\244\353\212\224_\352\265\254\355\230\204\355\225\230\353\212\224_\354\252\275\354\235\204_\354\203\235\352\260\201\355\225\264_\354\204\244\352\263\204\355\225\230\353\235\274.md" rename to "Ch04/item21/\354\235\270\355\204\260\355\216\230\354\235\264\354\212\244\353\212\224_\352\265\254\355\230\204\355\225\230\353\212\224_\354\252\275\354\235\204_\354\203\235\352\260\201\355\225\264_\354\204\244\352\263\204\355\225\230\353\235\274.md" diff --git "a/Ch04/item23/\355\203\234\352\267\270_\353\213\254\353\246\260_\355\201\264\353\236\230\354\212\244\353\263\264\353\213\244\353\212\224_\355\201\264\353\236\230\354\212\244_\352\263\204\354\270\265\352\265\254\354\241\260\353\245\274_\355\231\234\354\232\251\355\225\230\353\235\274.md" "b/Ch04/item23/\355\203\234\352\267\270_\353\213\254\353\246\260_\355\201\264\353\236\230\354\212\244\353\263\264\353\213\244\353\212\224_\355\201\264\353\236\230\354\212\244_\352\263\204\354\270\265\352\265\254\354\241\260\353\245\274_\355\231\234\354\232\251\355\225\230\353\235\274.md" new file mode 100644 index 0000000..eb30c98 --- /dev/null +++ "b/Ch04/item23/\355\203\234\352\267\270_\353\213\254\353\246\260_\355\201\264\353\236\230\354\212\244\353\263\264\353\213\244\353\212\224_\355\201\264\353\236\230\354\212\244_\352\263\204\354\270\265\352\265\254\354\241\260\353\245\274_\355\231\234\354\232\251\355\225\230\353\235\274.md" @@ -0,0 +1,101 @@ +### ☁️ 태그 달린 클래스 +태그 달린 클래스란, **하나의 클래스가 두 가지 이상의 의미를 표현** 가능할 때, 그중 현재 표현하는 의미를 태그 값으로 알려주는 클래스이다. 아래 예시를 보자. + + +```java +class Figure { + enum Shape { RECTANGLE, CIRCLE }; // TAG + + // 태그 필드 - 현재 모양을 나타낸다. + final Shape shape; + + // 다음 필드들은 모양이 사각형(RECTANGLE)일 때만 쓰인다. + double length; + double width; + + // 다음 필드는 모양이 원(CIRCLE)일 때만 쓰인다. + double radius; + + // 원용 생성자 + Figure(double radius) { + shape = Shape.CIRCLE; + this.radius = radius; + } + + // 사각형용 생성자 + Figure(double length, double width) { + shape = Shape.RECTANGLE; + this.length = length; + this.width = width; + } + + double area() { + switch(shape) { + case RECTANGLE: + return length * width; + case CIRCLE: + return Math.PI * (radius * radius); + default: + throw new AssertionError(shape); + } + } +} +``` + +#### 태그 달린 클래스 단점 + +1. 열거 타입 선언, 태그 필드, `switch` 문 등 쓸데없는 코드로 가독성이 떨어진다. +2. 다른 의미를 위한 코드도 해당하지 않는데 항상 존재하니 메모리를 많이 먹는다. +3. 필드들을 불변을 위해 `final` 로 선언하려면 해당 의미에 쓰이지 않는 필드들까지 생성자에서 초기화해야한다. +4. 또 다른 의미를 추가하려면 `switch` 문 코드를 수정해야 한다. +5. 인스턴스의 타입만으로는 현재 나타내는 의미를 알 길이 전혀 없다. + +> 즉, 태그 달린 클래스는 장황하고, 오류를 내기 쉽고, 비효율적이다. + +### ☁️ 클래스 계층 구조 +이러한 문제를, 클래스 계층구조를 활용하는 서브타이핑 (subtyping)을 통해 해결할 수 있다. 클래스 계층 구조는, **추상 클래스와 추상 메서드**를 통해 **타입 하나로 다양한 의미의 객체를 표현**할 수 있게 해준다. + + +1. 계층 구조의 루트가 될 추상 클래스 정의한다. +```java +abstract class Figure { + abstract double area(); +} +``` ++ 태그 값에 따라 **동작이 달라지는 메서드**들을 루트 클래스의 **추상 메서드**로 선언 ++ 태그 값에 상관없이 **동작이 일정한 메서드와 공통 메서드**들을 루트 클래스에 **일반 메서드**로 선언 + + +2. 루트 클래스를 확장한 구체 클래스를 의미별로 하나씩 지정한다. + +```java +class Circle extends Figure { + final double radius; + + Circle(double radius) { this.radius = radius; } + + @Override double area() { return Math.PI * (radius * radius); } +} +``` + +```java +class Rectangle extends Figure { + final double length; + final double width; + + Rectangle(double length, double width) { + this.length = length; + this.width = width; + } + @Override double area() { return length * width; } +} +``` ++ 루트 클래스가 정의한 추상 메서드를 각자 의미에 맞게 구현 + +#### 클래스 계층 구조 장점 + +1. 각 의미를 독립된 클래스에 담아 관련 없던 데이터 필드들을 제거했다. +2. 남아 있는 필드들은 모두 `final` 로 선언해 불변을 보장할 수 있다. +3. 각 클래스의 생성자가 모든 필드를 남김없이 초기화하고 추상 메서드를 모두 구현했는지 컴파일러 단에서 확인할 수 있다. +3. 루트 클래스의 코드를 건드리지 않고 독립적으로 계층 구조를 확장할 수 있다. + diff --git "a/Ch04/item24/\353\251\244\353\262\204 \355\201\264\353\236\230\354\212\244\353\212\224 \353\220\230\353\217\204\353\241\235 static \355\201\264\353\236\230\354\212\244\353\241\234 \353\247\214\353\223\244\353\235\274.md" "b/Ch04/item24/\353\251\244\353\262\204 \355\201\264\353\236\230\354\212\244\353\212\224 \353\220\230\353\217\204\353\241\235 static \355\201\264\353\236\230\354\212\244\353\241\234 \353\247\214\353\223\244\353\235\274.md" new file mode 100644 index 0000000..7a5baab --- /dev/null +++ "b/Ch04/item24/\353\251\244\353\262\204 \355\201\264\353\236\230\354\212\244\353\212\224 \353\220\230\353\217\204\353\241\235 static \355\201\264\353\236\230\354\212\244\353\241\234 \353\247\214\353\223\244\353\235\274.md" @@ -0,0 +1,160 @@ +# item24.멤버 클래스는 되도록 static 클래스로 만들라 + +### 중첩 클래스 (Nested Class) : 자신을 둘러싼 바깥 클래스에서만 쓰이는 클래스 +- 정적 멤버 클래스 (static member class) +- 비정적 멤버 클래스 +- 익명 클래스 +- 지역 클래스 + +### 정적 멤버 클래스 +- 클래스 내부의 static 클래스 +> 바깥 클래스의 private 멤버에도 바로 접근 가능. 이외에는 일반 클래스와 같다. + + +- private 정적 멤버 클래스 : 바깥 클래스의 구성 요소를 나타낼 때 +```java +public class Person { + private String firstName; + private String lastName; + + private static class Computer { // private, public + private String name; + private int price; + + public Computer(String name, int price) { + this.name = name; + this.price = price; + } + + public int getPrice() { + return price; + } + } +} +``` + +- public 정적 멤버 클래스 : 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스 +```java +public class Calculator { + public enum Operation1 { + PLUS(Integer::sum), + MINUS((x, y) -> x - y); + + private BiFunction calculate; + + Operation1(BiFunction calculate) { + this.calculate = calculate; + } + + public BiFunction getFunction() { + return calculate; + } + } + + + public int Sum(int x, int y) { + return Operation1.PLUS.getFunction().apply(x, y); + } +} +``` + +```java +public static void main(String[] args) { + Calculator c = new Calculator(); + System.out.println(c.Sum(1, 2)); // 3 + + System.out.println(Calculator.Operation1.PLUS); // PLUS + } +``` + +
+
+ +### 비정적 멤버 클래스 +- 비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결 +- 따라서, 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this 를 사용하여 바깥 인스턴스의 참조를 가져올 수 있음. +> 정규화된 this == 클래스명.this + +- 비 정적 멤버 클래스는 인스턴스를 감싸서 마치 다른 클래스처럼 보이게 하는 뷰인 어댑터에서 주로 쓰인다. +```java +public class TreeMap extends AbstractMap implements NavigableMap, Cloneable, java.io.Serializable { + // View class support + class Values extends AbstractCollection { + public Iterator iterator() { + return new ValueIterator(getFirstEntry()); + } + + public int size() { + return TreeMap.this.size(); + } + + public boolean contains(Object o) { + return TreeMap.this.containsValue(o); + } + + public boolean remove(Object o) { + for (Entry e = getFirstEntry(); e != null; e = successor(e)) { + if (valEquals(e.getValue(), o)) { + deleteEntry(e); + return true; + } + } + return false; + } + + public void clear() { + TreeMap.this.clear(); + } + + public Spliterator spliterator() { + return new ValueSpliterator<>(TreeMap.this, null, null, 0, -1, 0); + } + } + + + class EntrySet extends AbstractSet> { + public Iterator> iterator() { + return new EntryIterator(getFirstEntry()); + } + + public boolean contains(Object o) { + if (!(o instanceof Map.Entry)) + return false; + Map.Entry entry = (Map.Entry) o; + Object value = entry.getValue(); + Entry p = getEntry(entry.getKey()); + return p != null && valEquals(p.getValue(), value); + } + + public boolean remove(Object o) { + if (!(o instanceof Map.Entry)) + return false; + Map.Entry entry = (Map.Entry) o; + Object value = entry.getValue(); + Entry p = getEntry(entry.getKey()); + if (p != null && valEquals(p.getValue(), value)) { + deleteEntry(p); + return true; + } + return false; + } + } +} +``` +- TreeMap.this, getFirstEntry() 는 바깥 클래스의 인스턴스가 있어야 쓸 수 있다. +- 따라서 비정적 멤버 클래스의 인스턴스안에 관계정보가 저장되어 메모리 공간을 차지하고, 생성 시간도 더 걸린다. +- 또한 바깥 클래스의 인스턴스를 GC 가 회수하지 못한다. +```java + public Set> entrySet() { + EntrySet es = entrySet; + return (es != null) ? es : (entrySet = new EntrySet()); + } +``` +위의 코드에서 비정적 멤버 클래스 EntrySet의 객체를 생성한다. 암묵적으로 바깥 클래스와 멤버 클래스간의 연결되는 관계가 비정적 멤버 클래스의 인스턴스 안에 만들어지고, +Map 객체가 사용하는 곳이 없더라도 이 관계때문에 GC 가 일어나지 못한다. + +
+ +### 결론 +- 개념상 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있다면, 정적 멤버 클래스로 만들어야 한다. +- 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static 을 붙여서 정적 멤버 클래스로 만들자. \ No newline at end of file diff --git "a/Ch06/item34/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/Ch06/item34/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..a5edf9c --- /dev/null +++ "b/Ch06/item34/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,258 @@ +# item34. int 상수 대신 열거 타입을 사용하라 +> 열거 타입은 더 읽기 쉽고 안전하고 강력하다. 하나의 메서드가 상수별로 다르게 동작할 때가 있는데, 이때는 switch문 보다는 **상수별 메서드 구현**을 사용하자. 열거 타입 상수 일부가 같은 동작을 공유한다면 **전략 열거 타입 패턴**을 사용하자. + +## int enum pattern(정수 열거 패턴) +```java +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; +``` +- 타입 안전을 보장할 수 없다 +- 표현력이 좋지 않다 + - 예제에서는 사과용 상수는 APPLE_로, 오렌지용 상수는 ORANGE_로 시작한다 +- 정수 열거 패턴을 위한 별도 이름공간이 없다 +- 프로그램이 깨지기 쉽다 + - 평범한 상수를 나열한 것뿐이라 컴파일하면 그 값이 클라이언트 파일에 그대로 새겨진다 + - 상수의 값이 바뀌면 클라이언트도 반드시 다시 컴파일해야 한다 +- 문자열로 출력하기 까다롭다 + - 값을 출력하거나 디버거로 살펴보면 단지 숫자로만 보여서 썩 도움이 되지 않는다 + - 같은 정수 열거 그룹에 속한 모든 상수를 순회하는 방법도 마땅하지 않다 + - 또 이 안에 상수가 몇 개인지도 알 수 없다 + +### string enum pattern(문자열 상수를 사용하는 변형 패턴) +```java +public static final int APPLE_FUJI = "apple fuji"; +public static final int APPLE_PIPPIN = "apple pippin"; +public static final int APPLE_GRANNY_SMITH = "apple granny smith"; + +public static final int ORANGE_NAVEL = "orange navel"; +public static final int ORANGE_TEMPLE = "orange temple"; +public static final int ORANGE_BLOOD = "orange blood"; +``` +- 더 나쁘다 +- 상수의 의미를 출력할 수 있다는 점은 좋지만, 문자열 상수 이름 대신 문자열 값을 그대로 하드코딩하게 만들기 떄문이다 +- 문자열에 오타가 있어도 컴파일러는 확인할 길이 없으니 자연스럽게 런타임 버그가 생긴다 +- 문자열 비교는 비교적 성능 저하를 일으킨다 + +--- + +## ✅ enum type(열거 타입) +```java +public enum Apple { FUJI, PIPPIN, GRANNY_SMITH } +public enum Orange { NAVEL, TEMPLE, BLOOD } +``` + +### 장점 +- 열거 타입은 인스턴스 통제된다 + - 밖에서 접근할 수 있는 생성자를 제공하지 않으므로, 사실상 final이기 때문이다 +- 컴파일타임 타입 안정성을 제공한다 + - 위 코드에서 Apple의 열거 타입을 매개변수로 받는 메서드를 선언했다면, 건네받은 참조는 Apple의 세 가지 값 중 하나임이 확실하다 + - 다른 타입을 넘기려 하면 컴파일 오류가 난다 +- 각자의 이름공간이 있다 + - 이름이 같은 상수도 공존 가능하다 +- 클라이언트에는 아무 영향이 없다 + - 열거 타입에 새로운 상수를 추가하거나 순서를 바꿔도 다시 컴파일 하지 않아도 된다 +- 문자열로 출력하기 적합하다 + - `toString()`은 출력하기에 적합한 문자열을 내어준다 +- 임의의 메서드, 필드를 추가하고 임의의 인터페이스를 구현할 수도 있다 + - Object 메서드들을 높은 품질로 구현해놨다(3장) + - Comparable(item 14)과 Serializable(12장)을 구현해놨다 + - 직렬화 형태도 웬만큼 변형을 가해도 문제없이 동작하게끔 구현해놨다 + +## enum type 사용 경우 +### 1) 각 상수와 연관된 데이터를 해당 상수 자체에 내재시킬 때 +```java +/* 태양계의 여덟 행성 */ + +@Getter +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.447e7); + + 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; + this.surfaceGravity = G * mass / (radius * radius); + } + + public double surfaceWeight(double mass) { + return mass * surfaceGravity; + } +} +``` +- 열거 타입 상수 각각을 특정 데이터와 연관지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다 +- 열거 타입은 근본적으로 불변이라 모든 필드는 final이어야 한다(item 17) +- 필드를 public으로 선언해도 되지만 private로 두어 별도의 public 접근자 메서드를 두는게 낫다(item 16) + +어떤 객체의 지구에서의 무게를 입력받아 행성에서의 무게를 출력하는 일을 다음같이 짧게 작성할 수도 있다 +```java +public classs 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("%s에서 무게는 %f이다. %n", p, p.surfaceWeight(mass)); + } +} +``` + +### 2) 상수마다 동작이 달라져야 할 경우 +사칙연산 계산기의 연산 종류를 열거 타입으로 선언하고, 실제 연산까지 열거 타입 상수가 직접 수행하기로 한다 +```java +public enum Operation { + PLUS, MINUS, TIMES, DIVIDE +} +``` + +### switch문을 이용해 상수의 값에 따라 분기하는 방법 +```java +/* 상수가 뜻하는 연산을 수행한다 */ +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); +} +``` +- 깨지기 쉬운 코드이다 + - 새로운 상수를 추가하면 해당 case 문도 추가해주어야 한다 + +### ✅ 상수별 메서드 구현을 활용한 열거 타입 +```java +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); +} +``` +- 상수별 메서드 구현(constant-specific method implementation) + - 추상 메서드 `apply()`를 선언하고, 각 상수에서 자신에 맞게 재정의한다 +- `toString()`을 재정의해 연산 기호를 반환한다 + +### 단점 +- 열거 타입 상수끼리 코드를 공유하기 어렵다 +```java +/* 급여명세서에서 쓸 요일을 표현하는 열거 타입 + 직원의 (시간당) 기본 임금 + 그날 일한 시간(분 단위) -> 일당 계산 */ + +enum PayrollDay { + MONDAY, TUESDAY, WEDSDAY, 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; + } +} +``` +- 관리 관점에서 위험한 코드다 + - OCP 위반 + - 휴가와 같은 새로운 값을 열거 타입에 추가하려면, 그 값을 처리하는 case문을 잊지 말고 쌍으로 넣어줘야 한다 + +### ✅ 전략 열거 타입 패턴 +- 새로운 상수를 추가할 때 잔업수당 '전략'을 선택하도록 하는 것이다 +- 열거 타입 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자 +```java +enum PayrollDay { + MONDAY, TUESDAY, WEDSDAY, THURSDAY, FRIDAY, + SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND); + + private final PayType payType; + + PayrollDya(PayType payTyoe) {this.payType = payType;} + + int pay(int minutesWorked, int payRate) { + return payType.pay(minutesWorked, payRate); + } + + /* 전략 열거 타입 */ + enum PayType { + WEEKDAY { + int overtimePay(int minusWorked, int payRate) { + return minusWorked <= MINS_PER_SHIFT ? 0 : + (minusWorked - MINS_PER_SHIFT) * payRate / 2; + } + }, + WEEKEND { + int overtimePay(int minusWorked, int payRate) { + return minusWorked * 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); + } + } +} +``` +- 잔업 수당 계산을 `PayType`(전략 열거 타입)으로 옮기고, `PayrollDay`(열거 타입)의 생성자에서 이 중 적당한 것을 선택한다 +- `PayrollDay`은 잔업수당 계산을 `PayType`에 위임하여, switch 문이나 상수별 메서드 구현이 필요 없게 된다 + +### 때로는 switch문이 좋은 선택이 될 수도 있다 +- 추가하려는 메서드가 의미상 열거 타입에 속하지 않을 때 +```java +public static Operation inverse(Operation op) { + swith(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); + } +} +``` \ No newline at end of file diff --git "a/Ch06/item35/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/Ch06/item35/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..0738dd9 --- /dev/null +++ "b/Ch06/item35/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,50 @@ +# ordinal 메서드 대신 인스턴스 필드를 사용하라 +대부분의 열거 타입 상수는 자연스럽게 하나의 정숫값에 대응되며 모든 열거 타입은 해당 상수가 그 열거 타입에서 몇 번째 위치인지를 반환하는 ordinal이라는 메서드를 제공한다. + +#### 잘못 사용한 경우 +```java +public enum Ensemble { + SOLO, DUET, TRIO, QUARTET, QUINTET, SEXTET, SEPTET, OCTET, NONET, DECTET; + + public int numberOfMusicians() { + return ordinal() + 1; + } +} + +``` + +- 상수 선언 순서를 바꾸는 순간 numberOfMusicians 메서드의 기능을 상실하게 된다. -> 중간에 추가 되는 경우 +- 값을 중간에 비우기도 어려운 문제가 발생 +#### 인스턴스 필드를 사용하라 + +- 인스턴스 필드를 두어 값을 초기화 하여 사용 + +```java +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 numberOfMusicians) { + this.numberOfMusicians = numberOfMusicians; + } + + public int numberOfMusicians() { + return numberOfMusicians; + } +} +``` + +#### ordinal은 언제 사용하나? +- ordinal은 EnumSet이나 EnumMap 같은 열거 타입 기반의 범용 자료구조에 사용 될 목적으로 만들어 짐 diff --git "a/Ch06/item36/\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/Ch06/item36/\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..00cc18a --- /dev/null +++ "b/Ch06/item36/\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,89 @@ +### ☁️ 비트 필드 열거 상수 + +**열거한 값들이 단독이 아닌 집합으로 사용**되어야 할 경우, `EnumSet`이 나오기 전에는 각 상수에 서로 다른 2의 거듭제곱 값을 할당한 정수 열거 패턴을 사용했다. + +```java +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 + + public void applyStyles(int styles) { + System.out.printf("Applying styles %s to text%n", styles); + } +} +``` + +여러 상수를 하나의 집합으로 모을려면, 아래와 같이 비트 연산자 `OR`을 사용하면 된다. 이렇게 되면 비트 필드로 표현되는 하나의 집합을 생성할 수 있다. + +```java +text.applyStyles(STYLE_BOLD | STLYE_ITALIC); // 0001 OR 0010 = 0011 +``` + +> 🫧 **비트마스킹**
+비트 필드를 사용하여 비트별 연산을 통해 **집합 연산**을 수행하는 것을 의미한다. + +#### 비트 필드 단점 + +1. 예전에 보았던 정수 열거 상수의 단점을 그대로 지닌다.(타입 안전성 X, 이름 공간 충돌 등) +2. 비트 값 필드가 그대로 출력되었을 때 해석하기 어렵다. +3. 최대 몇 비트가 필요한지 예측해서 적절한 타입(int/long)을 선택해야 한다. + +```java +public static void main(String[] args) { + Text text = new Text(); + text.applyStyles(Text.STYLE_BOLD | Text.STYLE_ITALIC); +} +``` +![](https://velog.velcdn.com/images/semi-cloud/post/2a4d6200-5abf-42d5-bef4-a1c77dbea2cf/image.png) + + +### ☁️ EnumSet +`java.util` 패키지의 **EnumSet 클래스**는 **열거 타입 상수의 값으로 구성된 집합을 효과적으로 표현**해준다. + +![](https://velog.velcdn.com/images/semi-cloud/post/78de67ea-9c10-48c5-941b-e339dd1ea8f3/image.png) + + +`Set` 인터페이스 자체를 구현하고 있기 때문에 타입 안전하며, 출력했을 때도 알아보기 쉽게 정보를 담고 있다는 장점이 있다. + +> 🫧 **참고**
+`EnumSet` 은 비트 벡터로 구현되었기 때문에, 원소가 `64` 개 이하라면 `EnumSet` 전체를 `long` 변수 하나로 표현하여 비트 필드에 비견되는 성능을 보여준다. +```java +class RegularEnumSet> extends EnumSet { + private long elements = 0L; // 비트 필드 + } + ``` + ```java + class JumboEnumSet> extends EnumSet { + private long elements[]; +} +``` + +앞에서의 코드를 `EnumSet` 으로 수정해보자. + +```java +public class Text { + public enum Style {BOLD, ITALIC, UNDERLINE, STRIKETHROUGH} + + public void applyStyles(Set