├── README.md ├── chap02 ├── Item1.md ├── Item2.md ├── Item3.md ├── Item4.md ├── Item5.md ├── Item6.md ├── Item7.md ├── Item8.md └── Item9.md ├── chap03 ├── Item10.md ├── Item11.md ├── Item12.md ├── Item13.md └── Item14.md ├── chap04 ├── Item15.md ├── Item16.md ├── Item17.md ├── Item18.md ├── Item19.md ├── Item20.md ├── Item21.md ├── Item22.md ├── Item23.md ├── Item24.md └── Item25.md ├── chap05 ├── Item26.md ├── Item27.md ├── Item28.md ├── Item29.md ├── Item30.md ├── Item31.md ├── Item32.md └── Item33.md ├── chap06 ├── Item34.md ├── Item35.md ├── Item36.md ├── Item37.md ├── Item38.md ├── Item39.md ├── Item40.md └── Item41.md ├── chap07 ├── Item42.md ├── Item43.md ├── Item44.md ├── Item45.md ├── Item46.md ├── Item47.md └── Item48.md ├── chap08 ├── Item49.md ├── Item50.md ├── Item51.md ├── Item52.md ├── Item53.md ├── Item54.md ├── Item55.md └── Item56.md ├── chap09 ├── Item57.md ├── Item58.md ├── Item59.md ├── Item60.md ├── Item61.md ├── Item62.md ├── Item63.md ├── Item64.md ├── Item65.md ├── Item66.md ├── Item67.md └── Item68.md ├── chap10 ├── Item69.md ├── Item70.md ├── Item71.md ├── Item72.md ├── Item73.md ├── Item74.md ├── Item75.md ├── Item76.md └── Item77.md ├── chap11 ├── Item78.md ├── Item79.md ├── Item80.md ├── Item81.md ├── Item82.md ├── Item83.md └── Item84.md └── chap12 ├── Item85.md ├── Item86.md ├── Item87.md ├── Item88.md ├── Item89.md └── Item90.md /README.md: -------------------------------------------------------------------------------- 1 | # Effective-java 3/E 요약 2 | ### Effective Java 3/E 학습하면서 정리한 내용입니다. 3 | --- 4 | ### **chap02** 5 | - [[Item 1] 생성자 대신 정적 팩토리 메서드를 고려하라](https://github.com/brick0123/effective-java/blob/main/chap02/Item1.md) 6 | - [[Item 2] 생성자에 매개변수가 많다면 빌더를 고려하라](https://github.com/brick0123/effective-java/blob/main/chap02/Item2.md) 7 | - [[Item 3] private 생성자나 열거 타입으로 싱글턴임을 보증하라](https://github.com/brick0123/effective-java/blob/main/chap02/Item3.md) 8 | - [[Item 4] 인스턴스화를 막으려거든 private 생성자를 사용하라인스턴스화를 막으려거든 private 생성자를 사용하라](https://github.com/brick0123/effective-java/blob/main/chap02/Item4.md) 9 | - [[Item 5] 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라](https://github.com/brick0123/effective-java/blob/main/chap02/Item5.md) 10 | - [[Item 6] 불필요한 객체 생성을 피하라](https://github.com/brick0123/effective-java/blob/main/chap02/Item6.md) 11 | - [[Item 7] 다 쓴 객체 참조를 해제하라](https://github.com/brick0123/effective-java/blob/main/chap02/Item7.md) 12 | - [[Item 8]finalizer와 cleaner 사용을 피하라](https://github.com/brick0123/effective-java/blob/main/chap02/Item8.md) 13 | - [[Item 9] try-finally보다는 try-with-resources를 사용하라](https://github.com/brick0123/effective-java/blob/main/chap02/Item2.md) 14 | 15 | ### **chap03** 16 | - [[Item 10] equals는 일반 규약을 지켜 재정의하라](https://github.com/brick0123/effective-java/blob/main/chap03/Item10.md) 17 | - [[Item 11] equals를 재정의하려거든 hashCode도 재정의하라](https://github.com/brick0123/effective-java/blob/main/chap03/Item11.md) 18 | - [[Item 12] toString을 항상 재정의하라](https://github.com/brick0123/effective-java/blob/main/chap03/Item12.md) 19 | - [[Item 13] clone 재정의는 주의해서 진행하라](https://github.com/brick0123/effective-java/blob/main/chap03/Item13.md) 20 | - [[Item 14] Comparable을 구현할지 고려하라](https://github.com/brick0123/effective-java/blob/main/chap03/Item14.md) 21 | 22 | ## **chap04** 23 | - [[Item 15] 클래스와 멤버의 접근 권한을 최소화하라](https://github.com/brick0123/effective-java/blob/main/chap04/Item15.md) 24 | - [[Item 16] public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라](https://github.com/brick0123/effective-java/blob/main/chap04/Item16.md) 25 | - [[Item 17] 변경 가능성을 최소화하라](https://github.com/brick0123/effective-java/blob/main/chap04/Item17.md) 26 | - [[Item 18] 상속보다는 컴포지션을 사용하라](https://github.com/brick0123/effective-java/blob/main/chap04/Item18.md) 27 | - [[Item 19] 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라](https://github.com/brick0123/effective-java/blob/main/chap04/Item19.md) 28 | - [[Item 20] 추상 클래스보다는 인터페이스를 우선하라](https://github.com/brick0123/effective-java/blob/main/chap04/Item20.md) 29 | - [[Item 21] 인터페이스는 구현하는 쪽을 생각해 설계하라](https://github.com/brick0123/effective-java/blob/main/chap04/Item21.md) 30 | - [[Item 22] 인터페이스는 타입을 정의하는 용도로만 사용하라](https://github.com/brick0123/effective-java/blob/main/chap04/Item22.md) 31 | - [[Item 23] 태그 달린 클래스보다는 클래스 계층구조를 활용하라](https://github.com/brick0123/effective-java/blob/main/chap04/Item23.md) 32 | - [[Item 24] 멤버 클래스는 되도록 static으로 만들라](https://github.com/brick0123/effective-java/blob/main/chap04/Item24.md) 33 | - [[Item 25] 톱레벨 클래스는 한 파일에 하나만 담으라](https://github.com/brick0123/effective-java/blob/main/chap04/Item25.md) 34 | 35 | ## **chap05** 36 | - [[Item 26] 로 타입은 사용하지 말라](https://github.com/brick0123/effective-java/blob/main/chap05/Item26.md) 37 | - [[Item 27] 비검사 경고를 제거하라](https://github.com/brick0123/effective-java/blob/main/chap05/Item27.md) 38 | - [[Item 28] 배열보다는 리스트를 사용하라](https://github.com/brick0123/effective-java/blob/main/chap05/Item28.md) 39 | - [[Item 29] 이왕이면 제네릭 타입으로 만들라](https://github.com/brick0123/effective-java/blob/main/chap05/Item29.md) 40 | - [[Item 30] 이왕이면 제네릭 메서드로 만들라](https://github.com/brick0123/effective-java/blob/main/chap05/Item30.md) 41 | - [[Item 31] 한정적 와일드카드를 사용해 API 유연성을 높이라](https://github.com/brick0123/effective-java/blob/main/chap05/Item31.md) 42 | - [[Item 32] 제네릭과 가변인수를 함께 쓸 때는 신중하라](https://github.com/brick0123/effective-java/blob/main/chap05/Item32.md) 43 | - [[Item 33] 타입 안전 이종 컨테이너를 고려하라](https://github.com/brick0123/effective-java/blob/main/chap05/Item33.md) 44 | 45 | ## **chap06** 46 | - [[Item 34] int 상수 대신 열거 타입을 사용하라](https://github.com/brick0123/effective-java/blob/main/chap06/Item34.md) 47 | - [[Item 35] ordinal 메서드 대신 인스턴스 필드를 사용하라](https://github.com/brick0123/effective-java/blob/main/chap06/Item35.md) 48 | - [[Item 36] 비트 필드 대신 EnumSet을 사용하라](https://github.com/brick0123/effective-java/blob/main/chap06/Item36.md) 49 | - [[Item 37] ordinal 인덱싱 대신 EnumMap을 사용하라](https://github.com/brick0123/effective-java/blob/main/chap06/Item37.md) 50 | - [[Item 38] 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라](https://github.com/brick0123/effective-java/blob/main/chap06/Item38.md) 51 | - [[Item 39] 명명 패턴보다 애너테이션을 사용하라](https://github.com/brick0123/effective-java/blob/main/chap06/Item39.md) 52 | - [[Item 40] @Override 애너테이션을 일관되게 사용하라](https://github.com/brick0123/effective-java/blob/main/chap06/Item40.md) 53 | - [[Item 41] 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라](https://github.com/brick0123/effective-java/blob/main/chap06/Item41.md) 54 | 55 | ## **chap07** 56 | - [[Item 42] 매개변수가 유효한지 검사하라](https://github.com/brick0123/effective-java/blob/main/chap07/Item42.md) 57 | - [[Item 43] 매개변수가 유효한지 검사하라](https://github.com/brick0123/effective-java/blob/main/chap07/Item43.md) 58 | - [[Item 44] 매개변수가 유효한지 검사하라](https://github.com/brick0123/effective-java/blob/main/chap07/Item44.md) 59 | - [[Item 45] 매개변수가 유효한지 검사하라](https://github.com/brick0123/effective-java/blob/main/chap07/Item45.md) 60 | - [[Item 46] 매개변수가 유효한지 검사하라](https://github.com/brick0123/effective-java/blob/main/chap07/Item46.md) 61 | - [[Item 47] 매개변수가 유효한지 검사하라](https://github.com/brick0123/effective-java/blob/main/chap07/Item47.md) 62 | - [[Item 48] 매개변수가 유효한지 검사하라](https://github.com/brick0123/effective-java/blob/main/chap07/Item48.md) 63 | 64 | ## **chap08** 65 | - [[Item 49] 매개변수가 유효한지 검사하라](https://github.com/brick0123/effective-java/blob/main/chap08/Item49.md) 66 | - [[Item 51] 적시에 방어적 복사본을 만들라](https://github.com/brick0123/effective-java/blob/main/chap08/Item50.md) 67 | - [[Item 52] 다중정의는 신중히 사용하라](https://github.com/brick0123/effective-java/blob/main/chap08/Item50.md) 68 | - [[Item 53] 가변인수는 신중히 사용하라](https://github.com/brick0123/effective-java/blob/main/chap08/Item50.md) 69 | - [[Item 54] null이 아닌, 빈 컬렉션이나 배열을 반환하라](https://github.com/brick0123/effective-java/blob/main/chap08/Item50.md) 70 | - [[Item 55] 옵셔널 반환은 신중히 하라](https://github.com/brick0123/effective-java/blob/main/chap08/Item50.md) 71 | - [[Item 56] 공개된 API 요소에는 항상 문서화 주석을 작성하라](https://github.com/brick0123/effective-java/blob/main/chap08/Item50.md) 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /chap02/Item1.md: -------------------------------------------------------------------------------- 1 | # [Iteam 1] 생성자 대신 정적 팩토리 메서드를 고려하라 2 | 3 | ### 일반적으로 사용하는 public 생성자 대신, 별도로 **정적 팩토리 메소드**를 이용할 수 있다. 4 |
5 | Boolean 클래스에서 발췌한 예제 코드
6 | 7 | ``` java 8 | public static Boolean valueOf(boolean b) { 9 | return b ? Boolean.TRUE : Booelan.FALSE; 10 | } 11 | ``` 12 | 13 | ## **장점** 14 | ### **1. 이름을 가질 수 있다.**
15 | 생성자에 넘기는 매개변수만으로 반활된 객체의 특성을 제대로 설명하지 못합니다.
16 | 반면 정적 팩터리 메서드는 이름을 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있습니다.
17 | 하나의 시그니처로는 생성자를 하나만 만들 수 있으며 입력 매개변수들의 순서를 다르게 한 생성자를 추가하는 식으로 이 제한을 피할 수는 있지만,이 역시 객체의 특성을 살리기 힘들고 좋지 않은 방식입니다.
18 | 정적 팩토리 메소드는 이러한 제약이 없으며 시그니처가 같은 생성자가 여러 개 필요할 거 같으면
19 | 생성자를 정적 팩터리 메서드로 바꾸고 각각의 차이를 잘 드러내는 이름을 지어내는 방법을 사용할 수 있습니다. 20 |

21 | 22 | ### **2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.**
23 | 불변 클래스(immutable class)는 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있습니다. 따라서 객체가 자주 요청되는 상황이라면 성능을 상당히 끌어 올릴 수 있습니다.`Boolean.valueOf(boolean)` 메소드도 그 경우에 해당됩니다. 24 | 25 | ### **3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.**
26 | 반환할 객체의 클래스를 선택할 수 있는 유연함이 있습니다. 리턴 타입의 하위 타입의 인스턴스를 반환할 수 있으므로 가능합니다. API를 만들때 유연성을 이용하여 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있습니다.

27 | 자바 컬렉션 프레임워크는 핵심 인터페이스들에 수정 불가나 동기화 등의 기능을 덧붙인
28 | 총 45개의 유틸리티구현체를 제공하는데, 이 구현체는 대부분 단 하나의 인스턴스화 불가 클래스인`java.util.Collections`에서 정적 팩터리 메소드를 통해 얻도록 했습니다.

29 | 컬렉션 프레임워크는 이 45개의 클래스를 공개하지 않기 때문에 API의 외견을 훨씬 작게 만들 수 있었습니다.
30 | 여기서 무게는 프로그래머가 API를 사용하기 위해 익혀야하는 개념의 수와 난이도를 맗합니다.
31 | 클라이언트가 구현체가 아닌 인터페이스를 다루게 되므로 역할과 구현이 나눠지므로 결합도가 낮출 수 있습니다.

32 | ### **4.입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.**
33 | 장점 3에서 말했듯이 하위 타입이기만 하면 어떤 클래스의 객체를 반환하던 상관 없습니다.
34 | 가령 EumSet 클래스는 public 생성자 없이 오직 정적 펙토리 메서드만 제공합니다. `nonOf` 등..
nonOf 메서드를 살펴보면 원소 수에 따라 `RegularEnumSet`이나 `JumbEnumSet`인스턴스를 반환 합니다.
35 | ``` java 36 | public static > EnumSet noneOf(Class elementType) { 37 | Enum[] universe = getUniverse(elementType); 38 | if (universe == null) 39 | throw new ClassCastException(elementType + " not an enum"); 40 | 41 | if (universe.length <= 64) 42 | return new RegularEnumSet<>(elementType, universe); 43 | else 44 | return new JumboEnumSet<>(elementType, universe); 45 | } 46 | 47 | ``` 48 |

49 | ### **5.정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.** 50 | 대표적인 프레워크로는 JDBC가 있습니다. 구현체들은 클라이언트에 제공하는 역할을 프레임워크가 통제하여, 클라이언트를 구현체로부터 분리해줍니다.
51 | 52 | 서비스 제공자 프레임워크는 3개의 핵심 컴포넌트로 이루어집니다. (제공자는 서비스의 구현체를 의미합니다)
53 | - 서비스 인터페이스 (구현체의 동작을 정의) 54 | - 제공자 등록 API (제공자가 구현체를 등록할 때 사용) 55 | - 서비스 접근 API (클라이언트가 서비스의 인스터스를 얻을 때 사용) 56 | 57 | 서비스 접근 API를 사용할 때 원하는 구현체의 조건을 명시할 수 있습니다. 조건을 명시하지 않으면 기본 구현체를 반화하거나 지원하는 구현체들을 하나씩 돌아가면서 반환합니다. 58 | 59 | ## **단점** 60 | ### **1.상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩토리 메서드만 제공하면 하위 클래스를 만들 수 없다** 61 | 이 제약은 상속보다(is a) 컴포지션을(has) 사용하도록 유도하고 불변 타입으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점으로 받아들일 수도 있습니다. 62 |

63 | 64 | ### **2.정적 팩토리 메서드는 프로그래머가 찾기 어렵다.** 65 | 생성자처럼 API설명에 명확히 드러나지 않으므로 사용자는 정적 팩토리 메서드 사용 방식 클래스를 인스턴스화 시킬 방법을 알아내야 합니다. 그러므로 API 문서를 잘 정리하고 메서드 이름도 널리 알려진 규약을 따르는 게 좋습니다. 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /chap02/Item2.md: -------------------------------------------------------------------------------- 1 | # [Item 2] 생성자에 매개변수가 많다면 빌더를 고려해라. 2 |
3 | 매개변수가 많아질 경우 정적 팩토리 메서드와 생성자는 사용하기 불편해집니다.

4 | 5 | ### **첫번째 대안: 생성자를 이용할 경우** 6 |
7 | 8 | ```java 9 | Nutritionfact cocaCola = new Nutritionfact(240, 8, 100, 0, 35); 10 | ``` 11 | 이렇게 생성자를 만들 수 있지만 어떤 속성 값을 설정했는지 알기 힘듭니다. **매개변수의 수가 늘어날 수록 코드를 작성하거나 읽기 힘들어집니다.** 문제는 매개변수 타입이 같은 상황에서 실수로 순서를 바꿔 입력할 경우 컴파일 시점에서 알 수 없고 **런타임에 엉뚱하게 동작하게 됩니다.** 12 |

13 | 14 | ### **두번째 대안: 자바빈** 15 | 또 다른 대안으로는 매개변수가 없는 생성자를 만든 뒤 `setter`를 통해서 값을 설정한는 방법입니다. 16 |
17 | ```java 18 | Nutritionfact cocaCola = new Nutritionfact(); 19 | cocaCola.setServingSize(240); 20 | cocaCola.setServings(8); 21 | cocaCola.setCalories(100); 22 | .. 23 | 반복 24 | ``` 25 | 생성자를 이용한 방식보다 가독성이 훨씬 올라갔습니다. 하지만 객체가 완전히 생성되기 전까지는 중간에 사용할 수도 있으므로 일관성을 유지할 수 없습니다. 또한 불변 클래스로 만들 수 없다는 단점도 있으며(쓰레드 세이프하지 않습니다) 이를 해결하려면 추가 작업을 해줘야 합니다. 26 |

27 | ### **세번째 대안: 빌더** 28 | 발더 패턴은 필요한 매개변수만 전달할 수 있고 자바빈즈 패턴의 가독성을 모두 겸비한 대안입니다.

29 | 필수 매개변수만으로 생성자(혹은 정적팩토리)를 호출해 빌더 객체를 얻습니다.그 후 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수를 설정합니다.
30 | 마지막으로 매개변수가 없는`build`메서드를 호출하여 객체를 생성합니다. 31 |
32 | ``` java 33 | // 예제에서 사용한 Builder패턴 34 | 35 | public class NutritionFacts { 36 | 37 | private final int servingSize; 38 | private final int servings; 39 | private final int calories; 40 | private final int fat; 41 | private final int sodium; 42 | 43 | public static class Builder { 44 | 45 | // 필수 매개변수 46 | private final int servingSize; 47 | private final int servings; 48 | 49 | // 선택 매개변수 - 기본값으로 초기화 한다. 50 | private int calories = 0; 51 | private int fat = 0; 52 | private int sodium = 0; 53 | 54 | public Builder(int servingSize, int servings) { 55 | this.servingSize = servingSize; 56 | this.servings = servings; 57 | } 58 | 59 | public Builder calories(int val) { 60 | calories = val; 61 | return this; 62 | } 63 | 64 | public Builder fat(int val) { 65 | fat = val; 66 | return this; 67 | } 68 | 69 | public Builder carbohydrate(int val) { 70 | sodium = val; 71 | return this; 72 | } 73 | 74 | public NutritionFacts build() { 75 | return new NutritionFacts(this); 76 | } 77 | } 78 | 79 | private NutritionFacts(Builder builder) { 80 | this.servingSize = builder.servingSize; 81 | this.servings = builder.servings; 82 | this.calories = builder.calories; 83 | this.fat = builder.fat; 84 | this.sodium = builder.sodium; 85 | } 86 | 87 | } 88 | ``` 89 | ``` java 90 | NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) 91 | .calories(100) 92 | .sodium(35) 93 | .carbohydrate(27) 94 | .build(); 95 | ``` 96 |
97 | 아까보다 가독성도 좋아지고 NutritionFacts 클래스는 불변이 되었습니다. 추상 클래스는 추상 빌더를 갖게하고 구체 클래스는 구체 빌더를 갖게 합니다. 98 | 99 | ``` java 100 | public abstract class Pizza { 101 | 102 | public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE } 103 | final Set toppings; 104 | 105 | abstract static class Builder> { 106 | EnumSet toppings = EnumSet.noneOf(Topping.class); 107 | public T addTopping(Topping topping) { 108 | toppings.add(Objects.requireNonNull(topping)); 109 | return self(); 110 | } 111 | 112 | abstract Pizza build(); // Pizza를 상속한 인스턴스를 반환하기만 하면 된다. 113 | 114 | // 하위 클래스는 이 메서드를 재정의 하여 "this"를 반환하도록 해야 한다. 115 | protected abstract T self(); 116 | } 117 | 118 | Pizza(Builder builder) { 119 | toppings = builder.toppings.clone(); 120 | } 121 | 122 | } 123 | ``` 124 |
125 | 126 | ``` java 127 | public class NyPizza extends Pizza { 128 | 129 | public enum Size { SMALL, MEDIUM, LARGE } 130 | private final Size size; 131 | 132 | public static class Builder extends Pizza.Builder { 133 | private final Size size; 134 | 135 | public Builder(Size size) { 136 | this.size = Objects.requireNonNull(size); 137 | } 138 | 139 | @Override 140 | Pizza build() { 141 | return new NyPizza(this); 142 | } 143 | 144 | @Override 145 | protected Builder self() { 146 | return this; 147 | } 148 | 149 | } 150 | 151 | private NyPizza(Builder builder) { 152 | super(builder); 153 | size = builder.size; 154 | } 155 | 156 | ``` 157 |
158 | 159 | ``` java 160 | public class Calzone extends Pizza { 161 | 162 | private final boolean sauceInside; 163 | 164 | public static class Builder extends Pizza.Builder { 165 | private boolean sauceInside = false; 166 | 167 | public Builder sauceInde() { 168 | sauceInside = true; 169 | return this; 170 | } 171 | 172 | @Override 173 | public Calzone build() { 174 | return new Calzone(this); 175 | } 176 | 177 | @Override 178 | protected Builder self() { 179 | return this; 180 | } 181 | } 182 | 183 | private Calzone(Builder builder) { 184 | super(builder); 185 | sauceInside = builder.sauceInside; 186 | } 187 | 188 | } 189 | ```` 190 | build 메서드는 하위 클래스 어떤 걸 반환해도 상관 없습니다. 하위 메서드가 상위 클래스의 메서드 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능을 공반환 타이핑이라 합니다.
191 | ``` java 192 | Pizza pizza = new Builder(SMALL) 193 | .addTopping(SAUSAGE).addTopping(ONION).build(); 194 | 195 | Calzone calzone = new Calzone.Builder() 196 | .addTopping(HAM).sauceInde().build(); 197 | ``` 198 | 199 | 빌더는 다양한 방식으로 객체를 생생할 수 있으므로 생성자와 정적 팩토리 메서드 방식보다 상당히 유연합니다.
200 | 201 | 단점으로는 객체를 생성하려면 Builder()를 생성해야하는데 성능에 민감한 상황에서는 이 점이 문제가 될 수 있습니다.그리고 생성자에 비해서 코드가 장황에 질 수 있으므로 현재 필요한 매개변수와 확장성을 고려해서 잘 판단하면 될 거 같습니다. 202 | 203 | -------------------------------------------------------------------------------- /chap02/Item3.md: -------------------------------------------------------------------------------- 1 | # [Item 3] private 생성자나 열거 타입으로 싱글턴임을 보증하라 2 | 3 | 싱글턴(singletone)이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말합니다. 즉 객체를 호출할 때마다 new해서 새로 생성하지 않고 하나의 인스턴스를 계속 사용하는 것입니다.
4 | 싱글턴을 만드는 방식은 보통 둘 중 하나입니다. 5 | 6 | ### **public static 멤버가 final인 방식** 7 | ``` java 8 | public class Elvis { 9 | 10 | public static final Elvis INSTANCE = new Elvis(); 11 | 12 | private void Elvis() { ... } 13 | 14 | private void leaveTheBuilding() { ... } 15 | 16 | } 17 | ``` 18 | public이나 protected 생성자가 없으므로 Elvis 클래스가 초기화 될 때 만들어진 인스턴스는 하나 뿐입니다. 단 권한이 있는 클라이언트는 리플렉션 API인 AccessibleObject.setAccessible을 사용해 private 생성자를 호출할 수 있습니다.
public 필드 방식의 첫번째 장점은 해당 클래스가 싱글턴임이 API에 명백히 드러나는 것입니다.
19 | 두 번째 장점은 간결하다는 것입니다. 20 | 이 점은 생성자를 수정하여 두 번째 객체가 생성되려고 하면 예외를 단지면 됩니다. 21 | 22 |
23 | 싱글턴을 만드는 두 번째 방법에는 정적 팩토리 메서드를 public static 멤버로 제공합니다. 24 | 25 | ### **정적 팩토리 방식의 싱글톤** 26 | ``` java 27 | public class Elvis { 28 | 29 | private static final Elvis INSTANCE = new Elvis(); 30 | 31 | private void Elvis() { ... } 32 | public static Elvis getInstance() { return INSTANCE; } 33 | 34 | 35 | private void leaveTheBuilding() { ... } 36 | 37 | } 38 | ``` 39 | 40 | 이 역시 리플렉션을 통한 예외는 똑같이 적용됩니다.
41 | 장점 42 | - API를 바꾸지 않고 싱글턴이 아니게 변경할 수 있습니다. 43 | - 유일한 인스턴스를 반환하던 팩터리 메서드가 호출하는 스레드별로 다른 인스턴스를 넘겨주게 할 수 있습니다. 44 | - 정적 팩토리 메서드 참조를 공급자(supplier)로 사용할 수 있습니다. 45 |
46 | 47 | 위에서 살펴본 두 방법 모두 직렬화하려면 모든 인스턴스 필드를 일시적(transient)이라고 선언하고 `readResolve` 메서드를 다음과 같이 제공해야 합니다. 48 | ``` java 49 | private Object readResolve() { 50 | returm INSTANCE; 51 | } 52 | ``` 53 | 이렇게 하지 않으면 역직렬화 할때마다 새로운 인스턴스가 생성됩니다.

54 | 55 | ### **열거 타입의 방식의 싱글턴** 56 | ``` java 57 | public enum Elvis { 58 | INSTANCEl 59 | 60 | public void leaveTheBuilding() { ... } 61 | } 62 | ``` 63 | 직렬화 상황 그리고 리플렉션 공격에서도 싱글턴임을 보장할 수 있습니다. **대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법입니다.**
64 | 하지만 이 방법은 Enum 외의 클래스를 상속해야한다면 사용할 수 없습니다.(인터페이스를 구현할 수는 있습니다) 65 | -------------------------------------------------------------------------------- /chap02/Item4.md: -------------------------------------------------------------------------------- 1 | # [Item 4] 인스턴스를 막으려거든 private 생성자를 사용하라 2 | 3 | java.lang.Math와 java.util.Arrays와 같이 static 메서드와 static 필드만을 담을 클래스는 인스턴스화를 하는 건 낭비가 됩니다. 4 |
5 | 매개변수 없는 생성자를 만들지 않으면 기본생성자가 생성이 되는데 이것을 방치하면 클라이언트 입장에서는 정적 멤버만 담은 유틸리티 클래스인지 알 수 없으므로 인스턴스화를 시킬 가능성이 있습니다. 이를 방지하기 위해 private 생성자를 만들어서 인스턴스화를 막을 수 있습니다. 6 |
7 | ``` java 8 | public class UtilityClass { 9 | 10 | private UtilityClass() { } 11 | 12 | } 13 | ``` 14 | private 생성자이므로 상속을 시도하려는 클래스에서는 생성자를 호출할 수 없기 때문에 상속도 불가능 합니다. -------------------------------------------------------------------------------- /chap02/Item5.md: -------------------------------------------------------------------------------- 1 | # [Item 5] 자원을 직접 명시하지 말고 의존객체를 주입을 사용하여라. 2 | 3 | 대부분의 클래스는 하나 이상의 리소스에 의존합니다. 이번 예제에서는 `SpellChecker`가 `Lexicon`를 의존하고 있는 모습입니다. 4 |
5 | ## 부적절한 구현 6 | ### **정적 유틸리티를 잘못 사용한 예 - 유연하지 않고 테스트 할 수 없다** 7 | 8 | ``` java 9 | public class SpellChecker { 10 | 11 | private static final Lexicon dictionary = ...; 12 | 13 | private SpellChecker() { } 14 | 15 | public static boolean isValid(String word) { ... } 16 | public static List suggestions(String typo) { } 17 | } 18 | ``` 19 | ### **싱글턴을 잘못 사용한 예 - 유연하지 않고 테스트하기 어렵다** 20 | ``` java 21 | public class SpellChecker { 22 | 23 | private final Lexicon dictionary = ...; 24 | 25 | private SpellChecker() { } 26 | public static SpellChecker INSTANCE = new SpellChecker(); 27 | 28 | public static boolean isValid(String word) { ... } 29 | public static List suggestions(String typo) { ... } 30 | } 31 | ``` 32 | 여기서 말하는 유연함이란 구현체를 변경하는 용이성을 말합니다. 33 |
34 | 사용하는 리소스에 따라 행동을 다르게 해야하는 클래스는 위에 말한 두 방식을 사용하는 것은 부적절합니다. 이러한 불편 사항들을 해결하기 위해서는 인스턴스를 생성할 때 생성자에 자원을 넘겨주는 방식이 있습니다. 35 | ## 적절한 구현 36 | ### **의존 객체 주입은 유연성과 테스트 용이성을 높입니다.** 37 |
38 | 39 | ``` java 40 | public class SpellChecker { 41 | 42 | private final Lexicon dictionary; 43 | 44 | public SpellChecker(Lexicon dictionary) { 45 | this.dictionary = dictionary; 46 | } 47 | 48 | public static boolean isValid(String word) { ... } 49 | public static List suggestions(String typo) { } 50 | 51 | } 52 | ``` 53 | 흔히 생성자 주입 방식이라고 부르고 이러한 방식은 불변을 보장하며 원하면 하위 타입 인스턴스의 다양한 구현체를 주입할 수 있습니다.
54 | 위와 같이 의존성을 주입하는 방식은 유연함과 테스트 용이함을 크게 향상시켜 주지만, 의존성이 많아질 경우 코드가 장황해질 수 있습니다.
55 | 하지만 이 점은 스프링 같은 프레임워크로 해결할 수 있습니다.
56 | 요약하자면 의존하는 리소스에 따라 행동을 달리하는 클레스를 사용할 때는 싱글턴이나 스태틱 유틸클래스를 사용하지말고 57 | 리소스를 생성자를 통해 의존성을 주입받는 걸 추천합니다. 58 | -------------------------------------------------------------------------------- /chap02/Item6.md: -------------------------------------------------------------------------------- 1 | # [Item 6] 불피요한 객체를 생성을 피하라 2 | 기능이 똑같은 객체를 매번 생성하기 보다는 객체를 재사용하는 것이 적절합니다. 특히 **불변 객체는 항상 재사용할 수 있습니다.** 3 | 4 | ## **문자열 생성 방법** 5 | ### **객체 생성 방식 - 피해야되는 예시** 6 | ``` java 7 | String str = new String("hello"); 8 | ``` 9 | 이 방식을 이용하면 똑같은 문자열을 생성하더라도 항상 새로운 객체를 생성하므로 낭비가 됩니다. 10 | ### **리터럴 방식** 11 | ``` java 12 | String str = "hello"; 13 | ``` 14 | `리터럴` 방식을 사용하면 JVM에서 동일한 문자열이 존재한다면 그 리터럴을 재사용합니다. 15 |

16 | 17 | ## 생성 비용이 비싼 객체 18 | 생성 비용이 비싼 객체들 같은 경우 캐싱해서 재사용할 수 있는 방법을 고려해보는 게 좋습니다. 19 | 20 | ### **불필요한 객체 생성 1** 21 | ``` java 22 | static boolean isRomanNumeral(String s) { 23 | return s.matches("^(?=.)M*(C[MD]|D?C{0,3})" 24 | + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"); 25 | } 26 | ``` 27 | 정규표현식의 예제로 문자열이 로마 숫자인지 확인하는 코드입니다.
28 | `String.matches`는 내부에서 만드는 `Pattern`객체를 만들어서 사용하는데, 이 메서드에서는 한 번 쓰고 버려져서 곧바로 가비지 컬렉션 대상이 됩니다. 입력받은 정규표현식에 해당하는 `유한 상태 머신`을 만들기 때문에 생성 비용이 비쌉니다. 29 | ### **해결책** 30 | ``` java 31 | public class RomanNumber { 32 | 33 | private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"); 34 | 35 | static boolean isRomanNumeral(String s) { 36 | return ROMAN.matcher(s).matches(); 37 | } 38 | 39 | } 40 | ``` 41 | 성능을 개선한 코드 수정입니다.`Pattern`인스턴스 클래스 초기화 과정에서 직접 생성하고 캐싱해두었다가 해당 isRomanNumeral가 호출될 때마다 인스턴스를 재사용합니다. isRomanNumeral가 여러변 호출되는 상황에서 성능을 상당히 끌어올릴 수 있습니다. 42 | 43 | ### **어댑터** 44 | 어댑터는 인터페이스를 통해서 다른 객체의 연결해주는 객체라 여러개 만들 필요가 없습니다.
45 | 대표적으로 `Map` 인터페이스에 있는 `keySet` 메서드는 호출 될 때마다 새로운 객체가 나오는 게 아니라 같은 객체를 반환하기 때문에 리턴받은 `Set` 타입의 객체를 변경하면, 결국 그 뒤에 있는 `Map` 인터페이스도 변경되는 것입니다. 46 | -------------------------------------------------------------------------------- /chap02/Item7.md: -------------------------------------------------------------------------------- 1 | # [Item 7] 다 쓴 객체 참조를 해제하라 2 | **메모리 누수가 일어나는 예제 코드** 3 | ``` java 4 | public class Stack { 5 | private Object[] elements; 6 | private int size = 0; 7 | private static final int DEFAULT_INITIAL_CAPACITY = 16; 8 | 9 | public Stack() { 10 | elements = new Object[DEFAULT_INITIAL_CAPACITY]; 11 | } 12 | 13 | public void push(Object e) { 14 | ensureCapacity(); 15 | elements[size++] = e; 16 | } 17 | 18 | public Object pop() { 19 | if (size == 0) 20 | throw new EmptyStackException(); 21 | return elements[--size]; // 주의 22 | } 23 | 24 | private void ensureCapacity() { 25 | if (elements.length == size) 26 | elements = Arrays.copyOf(elements, 2 * size); 27 | } 28 | } 29 | ``` 30 | 스택에서 데이터를 `pop`을 실행해도 메모리는 줄어들지 않습니다. 왜냐면 `size`를 감소 시키지만 배열에서 여전히 레퍼런스는 그대로 갖고있기 때문입니다. 31 |
32 | 다음과 같이 코드를 수정할 수 있습니다. 33 | ``` java 34 | public Object pop() { 35 | if (size == 0) { 36 | throw new EmptyStackException(); 37 | } 38 | 39 | Object value = this.elements[--size]; 40 | this.elements[size] = null; 41 | return value; 42 | } 43 | ``` 44 | 45 | size를 감소시킬 뿐만 아니라 실제로 해당 참조를 `null`로 처리한 방법입니다. 46 |
47 | 하지만 객체 처리를 null로 처리하는 건 예외적인 경우여야 합니다. 다 쓴 참조를 해제하는 가장 좋은 방법은 다 쓴 참조를 담은 변수를 특정한 스코프 안에서만 사용하는 것입니다. 48 |

49 | 캐시를 사용할 때도 메모리 누수에 유의해야 합니다. 50 | - 외부에서 키를 참조하는 동안만 엔트리가 살아있는 캐시가 필요하다면 `WeakHashMap`을 사용합니다. 51 | - 시간이 지날 수록 엔티리의 가치를 떨어뜨리는 방식. 52 | 53 | 54 | -------------------------------------------------------------------------------- /chap02/Item8.md: -------------------------------------------------------------------------------- 1 | # [Item 8] finalizer와 cleaner 사용을 피하라 2 | 3 | 자바에서는 두 가지 객체 소멸자를 제공합니다. 바로 `finalizer`와 `cleaner`입니다. `finalizer`는 예측 불가능하며 일반적으로 불필요하며 오작동 낮은 성능과 같은 문제점들이 있습니다. 자바 9부터는 사용 자제(**deprecated**) API로 지정했습니다. 4 |
5 | cleaner는 finalizer보다 덜 위험하지만 이 역시 예측 불가능하고, 느리고 일반적으로 불필요합니다. 6 | ## 단점 7 | ### **언제 실행되는지 알 수가 없다** 8 | finalizer와 cleaner는 즉시 실행된다는 보장이 없습니다. 즉 제때 실행되어야 하는 작업을 절대 할 수 없다는 뜻입니다. 9 | ### **finalizer는 굉장히 lazy 합니다** 10 | 여기서 lazy하다는 뜻은 인스턴스의 자원 회수가 멋대로 지연될 수 있다는 뜻입니다. 11 | finalizer 쓰레드는 우선 순위가 낮아서 실행될 기회를 제대로 얻지 못하는 경우들도 생깁니다. 12 | ### **수행 여부가 보장되지 않습니다** 13 | 자바 언어 명세는 finalizer와 cleaner의 수행 시점과 수행 여부조차 보장하지 않습니다. 14 | 따라서 **상태를 수정하는 작업에는 절대 finalizer나 cleaner에 의존해서는 안 됩니다.** 15 | ### **예외가 무시됩니다.** 16 | finalizer가 동작하면서 발생한 예외는 무시되며 처리할 작업이 남아있더라도 그 순간 종료됩니다. finalzer같은 경우 예외가 발생해도 추적을 할 수 없으며 이는 디버깅하는데 큰 문제가 됩니다. 17 | ### **성능상 이슈** 18 | `Autocloseable` 객체를 만들고 가비지 컬렉터가 수거하기 전까지 12ns가 걸린 반면, finalizerf를 사용하면 550ns가 소요 됐습니다. finalizer가 가비지 컬렉터의 효율을 떨어뜨리기 때문입니다.
19 |
뿐만 아니라 다양한 이슈들로 인해 `finalizer`와 `cleaner`의 사용을 추천하고 있지 않습니다. -------------------------------------------------------------------------------- /chap02/Item9.md: -------------------------------------------------------------------------------- 1 | # [Item 9] try-finally보다는 try-with-resources를 사용하라 2 | 자바 라이브러리에는 `InputStream`, `OutputStream`, `java.sql.Connection`과 같이 직접 닫아(close)해줘야 하는 자원들 있습니다. 클라이언트는 실수로 자원을 닫아주지 않는 경우 예상치 못한 성능 문제로 이어질 수 있습니다. 3 | ``` java 4 | static String firstLineOfFile(String path) throws IOException { 5 | 6 | BufferedReader br = new BufferedReader(new FileReader(path)); 7 | try { 8 | return br.readLine(); 9 | } finally { 10 | br.close(); 11 | } 12 | } 13 | ``` 14 | 나쁘진 않지만 자원을 더 사용하게 되면 살수가 나올 가능성이 큽니다. 15 | 위 예제 같은 경우 try 블록과 finally 블록 모두에서 발생할 수 있는데, 예컨데 기기에 물리적 문제가 생긴다면 firstLineOfFile 메서드 안의 readLine 메서드가 예외를 던지고, 같은 이유로 close 메서드도 실패합니다. 이런 상황이라면 두 번째 예외가 첫 번째 예외를 완전히 집어삼키게 됩니다.(첫 번째 예외는 정보가 남지 않게 됩니다) 16 |

17 | 이러한 문제점들을 고안해서 자바 7부터는 `try-with-resources`으로 해결할 수 있습니다. 이 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해야 합니다.
자바 라이브러리와 서드파티 라이브러리들의 수많은 클래스와 인터페이스가 이미 Autocloseable을 구현하거나 확정했습니다.
18 | try-with-resources를 사용한 코드 19 | ``` java 20 | static String firstLineOfFile(String path) throws IOException { 21 | try (BufferedReader br = new BufferedReader(new FileReader(path))) { 22 | return br.readLine(); 23 | } 24 | ``` 25 | try-with-resources에서도 catch 절을 사용할 수 있습니다. 26 | ``` java 27 | static String firstLineOfFile(String path) { 28 | try (BufferedReader br = new BufferedReader(new FileReader(path))) { 29 | return br.readLine(); 30 | } catch (IOException e) { 31 | return defaultVal; 32 | } 33 | } 34 | ``` 35 | 36 | -------------------------------------------------------------------------------- /chap03/Item10.md: -------------------------------------------------------------------------------- 1 | # [Item 10] equals는 일반 규약을 지켜 재정의하라. 2 | equals 메서드를 오버라이딩 하는 경우는 논리적인 동치성을 확인하기 위해서입니다.여기서 말하는 논리적 동치성은 쉽게 말하자면 참조값을 비교하는 게 아닌 객체의 값이 같은지 비교하기 위함이라고 할 수 있습니다.

3 | equals메서드를 오버라아딩 할 때는 다음의 규약을 따라야 합니다. 4 |
5 | ### **반사성(reflexivity)** 6 | - null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true입니다. 7 | ### **대칭성(symmetry)** 8 | - null이 아닌 모든 참조 값 x, y에 대해 x.equals(y)는 y.equals(x)입니다. 9 | 10 | **잘못된 코드 - 대칭성 위반** 11 | ``` java 12 | public class CaseInsensitiveString { 13 | 14 | private String str; 15 | 16 | ... 생략 17 | 18 | @Override 19 | public boolean equals(Object o) { 20 | if (o instanceof CaseInsensitiveString) { 21 | return str.equalsIgnoreCase(((CaseInsensitiveString) o).str); 22 | } 23 | if (o instanceof String) { 24 | return str.equalsIgnoreCase((String) o); 25 | } 26 | return false; 27 | } 28 | } 29 | ``` 30 | ``` java 31 | CaseInsensitiveString cis = new CaseInsensitiveString("String"); 32 | String str = "string"; 33 | ``` 34 | cis.equals(str)는 true를 반환하고 str.equals(cis)는 false를 반환하게 되므로 대칭성에 위반됩니다. 35 | 36 | ### **추이성(transitivity)** 37 | - null이 아닌 모든 참조 값 x, y, z에 대해 x.equals(y)가 true고 y.equals(z)도 true입니다. 38 | ### **일관성(consistency)** 39 | - null이 아닌 모든 참조 값 x, y에 대해 x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환해야 합니다. 40 | ### **null-아님** 41 | - null이 아닌 모든 참조 값 x에 대해 x.equals(null)은 false다 42 | 43 | 이제 상위 클래스에 없는 필드를 하위 클래스에 추가하는 상황을 생각해봅시다. 여기서부터 신경써야 할 부분들이 많아집니다. 44 | ``` java 45 | public class Point { 46 | 47 | private int x; 48 | private int y; 49 | 50 | ... 생략 51 | 52 | @Override 53 | public boolean equals(Object o) { 54 | if (!(o instanceof Point)) { 55 | return false; 56 | } 57 | Point p = (Point) o; 58 | return this.x == p.x && this.y == p.y; 59 | } 60 | } 61 | ``` 62 | Point클래스를 확장해봅시다. 63 | ``` java 64 | public class CirclePoint extends Point{ 65 | 66 | private int x; 67 | private int y; 68 | private int z; 69 | 70 | public CirclePoint(int x, int y, int z) { 71 | super(x, y); 72 | this.z = z; 73 | } 74 | 75 | ... 생략 76 | } 77 | ``` 78 | 이대로 사용하면 Point의 구현이 상속되어 x, y만 비교하게 되므로 생각했던 결과랑 실제 결과값이 다르게 나오는 상황이 발생합니다.
79 | 객체 지향의 추상화의 이점을 포기하지 않는 이상 아쉽게도 이러한 문제점들을 모두 완전하게 해결할 수 있는 방법은 없습니다. 80 |
81 | ### **양질의 equals메서드 구현 단계** 82 | 1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인합니다. 83 | 2. **insetanceof** 연산자로 입력이 올바른 타입인지 확인합니다. 84 | 3. 입력을 올바른 타입으로 형변환 합니다. 85 | 4. 입력받은 객체와 자기 자신의 대응되는 필드들이 모두 일치하는지 검사합니다. 86 | 87 | 어떤 필드를 먼저 비교하느냐에 따라 성능의 차이도 생깁니다. 값이 다를 가능성이 크거나 비교하는 비용이 싼 필드를 먼저 비교하면 성능상 이점을 얻을 수 있습니다.(틀리면 다음 로직을 실행하지 않기에) 88 | 89 | -------------------------------------------------------------------------------- /chap03/Item11.md: -------------------------------------------------------------------------------- 1 | # [Item 11] equals를 재정의하려거든 hashCode도 재정의하라. 2 | 3 | equals와 hasoCode를 재정의 하지 않으면 `HashMap`이나 `HashSet`에서 같은 원소를 사용할 때 문제가 발생합니다. 4 |
5 | 1. equals 비교에 사용되는 정보가 변경되지 않는다면, 애플리케이션이 실행되는 동안 객체의 hashCode메서드는 여러번 호출해도 일관된 값을 반환해야 합니다. 6 | 2. equals가 두 객체를 같다고 판단하면 hashCode 또한 같은 값을 반환해야 합니다. 7 | 3. equals가 두 객체를 다르다고 판단해도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없습니다. 단, 다른 객체에 대해서는 다른 값을 반환해야 해쉬테이블의 성능이 좋아집니다. 8 | 9 | 서로 다른 인스턴스에 대해서 모두 다른 해시코드를 반환하면 좋겠지만 hashCode는 int형이므로 2^32만큼의 경우의 수로 제한되어 있기 때문에 비둘기 집의 원리로 예외가 생길 수 있습니다.
10 | 다음은 hashCode를 작성하는 요령입니다. 11 | 12 | 1. int 변수 result를 선언한 후 값 c로 초기화 합니다. 이때 c는 해당 객체의 첫번째 핵시 필드를 2-a 방식으로 계산한 해시코드입니다. 13 | 2. 해당 개게의 나머지 필드 f 각각에 대해서 다음 작업을 수행합니다.
14 | a. 해당 필드의 해시코드를 c를 계산합니다.
15 | - 기본 타입 필드라면, Type.hashCode(f)를 수행합니다. 여기서 Type은 해당 기본 타입의 박싱 클래스입니다. 16 | - 참조 타입 필드면서 이 클래스의 equals 메서드가 이 필드의 equals를 재귀적으로 호출해 비교한다면, 이 필드의 hashCode를 재귀적으로 호출합니다. 17 | - 필드가 배열이라면, 원소 각각을 별도 필드처럼 다룹니다. 이상의 규칙을 재귀적으로 적용해 각 핵심 원소의 해시코드를 게산한 다음, 2.b방식으로 갱신합니다. 배열에 핵심 원소가 하나도 없다면 단순 상수를 사용하고 모든 원소가 핵심 원소라면 Arrays.hashCode를 사용합니다. 18 | - 19 |
20 | b. 단계 2.a에서 계산한 해시코드 c로 result를 갱신합니다. 21 | result = 31 * result + c; 22 |
23 | 3. result를 반환합니다. 24 | 25 |
26 | 27 | `equals` 비교에서 사용되지 않는 필든 '반드시'제외해야 합니다. 그렇지 않으면 맨 처음에 언급했던 두 번째 규약을 어기게됩니다.
28 | 숫자를 곱하는 이유는 만약 곱셉 없는 hahCode를 구현하게 되면 모든 아나그램의 해시코드가 같아집니다. 그리고 31을 선택한 이유는 홀수이면서 소수이기 때문입니다.
29 | ``` java 30 | 전형적인 hashCode 메서드 31 | 32 | @Override 33 | public int hashCode() { 34 | int result = Short.hashCode(areaCode); 35 | result = 31 * result + Short.hashCode(prefix); 36 | result = 31 * result + Short.hashCode(lineNum); 37 | return result; 38 | } 39 | ``` 40 | ``` java 41 | 한 줄짜리 hasoCode 메서드 - 성능이 살짝 아쉽습니다. 42 | @Override 43 | public int hashCode(){ 44 | return Objects.hash(lineNum, prefix, areaCode); 45 | } 46 | ``` 47 | ### 정리 48 | `equals`를 재정의 할 때는 `hashCdoe`도 재정의해야 합니다. 그렇지 않으면 프로그래밍이 제대로 동작하지 않을 수 있습니다. 서로 다른 인스턴스라면 되도록 해시코드도 서로 다르게 구현해야 합니다. 49 | -------------------------------------------------------------------------------- /chap03/Item12.md: -------------------------------------------------------------------------------- 1 | # [Item 12] toString을 항상 재정의하라. 2 | toString을 오버라이딩 하지 않으면 기본적으로 **클래스_이름@16진수로_표현한_해시코드**를 반환합니다.
3 | 이렇게 되면 객체의 특성을 알아볼 수 있으므로 `toString`을 재정의 할 필요가 있습니다. 4 | ``` java 5 | Student student = new Student("kim", 16); 6 | System.out.println(student); 7 | ``` 8 | 위 코드를 실행하면 Student@abcd같은 형태로 콘솔에 출력되므로 객체의 특성을 파악하기가 힘듭니다. 9 | 10 | ### **toString 재정의하는 좋은 방법.** 11 | - 객체가 가진 가진 **주요 정보를 모두 반환**하는 게 좋습니다. 12 | - toString을 구현하면 반환값의 포맷을 문서화할지 정해야 합니다. 13 | - 규칙이 명확해지는 장점이 있지만, 한번 명시하면 그 포맷에 얽매이게 됩니다. 14 | - 이러한 문제는 롬복으로 해결할 수 있습니다. 15 | - 포맷을 명시하든 아니든 여러분의 **의도**는 명확하게 밝혀져야합니다. 16 | - toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공합시다. 17 | 18 | ### **정리** 19 | 객체의 정보를 출력할 일이 있으면 toString을 재정의 합시다. toString을 재정의하면 사용자가 보기 편하고 디버깅을 쉽게 해줍니다. toString은 객체에 관해 명확하고 정보를 읽기 좋은 형태로 반환해야합니다. -------------------------------------------------------------------------------- /chap03/Item13.md: -------------------------------------------------------------------------------- 1 | # [Item 13] clone 재정의는 주의해서 진행하라. 2 | 3 | 실무에서 `Cloneable`을 구현한 클래스는 clone 메서드를 public으로 제공하며, 사용자는 당연히 복제가 제대로 이뤄지리라 기대합니다. 4 | 하지만 clone 메서드의 일반 규약은 허술한 부분이 있습니다. 다음은 Object 명세에서 가져온 설명입니다. 5 |

6 | 이 객체의 복사본을 생성해 반환합니다. '복사'의 정확한 뜻은 그 객체를 구현한 클래스에 따라 다를 수 있지만 일반적은 의도는 다음과 같습니다. 어떤 객체 x에 대해 다음 식은 참입니다. 7 | - x.clone() != x 8 | - x.clone.getClass() = x.getClass() 9 | - x.clone().equals(x) 10 | 11 | clone을 사용하는 방법은 굉장히 쉽습니다. Cloneable인터페이스를 구현하고 super.clone을 호출하면 됩니다. 이렇게 얻은 객체는 원본의 모든 필드랑 똑같은 값을 가지게 됩니다. 12 | ``` java 13 | @Override 14 | public PhoneNumber clone() { 15 | try{ 16 | return (PhoneNumber) super.class(); 17 | } catch (CloneNotSupportedException e) { 18 | throw new AssertionsError(); 19 | } 20 | } 21 | ``` 22 | Obejct의 clone은 Object를 반환하지만 공변 반환 타입을 이용해서 PhoneNumber로 반환했습니다. 이 방식으로 사용하는 클라이언트는 형변환을 따로 해줄 필요가 없습니다. 23 |
24 | 간단했던 앞서의 구현이 클래스가 가변 객체를 참조하는 순간 문제점이 발생합니다. 25 | ``` java 26 | public class Stack { 27 | private Object[] elements; 28 | private int size = 0; 29 | private static final int DEFAULT_INITIAL_CAPACITY = 16; 30 | 31 | public Stack() { 32 | this.elements = new Object[DEFAULT_INITIAL_CAPACITY]; 33 | } 34 | 35 | ... 생략 36 | } 37 | ``` 38 | clone 메서드가 단순히 super.clone의 결과를 그대로 반환하면 Stack인스턴스의 size 필드는 올바른 값을 갖겠지만, elements필드는 원본 Stack 인스턴스와 똑같은 배열을 참조하게 되는 상황이 발생합니다.
39 | clone메서드는 사실상 생성자와 같은 효과를 냅니다. 즉 clone은 원본 객체에 아무런 변화가 없는 동시에 복제된 객체의 **불변식을** 보장해야 합니다. 40 | ``` java 41 | // 가변 상태를 참조하는 클래스용 clone 메서드 42 | @Override 43 | public PhoneNumber clone() { 44 | try{ 45 | Stack result = (Stack) super.clone(); 46 | result.elements = elements.clone(); 47 | return result; 48 | } catch (CloneNotSupportedException e) { 49 | throw new AssertionsError(); 50 | } 51 | } 52 | ``` 53 | 54 | 55 | -------------------------------------------------------------------------------- /chap03/Item14.md: -------------------------------------------------------------------------------- 1 | # [Item 14] Comparable를 구현할지 고려하라 2 | 자바에서는 `Comparable`과 `Comparator`이라는 정렬 인터페이스를 제공합니다. Comparable은 기본 정렬기준을 구현하는 데 사용하고, Comparator은 기본 정렬기준 외에 다른 기준으로 정렬하고자 할 때 사용합니다. 여기서는 Comparable의 하나 밖에 없는 `compareTo`메서드에 대해서 알아봅시다. 3 |
4 | Comparable을 구현했다는 것은 그 클래스의 인스턴스들에는 `natural order`가 있음을 의미합니다.
5 | 그래서 Comparable을 구현한 객체들의 배열은 다음과 같이 정렬할 수 있습니다. 6 | ``` java 7 | Arrays.sort(a); 8 | ``` 9 | 10 | ### **compareTo 메서드의 규약** 11 | 이 객체가 주어진 객체(매개변수로 받는)보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환합니다. 이 객체와 비교할 수 없는 타입이면 ClassCastException을 던집니다.
12 | 다음 설명에서는 sgn(표현식) 표기는 수학에서 말하는 부호 함수를 뜻하며, 표현식의 값이 음수, 0, 양수일 때 -1, 0, 1을 반환하도록 정의했습니다. 13 | - Comparable을 구현한 클래스는 모든 x, y에 대해 sgn(x.comparaTo(y)) == -sgn(y.comparaTo(x))여야 합니다.(따라서 x.compareTo(y))는 y.compareTo(x)가 예외를 던질 때에 한해 예외를 던져야 합니다. 14 | - Comparable을 구현한 클래스는 추이성을 보장해야 합니다. x.comparaTo(y) > 0 && y.comparaTo(z) > 0이면 x.compareTo(z) > 0입니다. 15 | - Comparable을 구현한 클래스는 모든 z에 대해 x.compareTo(y) == 0이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z))입니다. 16 | - 이번 권고가 필수는 아니지만 꼭 지키는 게 좋습니다. (x.compareTo(y) == 0) == (x.equals(y))여야 합니다. 17 | 18 | hashCode 규약을 지키지 못하면 해시를 사용하는 클래스에서 오작동이 날 수 있듯이 compareTo 규약을 지키지 못하면 TreeSet, TreeMap과 같이 비교를 활용하는 클래스에서 오작동이 날 수 있습니다. 19 |
20 | Compareble 인터페이스은 타입 인수를 받는 제네릭 인터페이스므로 compareTo 메서드의 인수 타입은 컴파일타임에 정해집니다. 즉 매개변수 타입이 잘못 됐다면 컴파일 자체가 되지 않으므로 형변환 할 필요가 없다는 뜻입니다.
21 | 객체 참조 필드를 비교하려면 compareTo 메서드를 재귀적으로 호출합니다. Compareble을 구현하지 않은 필드나 표준이 아닌 순서로 비교해야 한다면 `Comparator`를 사용하면 됩니다. 22 |

23 | 클래스의 핵심 필드가 여러개라면 가장 핵심적인 필드부터 비교하는 것을 추천드립니다. 비교 결과가 0이 아니라면 순서는 거기서 셜정되기 떄문입니다.
24 | **자바8에서 compareTo 메서드를 구현하기** 25 | ``` java 26 | List students = Arrays.asList( 27 | new Student(30, "kim"), 28 | new Student(50, "jake"), 29 | new Student(50, "foo") 30 | ); 31 | 32 | students.sort( 33 | Comparator.comparingInt(Student::getAge) 34 | .thenComparing(Student::getName) 35 | ); 36 | // 이뿐만 아니라 다양한 방식으로 정렬을 할 수 있습니다. 37 | ``` 38 | 이런식으로 간결하게 코드를 짤 수 있습니다. 대신 약간의 성능 저하가 있습니다. 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /chap04/Item15.md: -------------------------------------------------------------------------------- 1 | # [Iteam 15] 클래스와 멤버의 접근 권한을 최소화하라. 2 | 3 | 4 | 잘 설계된 컴포넌트와 그렇지 못한 컴포넌트의 가장 큰 차이는 클래스 내부 데이터와 내부 구현 정보를 외부 컴포넌트로부터 얼마나 잘 숨겼느냐입니다. 설계가 잘 된 컴포넌트는 모든 내부 구현을 숨겨, 구현과 API를 깔끔하게 **분리**합니다. 5 | 6 | ### **정보 은닉(캠슐화)의 장점** 7 | - 시스템 개발 속도를 높입니다. (여러 컴포넌트를 병렬로 개발할 수 있기 때문에) 8 | - 시스템 관리 비용을 낮춥니다. (각 컴포넌트를 더 빨리 파악하여 디버깅할 수 있고, 다른 컴포넌트로 교체하는 비용도 적기 때문에) 9 | - 성능 최적화에 도움을 줍니다. (다른 컴포넌트에 영향을 주지 않고 특정 컴포넌트만 최적화 할 수 있어서) 10 | - 재사용성을 높입니다. (독자적으로 동작하는 컴포넌트는 다른 환경에서도 쓰일 수 있기 때문에) 11 | 12 | 접근 제어자를 잘 활용하는 것이 정보 은닉의 핵심입니다.
13 | 가본 원칙은 모든 클래스와 멤버의 접근성을 가능한 좁혀야 합니다. 즉 애플리케이션이 제대로 동작하는 한 항상 가장 낮은 접근 수준을 부여해야 합니다.

14 | 톱레벨(가장 바깥) 클래스와 인터페이스에 부여할 수 있는 접근 제어자는 `public`과 `package-private`(default) 두 가지 입니다. 15 |
16 | public으로 선언하면 공개 API가 되므로 패키지 외부에서 쓸 일이 없다면 package-private로 선언하는 걸 권장합니다.

17 | 18 | 한 클래스에서만 사용하는 package-private 톱레벨 클래스나 인터페이스는 이를 사용하는 클래스 안에 private static으로 중첩시켜봅시다. 이렇게 하면 바깥 클레스 하나에서만 접근할 수 있습니다. 19 |

20 | 21 | **멤버**(필드, 메섣, 중첩 클래스, 중첩 인터페이스)에 사용할 수 있는 **접근제한자** 22 | 23 | - **private**: 멤버를 선언한 톱레벨 클래스에서만 접근 가능합니다. 24 | - **package-private**: 멤버가 소속된 패키지안에 모든 클래스에서 접근 가능합니다 25 | - **protected**: package-private + 하위 클래스 26 | - **public**: 모든 곳에서 접근할 수 있습니다. 27 | ### public 클래스의 인스턴스 필드는 가능한 public이 아니어야 합니다. 28 | 필드가 가변 객체를 참조하거나, final이 아닌 인스턴스필드를 public으로 선언하면 필드의 내용이 수정될 수 있으므로 불변을 보장할 수 없게 되므로 유의해야 합니다. 필드가 final이면서 불변 객체를 참조해도 문제는 있습니다. 내부 구현을 바꾸고 싶어도 public 필드를 없애는 방식으로는 리팩터링 할 수 없게 됩니다.
29 | 정적 필드 같은 경우 추상 개념을 완성하는데 꼭 필요한 구성요소로써의 상수라면 public static final필드로 공개해도 좋으나 반드시 `primitive type`이나 `불변 객체`를 참조해야 합니다. 가변 객체를 참조한다면 참조는 변경하지 못하나 객체 값은 수정될 수 있으므로 주의해야 합니다.
30 | 31 | 32 | > 정리
33 | > 프로그램의 접근 제어자는 가능한 **최소한**으로 합시다. 꼭 필요한 것만 public API를 설계하고 그 외에는 의도치 않게 API로 공개되는 일이 없도록 해야 합니다. public클래스는 public static final 필드 외에는 어떠한 public 메서드도 가져서는 안 됩니다. 또한 public static final 필드가 참조하는 객체가 불변인지 확인합시다. 34 | -------------------------------------------------------------------------------- /chap04/Item16.md: -------------------------------------------------------------------------------- 1 | # [Item 16] public 클래스에서는 public 필드가 아닌 접근 메서드를 사용하라. 2 | 3 | ``` java 4 | // 부적적한 코드 5 | public class Point { 6 | public int x; 7 | public int y; 8 | } 9 | 10 | ``` 11 | 위 코드는 객체지향의 특징 중 하나인 캡슐화를 살리지 못했습니다.
12 | 다음과 같이 추상화의 이점을 살려서 코드를 수정할 수 있습니다. 13 | ``` java 14 | public class Point { 15 | public int x; 16 | public int y; 17 | 18 | public Point(int x, int y) { 19 | this.x = x; 20 | this.y = y; 21 | } 22 | 23 | public int getX() { return x; } 24 | public int getY() { return y; } 25 | 26 | public void setX(int x) { this.x = x; } 27 | public void setY(int y) { this.y = y; } 28 | } 29 | 30 | ``` 31 | 외부에서 접근할 수 있는 클래스라면 접근자를 제공함으로써 클래스 내부 표현 방식을 변경할 수 있는 유연성을 얻을 수 있습니다.
32 | public 클래스의 불변이라면 직접 노출할 때의 단점이 줄어들지만 여전히 문제점은 있습니다. API를 변경하지 않고는 표현 방식을 바꿀 수 없고, 필드를 읽을 때 부수 작업을 수행할 수 없다는 단점이 있습니다. 하지만 불변식은 보장할 수 있습니다. 33 | > 정리
34 | > public 클래스의 가변 필드는 절대 노출해서는 안 됩니다. 불변 필드라면 위험은 덜 하지만 완전히 안심할 수는 없습니다. 하지만 package-private 클래스나 private 중첩 클래스에서는 필드를 노출하는 편이 나을 때도 있습니다. -------------------------------------------------------------------------------- /chap04/Item17.md: -------------------------------------------------------------------------------- 1 | # [Item 17] 변경 가능성을 최소화하라. 2 | 3 | **불변** 클래스(Immutable Class)란 말 그대로 객체가 생성된 후에 더이상 값을 변경할 수 없는 것을 의미합니다. 자바에서는 대표적으로 `String`, `Integer`, `Float`,`Long` 등이 있습니다.
4 | **불변 클래스의 장점** 5 | 6 | ### 클래스를 불변으로 만들기 위한 규칙 7 | - 객체의 상태를 변경하는 메서드를 제공하지 않습니다. 8 | - 클래스를 확장할 수 없도록 합니다. 9 | - 모든 필드를 private final으로 선언합니다. 10 | - 생성자 관리를 잘할 것 (밑에서 설명) 11 | - 자신 외에는 내부에 가변 컴포넌트에 접근할 수 없도록 합니다. 12 | 13 | ``` java 14 | public final class Calculator { 15 | private final int x; 16 | private final int y; 17 | 18 | public Calculator(int x, int y) { 19 | this.x = x; 20 | this.y = y; 21 | } 22 | 23 | public Calculator plus(Calculator c) { 24 | return new Calculator(x + c.x, y + c.y); 25 | } 26 | 27 | public Calculator minus(Calculator c) { 28 | return new Calculator(x - c.x, y - c.y); 29 | } 30 | 31 | ... 생략 32 | } 33 | ``` 34 | 여기서 주목할 점은 메서드들이 인스턴스 자신을 수정하지 않고 새로운 `Calculator` 인스턴스를 만들어 반화하는 것입니다. 35 | ### 불변 객체의 장점 36 | - 불변 객체는 Thread-Safe 하므로 멀티 쓰레드 환경에서 안전하게 사용할 수 있습니다. 37 | - 불변 객체는 하나의 상태만을 갖고 있으므로 데이터를 신뢰할 수 있습니다. 38 | 39 | 따라서 불변 클래스라면 한번 만든 인스턴스를 최대한 재활용 하길 추천합니다. 재활용 하기 가장 쉬운 방법은 자주 쓰이는 값들을 상수로 제공하는 것입니다. 40 | ``` java 41 | public static final Calculator ONE = new Calculator(1, 0); 42 | ... 43 | ``` 44 | 이 방식을 더 살펴보면 불변 클래스는 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해주는 정적 팩터리를 제공할 수 있습니다. 대표적으로 박싱된 기본 타입 클래스가 있습니다.
45 | 이런 정적 팩터리를 사용하면 여러 클라이언트가 인스턴스를 공유하여 메모리 사용량과 가비지 컬렉션 비용이 줄어듭니다. 하지만 붋변 객체에도 **단점**이 있습니다.**값이 다르면 반드시 독립된 객체로 만들어야** 합니다. (성능 이슈) 46 | ### 불변 클래스를 만드는 설계 방법 47 | 48 | 클래스가 불변임을 보장하려면 자신을 상속하지 못하게 해야하는데 가장 쉬운 방법은 final으로 선언해주는 것입니다. 하지만 이것보다 더 유연한 방법이 있습니다. 모든 생성자를 private 혹은 package-private으로 만들고 public 정적 펙터리를 제공하는 방법입니다. 49 | ``` java 50 | public final class Calculator { 51 | private final int x; 52 | private final int y; 53 | 54 | private Calculator(int x, int y) { 55 | this.x = x; 56 | this.y = y; 57 | } 58 | 59 | public static Calculator valueOf(int x, int y) { 60 | return new Calculator(x, y); 61 | } 62 | ... 63 | 64 | ``` 65 | 정적 팩토리 방식으로 다수의 구현 클래스를 활용해 유연성을 제공하고, 객체 캐싱 기능을 추가해 성능을 끌어올리 수 있습니다.

66 | ### `BigInteger`와 `BigDecimal`의 주의점
67 | 두 클래스를 설계할 당시 불변 객체가 final이어야 한다는 인식이 없었습니다. 그래서 두 클래스의 메서드들은 모두 재정의할 수 있게 설계 되었고 하위 호환성이 발목을 잡아 지금까지도 이 문제를 고치지 못했습니다. 그러므로 신뢰할 수 없는 클라이언트로부터 `BigInteger`와 `BigDecimal`의 인스턴스를 인수로 받는다면 주의해야 합니다. 68 | ``` java 69 | public static BigInteger safeInstance(BingInteger val) { 70 | return val.getClass() == BigInteger.class ? 71 | val : new BigInteger(val.toByteArray()); 72 | } 73 | ``` 74 |
75 | 정리 76 | - **클래스는 꼭 필요한 경우가 아니면 불변이어야 합니다**. 77 | - 불변 클래스의 장점들도 있지만 성능 문제도 함께 고려해야합니다 78 | - 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분은 최소한으로 줄입시다. 79 | - 다른 합당한 이유가 없다면 모두 private final이어야 합니다. 80 | - 생성자는 불변식 설정 초기화가 완벽히 끝난 상태의 객체를 생성해야 합니다. -------------------------------------------------------------------------------- /chap04/Item18.md: -------------------------------------------------------------------------------- 1 | # [Item 18] 상속보다는 컴포지션을 사용하라. 2 | 우선 이번 아이템에서 다루는 상속은 클래스가 다른 클래스를 확장하는 것을 말합니다.
3 | 상속 같은 경우 상위 클래스가 구현 방식에 따라 하위 클래스 동작에 영향을 미칠 수 있습니다. 4 |
5 | **예제를 위한 코드** 6 | ``` java 7 | public class CustomHashSet extends HashSet { 8 | private int addCount = 0; 9 | 10 | public CustomHashSet() { } 11 | 12 | @Override 13 | public boolean add(E e) { 14 | addCount++; 15 | System.out.println("hello"); 16 | return super.add(e); 17 | } 18 | 19 | @Override 20 | public boolean addAll(Collection c) { 21 | addCount += c.size(); 22 | return super.addAll(c); 23 | } 24 | 25 | public int getAddCount() { 26 | return addCount; 27 | } 28 | 29 | } 30 | ``` 31 | ```java 32 | CustomHashSet set = new CustomHashSet<>(); 33 | set.addAll(Arrays.asList("가","나","다")); 34 | System.out.println(set.getAddCount()); 35 | } 36 | ``` 37 | 원소를 3개 삽입했지만 addCount출력하면 결과값은 `6`이 나옵니다. 38 | 39 | 디버깅을 해본 결과 상위 클래스에서 addll메서드에서 add를 호출하는데 여기서 add는 오버라이딩 된 add메서드를 결과적으로 3번 더 호출하게 됐습니다.
40 | 추가로 다음 릴리즈에서 상위 클래스에 새로운 메서드를 추가했는데, 하필 하위 클래스에 추가한 메서드와 시그니처가 같고 반환 타입이 다를 경우 컴파일조차 되지 않습니다.
41 | 이러한 문제점들은 컴포지션을 통해서 쉽게 피해갈 수 있습니다. 컴포지션은 기존 클래스를 확장하는 대신 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조해서 이용할 수 있습니다. 42 |
43 | 44 | 45 | 상속은 클래스 B가 클래스 A와 **is-a** 관계일 때만 클래스 A를 상속해야 합니다. 46 | 클래스 B가 A를 상속하기 전에 B가 A인가? 자문해보고 "그렇다"라는 확신이 들지 않으면 **컴포지션(has-a)** 을 이용하는 게 좋습니다. 47 |
48 | 49 | 컴포지션을 써야 할 상황에서 상속을 사용하는 건 내부 구현을 불필요하게 노출하는 꼴입니다. 그 결과 API가 내부 구현에 묶이고 그 클래스의 성능도 영원히 제한됩니다. 더 심각한 문제는 클라이언트가 노출된 내부에 **직접 접근**할 수 있다는 점입니다. 50 |
51 | 컴포지션 대신 상속을 사용하기로 결정하면 마지막으로 자문해야 될 질문이 있습니다. "확장하려는 클래스의 API에 아무런 결함이 없는가" 결함이 있다면, 이 결함이 여러분 클래스의 API까지 전파돼도 괜찮은가?. 상속은 상위 클래스의 API를 그 결함까지도 그대로 승계합니다. 52 |
53 | ### **정리** 54 | - 상속은 강력하지만 캡슐화를 해친다는 단점이 있습니다. 55 | - 상속은 상위 클래스와 하위클래스가 순수한 is-a 관계일 때만 사용해야합니다. 56 | - 상속의 단점을 피라려면 컴포지션을 활용합시다. -------------------------------------------------------------------------------- /chap04/Item19.md: -------------------------------------------------------------------------------- 1 | # [Item 19] 상속을 고려해 설계하고 문서화하라. 그렇지 않았다면 상속을 금지하라. 2 | 3 | 여기서 말하는 **외부**란 프로그래머의 통제권 밖에 있어서 언제 어떤식으로 변경될지 모른다는 뜻입니다. 4 | ### **상속을 고려한 문서화** 5 | 1. 상속용 클래스는 재정의할 수 있는 메서드들은 내부적으로 어떻게 이용하는지 문서로 남겨야 합니다. 6 | 2. 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 `protected`메서드 형태로 공개하는 것도 고려해보는 것도 좋습니다. (한편으로 너무 적게 노출해서 상속으로 얻는 이점을 없애지 않도록 주의해야 합니다.) 7 | 3. 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 **검증**해야 합니다. 8 | 9 | ### 상속을 허용하는 클래스가 지켜야 할 제약 10 | 11 | **상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 됩니다**. 상위 클래스의생성자가 하위 클래스의 생성자보다 먼저 실행되므로 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출됩니다. 12 |
13 | 상속을 금지하는 방법에는 두 가지 방법이 있습니다. 첫 번째는 클래스를 final으로 선언하는 것과 두번째는 모든 생성자를 private나 package-private로 선언하고 public 정적 팩터리를 만들어주는 방법입니다. 정적 팩터리 방법은 다양한 하위 클래스를 만들어 쓸 수 있는 유연성을 주며 이와 관련해서는 아이템 17에서 다뤘습니다. 14 |
15 | ### 정리 16 | - 상속을 하려면 문서화를 해야 하며, 문서화 한 것은 반드시 지켜야 합니다. 17 | - 클래스를 확장해야 할 명확한 이유가 없으면 아이템 17에서 다뤘던 방식으로 상속을 금지합시다. 18 | 19 | -------------------------------------------------------------------------------- /chap04/Item20.md: -------------------------------------------------------------------------------- 1 | # [Item 20] 추상 클래스보다는 인터페이스를 우선하라. 2 | 3 | 자바가 제공하는 다중 구현 메커니즘은 인터페이스와 추상 클래스가 있습니다. 자바8 부터는 인터페이스에서 **defualt moethod**를 제공할 수 있게 되어서 두 메커니즘 모두 인스턴스 메서드를 구현 형태로 제공할 수 있습니다.
4 | 추상 클래스와 인터페이스의 큰 차이점은 추상 클래스의 정의한 타입을 구현 클래스는 반드시 서브클래스가 된다는 점입니다. 자바에서는 단일 상속만 지원하기 때문에 이런 제약은 새로운 타입을 정의하는데 커다란 제약이 됩니다.

5 | 반면 인터페이스의 준수 사항을 잘 지키고 모든 메서드를 구현한 클래스는 어느 계층에 있든 인터페이스를 구현할 수 있습니다. 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해 넣을 수 있습니다. 그저 인터페이스가 요구하는 메서드를 (아직 없다면) 추가하고, 클래스 선언에 `implements` 구문을 추가하면 됩니다.

6 | 반면 일반적으로 기존 클래스에 새로운 추상 클래스를 끼워넣는 일은 매우 간단합니다. 만약 두 클래스로 하나의 추상 클래스를 상속하길 원한다면, 그 추상 7 | 클래스는 계층구조상 두 클래스의 공통 조상이어야합니다. 8 | 이러한 방식은 클래스 계층구조에 커다란 혼란을 야기할 수 있습니다. 적절하지 않은 상황에서도 강제로 새로 추가된 추상 클래스의 모든 자손이 이를 상속하게 되는 것이다.
9 | 인테페이스는 **믹스인(mixin)** 정의에 안성맞춤입니다. 믹스인이란 클래스가 구현할 수 있는 타입으로, 믹스인을 구현한 클래스에 원래의 '주된 타입'외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 줍니다. 예컨대 `Comparable`은 자신을 구현한 클래스의 인스턴스들끼리는 순서를 정할 수 있다고 선언하는 믹스인 인터페이스입니다. 이처럼 대상 타입의 주된 기능에 선택적 기능을 '혼합(mixed in)'한다고 해서 믹스인이라고 불립니다.
10 | 반면 추상 클래스로는 믹스인을 정의할 수 없습니다. 기존 클래스에 덧씌울 수 없기 때문입니다. 클래스는 두 부모를 섬길 수 없고, 클래스 계층구조에는 믹스인을 삽입하기엔 합리적인 위치가 없기 때문입니다. 11 | 12 | ### **인터페이스는 계층구조가 없는 타입 프레임워크 만들 수 있습니다.** 13 | ``` java 14 | public interface Singer { 15 | AudioClip sing(Song s); 16 | } 17 | public interface Songwriter { 18 | Song compose(int chartPosition); 19 | } 20 | ``` 21 | 현실에는 계층을 엄격히 구분하기 어려운 개념이 있습니다. 위 코드처럼 타입을 인터페이스로 정의하면 가수 클래스가 `Singer`와 `SongWriter` **모두**를 구현해도 전혀 문제가 되지 않습니다. 22 | ``` java 23 | public interface SingerSongWriter extends Singer, Songwriter { 24 | AudioClip strum(); 25 | } 26 | ``` 27 | 이런식으로 Singer와 Songwriter모두를 확장하고 새로운 메서드까지 추가한 제3의 인터페이스도 정의할 수 있습니다. 같은 구조를 클래스로 만드려면 속성이 n개라면 지원해야 할 조합의 수는 2^n개가 됩니다 이러한 현상을 **조합 폭발**(combinatorial explosion)이라고 부릅니다. 28 |

29 | 인터페이스의 메서드 중 구현 방법이 명백한 것이 있다면, 그 구현을 디폴트 메서드로 제공하는 것도 방법입니다. 하지만 디폴트 메서드에도 제약이 있습니다. 많은 인터페이스가 `equals`와 `hashCode`같은 Object의 메서드를 정의하고 있지만, 이들은 디폴트 메서드로는 제공해서는 안 됩니다. 또하 인터페이스는 인스턴스 필드를 가질 수 없고 public이 아닌 정적멤버도 가질 수 없습니다. 마지막으로 여러분들이 만들지 않은 인터페이스에는 디폴트 메서드를 추가할 수 없습니다.
30 | > 정리 31 | > - 일반적으로 다중 구현용 타입으로는 인터페이스가 적합합니다. 32 | > - 복잡한 인터페이스라면 구현하는 수고를 덜어주는 골격 구현을 함께 제공하는 방법도 고려하는걸 추천합니다. -------------------------------------------------------------------------------- /chap04/Item21.md: -------------------------------------------------------------------------------- 1 | # [Item 21] 인터페이스는 구현하는 쪽을 생각해 설계하라. 2 | 3 | 자바 8 이전에는 기존 구현체를 깨뜨리지 않고 인터페이스에 새로운 메서드를 추가할 방법이 없었습니다. 자바8 부터는 디폴트 메서드를 제공해서 이러한 문제점들을 해결해줬지만 위험이 완전히 사라진 것은 아닙니다. 4 |
5 | 6 | 자바 8이전까지는 인터페이스에 새로운 메소드가 추가될리 없다는 암묵적인 가정으로 작성되었습니다. 즉 디폴트 메서드는 구현한 클래스에 동의 없이 무작정 삽입되었습니다. 자바 8에서는 핵심 컬렉션 인터페이스들에 다수의 디폴트 메서드가 추가되었습니다.주로 **람다**를 활용하기 위해서입니다. 자바라이브러리의 디폴트 메서드는 코드 품질이 높고 범용적이라 대부분 잘 작동하지만, **생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어려운 법입니다.** 7 | 8 | ``` java 9 | default boolean removeIf(Predicate filter) { 10 | Objects.requireNonNull(filter); 11 | boolean removed = false; 12 | final Iterator each = iterator(); 13 | while (each.hasNext()) { 14 | if (filter.test(each.next())) { 15 | each.remove(); 16 | removed = true; 17 | } 18 | } 19 | return removed; 20 | } 21 | ``` 22 | 자바 8 `Collection` 인터페이스에 추가된 `removeIf` 메서드입니다. 이코드보다 더 범용적으로 구현하기도 어렵겠지만, 그렇다고 해서 현조하는 모든 Collection 구현체와 잘 어우러지는 것은 아닙니다.
23 | 대표적인 예가 org.apache.commons.collections4.collection.SynchronizedCollection입니다. 이 클래스는 java.util.Collections.synchronizedCollction 정적 팩터리 메서드가 반환하는 클래스와 비슷합니다. 아파치 버전은 클라이언트가 제공한 객체로 락을 거는 능력을 추가로 제공합니다. 즉, 모든 메서드에 주어진 락 객체로 동기화한 후 내부 컬렉션 객체에 기능을 위임하는 래퍼 클래스입니다.
24 | 아파치의 SynchronizedCollection는 removieIf 메서드를 재정의하고 있지 않습니다. 이 첵이 쓰여진 시점에는 removeIf를 재정의하고있지 않습니다. 이 클래스는 removeIf를 재정의 하고있지 않습니다. 그래서 이 클래스를 자바 8과 함께 사용하면 모든 메서드 호출을 알아서 동기화해주지 못합니다.
25 | 자바 플랫폼 라이브러리에서도 이런 문제를 예방하기 위해 조치를 취했습니다. 예를 들어 구현한 인터페이스의 디폴트 메서드를 재정의하고, 다른 메서드에서는 디폴트 메서드를 호출하기 전에 필요한 작업을 수행하도록 했습니다. 26 |
27 | 핵심은 인터페이스를 설계할 때는 세심한 주의를 기울여야 합니다. 디폴트 메서드로 기존 인터페이스에 새로운 메서드를 추가하면 커다란 위험도 딸려옵니다. 새로운 인터페이스라면 릴리즈 전에 반드시 테스트를 거쳐야 합니다. -------------------------------------------------------------------------------- /chap04/Item22.md: -------------------------------------------------------------------------------- 1 | # [Item 22] 인터페이스는 타입을 정의하는 용도로만 사용하라. 2 | 3 | 인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 합니다. 그러므로 인터페이스를 구현한 클래스는 클라이언트에게 자신의 인스턴스로 무엇을 할 수 있는지 말해주는 것입니다. 인터페이스를 다른 용도로 사용하는 것은 부적절합니다. 4 |
5 | 이 지침에 맞지 않는 예로 소위 상수 인터페이스라는 것이 있습니다. 상수 인터페이스는 메서드가 없이, 상수를 뜻하는 static final 필드로만 가득 찬 인터페이스를 뜻합니다.
6 | 7 | ``` java 8 | public interface PhysicalConstants { 9 | static final double AVOGADROS_NUMBER = 6.022_140_857e34; 10 | 11 | static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23; 12 | 13 | static final double ELECTRON_MASS = 9.109_383_56e-31; 14 | } 15 | ``` 16 | 17 | 상수 인터페이스 안티패턴은 인터페이스를 잘못 사용한 예입니다. 클래스 내부에서 사용하는 상수는 외부 인터페이스가 아니라 내부 구현에 해당합니다. 따라서 상수 인터페이스를 구현하는 것은 이 내부 구현을 클래스의 API로 노출하는 행위입니다. 클래스가 어떤 상수 인터페이스를 사용하든 사용자에게는 아무런 의미가 없습니다. 오히려 혼란을 줄 수가 있으며, 더 심각하게는 클라이언트 코드가 내부 구현에 해당하는 이 상수들에게 종속되게 합니다. 18 |
19 | 그래서 다음 릴리즈에서 이 상수들을 더는 쓰지 않게 되더라도 바이너리 호환성을 위해 여전히 상수 인터페이스를 구현하고 있어야 합니다. final이 아닌클래스가 상수 인터페이스를 구현한다면 모든 하위 클래스의 네임스페이스가 그 인터페이스가 정의한 상수들로 오염되어 버립니다. 20 |
21 | java.io.ObjectStreamConstants 등, 자바 플랫폼 라이브러리에도 상수 인터페이스가 몇 개 있으나, 인터페이스를 잘못 활용한 예이니 따라 해서는 안 됩니다. 상수를 공개할 목적이라면 더 합당한 방법들이 있습니다. 22 |
23 | 특정 클래스나 인터페이스와 강하게 연관된 상수라면 그 클래스나 인터페이스 자체에 추가해야합니다. 대표적으로 `Integer`와 `Double`에 선언된 `MIN_VALUE`와 `MAX_VALUE` 상수가 이런 예입니다. 열거 타입으로 나타내기 적합한 상수라면 열거 타입으로 만들면 되고 그것도 아니라면 인스턴스화 할 수 없는 유틸클래스에 담아 공개하면 됩니다. 24 | 25 | ``` java 26 | // 유틸리티 클래스 27 | 28 | public class PhysicalConstants { 29 | private PhysicalConstants() 30 | 31 | static final double AVOGADROS_NUMBER = 6.022_140_857e34; 32 | 33 | ... 생략 34 | } 35 | ``` 36 | 37 | ### 정리 38 | - 인터페이스는 타입을 정의하는 용도로만 사용해야 합니다, 실수 공개용 수단으로 사용하지 맙시다. -------------------------------------------------------------------------------- /chap04/Item23.md: -------------------------------------------------------------------------------- 1 | # [Item 23] 태그 달린 클래스보다는 클래스 계층구조를 활용하자. 2 | 3 | 때때로 두 가지 이상의 의미를 표현하고 인스턴스의 특징을 알려주는 태그 필드로 나타내는 클래스를 본 적이 있을겁니다. 4 | 5 | ### 안 좋은 예시 6 | ``` java 7 | public class Figure { 8 | 9 | enum Shape { RECTANGLE, CIRCLE }; 10 | 11 | // 태그 필드 - 현재 모양을 나타냅니다. 12 | final Shape shape; 13 | 14 | // 모양이 사각형(RECTANGLE)일 때만 쓰입니다. 15 | double length; 16 | double width; 17 | 18 | // 다음 필드 모양이 원(CIRCLE)일 때만 쓰입니다. 19 | double radius; 20 | 21 | // 원 생성 22 | Figure(double radius) { 23 | shape = Shape.CIRCLE; 24 | this.radius = radius; 25 | } 26 | 27 | Figure(double length, double width) { 28 | shape = Shape.RECTANGLE; 29 | this.length = length; 30 | this.width = width; 31 | } 32 | 33 | double area() { 34 | switch (shape) { 35 | case RECTANGLE: 36 | return length * width; 37 | case CIRCLE: 38 | return Math.PI * (radius * radius); 39 | default: 40 | throw new AssertionError(); 41 | } 42 | } 43 | } 44 | ``` 45 | 46 | 태그 달린 클래스에는 단점이 많습니다. 열거 타입 선언, 태그 필드, switch문 등 잡다한 코드가 너무 많습니다. 여러 구현이 한 클래스에 있어서 구현도 나쁩니다. 인스턴스가 다른 특징에 속하는 관련없는 필드로 인해 메모리도 많이 사용하게 됩니다. 또 다른 의미를 추가하려면 switch문을 찾아 새 의미를 처리하는 코드를 추가해야하는 데 하나라도 빠뜨리면 문제가 될 수도 있습니다. 마지막으로, 인스턴스의 타입만으로는 현재 나타내는 의미를 알 길이 없습니다. 한 마디로 **태그 달린 클래스는 장황하고, 오류를 내기 쉽고, 비효율적입니다.** 47 |
48 | 클래스 계층구조를 활용하는 **서브타이핑**을 이용해서 타입 하나로 다양한 의미의 객체를 표현할 수 있습니다. 태그 달린 클래스는 클래스 계층구조를 어설프게 흉내낸 아류일뿐입니다. 49 |
50 | ### 태그달린 클래스를 클래스 계층 구조로 바꾸는 방법 51 | 1. 계층구조의 루트가 될 추상 클래스를 정의하고, 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언합니다. 52 | 2. 태그 값에 상관없이 동작이 일정한 메서드들을 루트 클래승 일반 메서드로 추가합니다. 53 | 3. 모든 하위 클래스에서 공통으로 사용하는 데이터 필드들도 전부 루트 클래스로 올립니다. 54 | 4. 루트 클래스를 확장한 구체 클래스를 의미별로 하나씩 정의합니다. 55 | 56 | Figure 클래스에서는 태그 값과 상관없는 메서드가 하나도 없고, 모든 하위 클래스에서 사용하는 공통 데이터 필드가 없습니다. 그 결과 루트 클래스에는 추상메서드 area하나만 남게 됩니다.
57 | 다음은 Figure 클래스를 계층구조 방식으로 구현한 코드입니다. 58 | ``` java 59 | abstract class Figure { 60 | abstract double area; 61 | } 62 | 63 | public class Circle extends Figure{ 64 | final double radius; 65 | 66 | public Circle(double radius) { 67 | this.radius = radius; 68 | } 69 | 70 | @Override 71 | double area() { 72 | return Math.PI * (radius * radius); 73 | } 74 | 75 | } 76 | 77 | // Rentangle 클래스 위와 같은 방식으로 구현하면 됩니다. 78 | ``` 79 | 태그 달린 클래스에 비교하면 쓸데 없는 코드들은 모두 제거했고, 각 의미를 독립된 클래스에 담아 관련 없던 데이터 필드를 모두 제거했습니다. 살아 남은필드들은 모두 final이며 각 클래스의 생성자가 모든 필드를 남김없이 초기화하고 추상 메서드를 구현했는지 컴파일러가 확인해줍니다. 80 | ### 정리 81 | - 태그 달린 클래스를 써야 하는 상황은 거의 없습니다. 82 | - 새로운 클래스를 작성할 때 태그 필드가 등자했다면 계층 구조롤 대체하는 방법을 고려해보자. 83 | - 기존 클래스가 필드를 사용하고 있다면 걔층구조로 리팩터링 하는 걸 고려민해보자. -------------------------------------------------------------------------------- /chap04/Item24.md: -------------------------------------------------------------------------------- 1 | # [Item 24] 멤버 클래스는 되도록 static으로 만들어라. 2 | 3 | 중첩 클래스(nested class)는 자신을 감싼 바깥 클래스에서만 쓰어야 하며 그 이외 쓰임새가 있다면 톱레벨 클래스로 만들어야 합니다. 4 |
5 | 중첩 클래스는 다음과 같이 네 종류로 나눌 수 있습니다. 6 | - 정적 멤버 클래스 7 | - (비정적)멤버 클래스 8 | - 익명 클래스 9 | - 지역 클래스 10 | 11 | 이 중 정적 멤버 클래스를 제외한 나머지는 내부 클래스(inner class)에 해당합니다. 12 | ### 정적 멤버 클래스 13 | 정적 멤버 클래스는 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할 수 있다는 점만 제외하고는 일반 클래스외 똑같습니다. 정적 멤버 클래스는 흔히 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스로 쓰입니다. 14 | ### 비정적 멤버 클래스 15 | 비정적 멤버 클래스의 인스턴스 바깥 클래스의 인스턴스와 암묵적으로 연결됩니다. 그래서 비정적 멤버 클래스의 인스턴스는 메서드에서 정규화된 this를 사용해 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있습니다.
16 | 여기서 말하는 정규화된 this란 **클랴스명.this** 형태로 바깥 클래스의 이름을 명시하는 용법을 말합니다. 따라서 바깥 인스턴스와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들어야 합니다. 비정적 멤버 클래스는 바깥 인스턴스 없이 생성할 수 없기 때문입니다. 17 |
18 | 바깥 클래스의 인스턴스 메서드에 비정적 멤버 클래스의 생성자를 호출할 때 자동으로 만들어지는 게 보통이지만, 드물게 직접 바깥 인스턴스의 클래스.new MemberClass()를 호출해 수동으로 만들기도 하지만 비정적 멤버 클래스의 인스턴스 안에 만들어져 메모리 공간을 차지하며, 생성 시간도 더 걸립니다. 19 |
20 | 비정적 멤버 클래스는 **어댑터**를 정의할 때 자주 사용됩니다. 즉 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 사용됩니다. 예컨대 `Map` 인터페이스의 구현체들은 보통 (keySet, entrySet 메서드가 반환하는) 자신의 컬렉션 뷰를 구현할 때 비정적 멤버 클래스를 사용합니다. 21 |
22 | **멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만듭시다**. static을 생략하면 바깥 인스턴스로의 숨은 외부 참조를 갖게 됩니다. 이렇게 되면 시간과 공긴이 소비되고 더 심각한 문제는 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수가 생길 수 있습니다. 23 | 24 | ### 익명 클래스 25 | 익명 클래스는 바깥 클래스의 멤버가 아닙니다. 멤법와 달리, 쓰이는 시점에 선언과 동시에 인스턴스로 만들어집니다. 그리고 오직 비정적인 문맥에서 사용될 때만 바깥 클래스의 인스턴스를 참조할 수 있으며 instance of검사나 클래스의 이름이 필요한 작업은 수행할 수 없습니다.
26 | 자바가 람다를 지원하기 전에는 즉석에서 작은 함수 객체나 처리 객체(process object)를 만드는 데 만드는 데 익명 클래스를 주로 사용했으며 또 정적 팩터리 메서드를 구현할 때입니다. 27 | ### 지역 클래스 28 | 지역 클래스는 네 가지 중첩 클래스 중 가장 드물게 사용됩니다. 지역 클래스는 지역 변수를 선언할 수 있는 곳이면 실직적으로 어디서든 선언할 수 있고, 유효범위도 지역변수와 같습니다. 29 | 30 | ### 정리 31 | - 중첩 클래스에는 네 가지가 있으며, 각각의 쓰임이 다릅니다. 32 | - 메서드 밖에서도 사용해야 하거나 메서드 안에 정의하기엔 너무 길다면 멤버 클래스로 만듭니다. 33 | - 멤버 클래스의 인스턴스 각각이 바깥 인스턴스를 참조한다면 비정적으로. 그렇지 않으면 정적으로 만듭시다. 34 | - 중첩 클래스가 한 메서드 안에서만 사용되며 그 인스턴스를 생선하는 지점이 단 한 곳이고 해당 타입으로 쓰기에 적합한 클래스나 인터페이스가 있다면 익명 클래스로 만들고, 그렇지 않으면 지역 클래스로 만듭시다. 35 | -------------------------------------------------------------------------------- /chap04/Item25.md: -------------------------------------------------------------------------------- 1 | # [Item 25] 톱레벨 클래스는 한 파일에만 담으라 2 | ``` java 3 | public class Main { 4 | public static void main(String[] args) { 5 | System.out.println(Utensil.NAME + Dessert.NAME); 6 | } 7 | } 8 | ``` 9 | 위 코드의 소스 파일은 Main 클래스 하나를 담고 있고, Main 클래스는 다른 톱레벨 클래스 2개(Utensil과 Dessert)를 참조합니다. 10 |
11 | `Utensil`와 `Dessert` 클래스가 **Utensil.java**라는 한 파일에 정의되어 있다고 가정해봅시다. 12 | ``` java 13 | class Utensil { 14 | static final String NAME = "pan"; 15 | } 16 | class Dessert { 17 | static final String NAME = "cake"; 18 | } 19 | ``` 20 | Main을 실행하면 pancake가 출력됩니다. 21 |
22 | 이제 두 클래스를 담은 **Dessert.java**라는 파일을 만들었다고 가정해봅시다. 23 | ``` java 24 | class Utensil { 25 | static final String NAME = "pot"; 26 | } 27 | class Dessert { 28 | static final String NAME = "pie"; 29 | } 30 | ``` 31 | 컴파일러는 가장 먼저 Main.java를 컴파일하고, 그 안에서 (Dessert 참조보다 먼저 나오는) Utensil 참조를 만나면 Utensil.java 파일을 살펴 Utensil과 Dessert를 모두 찾아낼 것입니다. 그런 다음 컴파일러가 두 번째 명령줄 인수로 넘어온 Dessert.java를 처리하려 할 때 같은 클래스의 정의가 이미 있음을 알게 됩니다. 32 |
33 | 한편 javac Main.java나 javac Main.java Utensil.java 명령으로 컴파일하면 Dessert.java 파일을 작성하기 전처럼 pancake를 출력합니다. 그러나 javac Dessert.java Main.java 명령으로 컴파일하면 potpie를 출력합니다. 이처럼 컴파일러에 어느 소스 파일을 먼저 건네느냐에 따라 동작이 달라지므로 반로 잡아야할 문제입니다. 다행히 해결책은 간단합니다. 단순힌 톱레벨 클래스들을 서로 다른 소스 파일로 분리하면 그만입니다. 34 |
35 | 굳이 여러 톱레벨 클래스를 한 파일에 담고 싶다면 정적 멤버 클래스를 사용하는 방법을 고민해볼 수 있습니다. 36 | ``` java 37 | public class Test { 38 | public static void main(String[] args) { 39 | System.out.println(Utensil.NAME + Dessert.NAME); 40 | } 41 | private static class Utensil { 42 | static final String NAME = "pan"; 43 | } 44 | private static class Dessert { 45 | static final String NAME = "cake"; 46 | } 47 | } 48 | 49 | ``` 50 | -------------------------------------------------------------------------------- /chap05/Item26.md: -------------------------------------------------------------------------------- 1 | # [Item 26] Raw 타입은 사용하지 마라. 2 | 3 | `raw type`이란 제네릭 타입에서에서 타입 파라미터를 전혀 사용하지 않았을 때를 말합니다. 4 | ``` java 5 | // raw type 6 | List list = new ArrayList(); 7 | ``` 8 | 로 타입은 타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작하는데, 제네릭이 도래하기 전 코드와 호환성을 위해서 주로 존재합니다.
9 | ``` java 10 | // 문자열을 저장하는 컬렉션 11 | private final List names = new ArrayList(); 12 | ``` 13 | 위 코드를 사용하면 String대신 다른 타입을 넣어도 오류없이 실행됩니다. (경고 메세지를 보여주긴 합니다.) 14 | ``` java 15 | naems.add(1); // "Unchecked call" 경고를 보여줍니다. 16 | ``` 17 | ``` java 18 | String str = (String) names.get(0); // ClassCastException 19 | ``` 20 | 컴파일 오류를 체크하지 못하고 런타임할 때 예외가 발생하게 됩니다. 가장 이상적인 오류는 컴파일할 때 발견하는 것이 좋습니다. 이렇게 되면 런타임에 문제를 겪는 코드와 원인을 제공한 코드가 물리적으로 상당히 떨어져 있을 가능성이 커집니다. 21 |
22 | 제네릭을 활용하면 이 정보가 주석이 아닌 타입 선언 자체에 녹아듭니다. 23 | ``` java 24 | // 매개변수화된 컬렉션 타입 - 타입 안전성 확보 25 | private final List names = new ArrayList<>(); 26 | ``` 27 | 28 | 이렇게 선언하면 컴파일러는 names에는 String만 넣어야 함을 컴파일러가 인지하게 됩니다. 이제 names에 엉뚱한 타입의 인스턴스를 넣으려고하면 컴파일 오류가 발생합니다. 29 | ``` java 30 | names.add(1); // 컴파일 오류 31 | ``` 32 | 앞에서도 얘기했듯, 로 타입(타입 매개변수가 없는 제네릭 타입)은 절대로 사용해서는 안 됩니다. **로 타입을 쓰면 제네릭이 안겨주는 안전성과 표현력을 모두 잃게 됩니다.** 로 타입을 만들어놓은 이유는 호환성 때문입니다. 자바가 제네릭을 받아들이기까지 거의 10년이 걸린 탓에 제네릭 없이 짠 코드를 모두 수용하면서 제네릭을 사용하는 새로운 코드와도 맞물려 돌아가게 해야만 했습니다.
33 | 34 | ### List vs List\ 35 | 로 타입 List는 제네릭 타입에서 완전히 발을 뺀 것이고, List\는 모든 타입을 허용한다는 의사를 컴파일러에게 전달한 것입니다. 매개변수로 List를 받는 메서드에 List\를 넘길 수 있지만, List\를 받는 메서드에는 List\를 넘길 수 없습니다. 이는 제네릭의 하위 타입 규칙 때문입니다.
36 | 즉, List\는 로 타입인 List의 하위타입 이지만, List\의 하위타입은 아닙니다. 그 결과 List\같은 매개변수화 타입을 사용할 때와 달리 List같은 로 타입을 사용하면 타입 안전성을 잃게 됩니다. 37 |
38 | 39 | 예제를 위한 코드 40 | 41 | ``` java 42 | // Interger는 Number의 서브 타입입니다. 43 | List intList = new ArrayList<>(); 44 | List numberList = intList; // 컴파일 에러 45 | ``` 46 | 47 | ``` java 48 | List intList = new ArrayList<>(); 49 | List rawType = intList; // 오류가 발생하지 않지만, 안전하지 않습니다. 50 | ``` 51 | 이쯤 되면 원소의 타입을 몰라도 되는 로 타입을 쓰고 싶어질 수 있습니다. 그럴 땐 비한정적 와일드 타입(unbounded wildcard types)를 사용하는 대안이 있습니다. 이는 타입 파리머티에 `?`을 작성하면 됩니다. 52 | 53 | ``` java 54 | // unbounded wildcard type - 타입 세이프하고 유연합니다, 55 | static int numElementsInCommon(Set s1, Set s2) { ... } 56 | ``` 57 | 58 | 그렇다면 비한정적 와일드카드 타입인 Set\와 로타입인 Set의 차이는 무엇일까? 물음펴가 무언가 멋진 일은 해주는 것일까?
59 | 와일드 카드는 type safe 하고 로 타입은 그렇지 않다는 점입니다. 로 타입 원소에는 아무 원소나 넣을 수 있으니 타입 붋변식을 훼손하기 쉽습니다.
60 | 반면 Collection\에는 (null 이외는) 어떤 원소도 넣을 수 없습니다. 다른 원소를 넣으려 하면 컴파일 할 때 오류 메세지를 보여줍니다. 이러한 제약을 받아들일 수 없다면 제네릭 메서드나 **bounded wildcard type**을 사용하면 됩니다.
61 | 로 타입을 쓰지 말라는 규칙에도 소소한 예외가 몇 개 있습니다. **class 리터럴에는 로 타입을 써야 합니다. 자바 명세는 class 리터럴에 매개변수화 타입을 사용하지 못하게 했습니다.**(배열과 기보 타입은 허용합니다)
62 | 예를 들어 List.class.String[].class, int.class는 허용하고 List\.class와 List\.class는 허용하지 않습니다. 63 |
64 | 두 번째 예외는 instanceof 연산자와 관련이 있습니다. 런타임에는 제네릭 타입 정보가 지워지므로 instanceof 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없습니다. 65 |
66 | ### 정리 67 | - 로 타입을 사용하면 런타임 예외가 일어날 수 있으니 사용하면 안 됩니다. 68 | - 로 타입은 제네릭이 도입되기 이전 코드와의 호환성을 위해 제공될 뿐입니다. 69 | - Set\는 어떤 타입의 객체도 저장할 수 있는 매개변수화 타입입니다. 70 | 71 | -------------------------------------------------------------------------------- /chap05/Item27.md: -------------------------------------------------------------------------------- 1 | # [Item 27] 비검사 경고를 제거하라. 2 | 3 | 제네릭에 관련된 수 많은 컴파일 경고들이 있습니다. 비검사 형변환 경고, 비검사 메서드 호출 경고, 비검사 매개변수화 가변인수 타입 경고, 비검사 변환 경고 등이 있습니다. 제네릭에 익숙해질 수록 마주치는 경고 수는 줄어들겠지만 새로 작성한 코드가 한 번에 깨끗하게 컴파일되리라 기대하지는 맙시다.
4 | 대부분의 비검사 경고는 쉽게 제거할 수 있습니다. 코드를 다음처럼 잘못 작성했다고 예시를 들어봅시다. 5 | ``` java 6 | Set fruits = new Hashset(); 7 | ``` 8 | 그러면 컴파일러는 무엇이 잘못 됐는지 설멸해 줄 것입니다(javac 명령줄 인수에 -Xlint:unchecked 옵션을 추가해야 합니다) 9 |
10 | 컴파일러가 알려준 대로 수정하면 경고가 사라집니다. 자바 7부터는 컴파일러가 알려준 타입 매개변수로 명시하지 않아도 타입 추론을 지원합니다. 11 | ``` java 12 | Set fruits = new Hashset<>(); 13 | ``` 14 | 할 수 있는 한 모든 비검사 경고를 제거합시다. 모두 제거한다면 그 코드는 타입 안전성이 보장됩니다. 경고를 제거할 수는 없지만 타입이 안전하다고 확신이 들 때는 **@SuppressWarning("unchecked")** 애너테이션을 달아 경고를 숨깁시다. @SuppressWarning 애너테이션은 개별 지역번수 선언부터 클래스 전체까지 어떤 선언에도 달 수 있습니다. 하지만 가능한 좁은 범위에 적용하는 게 좋습니다.
15 | 자칫 심각한 경고를 놓칠 수 있으니 절대로 클래스 전체에 적용해서는 안 됩니다. @SuppressWarning("unchecked") 애너테이션을 사용할 때면 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야 합니다. 16 | ### 정리 17 | - 비검사 경고는 중요하니 무시하지 말자. 모든 비검사 경고는 런타임에 ClassCastException을 일으킬 수 있는 잠재적 가능성이 있습니다. 18 | - 경고를 없앨 방법을 찾지 못했다면, 그 코드의 안전함을 증명하고 가능한 범위를 좁혀서 @SuppressWarning("unchecked") 애노테이션으로 경고를 숨깁시다. -------------------------------------------------------------------------------- /chap05/Item28.md: -------------------------------------------------------------------------------- 1 | # [Item 28] 배열보다는 리스트를 사용하라. 2 | 3 | ### 배열과 제네릭 타입의 차이 4 | 첫번째 차이점. 배열은 공변 입니다. 어려워 보이는 단어지만 뜻은 간단합니다. Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 됩니다.
5 | 반면, 제네릭은 불공변입니다. 즉 서로 다른 타입 Type1과 Type2가 있을 때, List은 List의 하위 타입도 아니고 상위 타입도 아닙니다. 6 | 이것만 보면 제네릭에 문제가 있다고 생각할 수 있지만, 사실 문제가 있는 건 배열 쪽입니다. 다음은 문법상 허용되는 코드입니다. 7 | ``` java 8 | // 런타임 에러 9 | Object[] objectArray = new Long[1]; 10 | objectArray[0] = "타입이 달라 넣을 수 없다."; // 런타임시 ArrayStoreException을 던진다 11 | ``` 12 | ``` java 13 | // 컴파일 에러 14 | List ol = new ArrayList(); // 호환되지 않는 타입 15 | ``` 16 | 어느 쪽이든 Long용 저장소에 String을 넣을 수 없습니다. 다만 배열 같은 경우 런타임에야 알 수 있고, 리스트는 컴파일 시에 알아챌 수 있어서 더 좋습니다. 17 |
18 | 두번째 차이점. 배열은 실체화(reify)됩니다. 무슨 뜻이냐면 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인합니다. 그래서 위 코드에서 Long배열에 String을 넣으려 하면 19 | ArrayStoreException이 발생합니다. 반면 제네릭은 타입 정보가 런타임에는 소거(erasure)됩니다.
20 | 원소 타입을 컴파일타임에만 검사하며 런타임에는 알 수 조차도 없다는 뜻입니다. 소거는 제네릭 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘으로, 자바 5가 제네릭으로 순조롭게 전환될 수 있도록 해주었습니다. 21 |
22 | 이상의 주요 차이로 인해 배열과 제네릭은 잘 어울러지지 못합니다. 예컨대 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없습니다. 즉 코드를 new List[], new List[], new E[] 식으로 작성하면 컴파일할 때 제네릭 배열 생성 오류를 일으킵니다.
23 | 24 | ### 제네릭 배열을 만들지 못하게 막은 이유 25 | 타입이 안전하지 않기 때문입니다. 이를 허용하면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 **ClassCaseException**이 발생할 수 있습니다. 런타임에 ClassCaseException이 발생하는 일을 막아주겠다는 제네릭 타입 시스템의 취지에 어긋나는 것입니다. 26 | ``` java 27 | // 컴파일 되지 않는다. 28 | List[] stingLists = new List[1]; // (1) 29 | List intList = List.of(42); // (2) 30 | Object[] objects = stingLists; // (3) 31 | Object[0] = intList; // (4) 32 | String s = stingLists[0].get(0); // (5) 33 | ``` 34 | 제네릭 배열을 생성하는 (1)이 허용된다고 가정해봅시다. (2)는 원소 하나인 List를 생성합니다. (3)은 (1)에서 생성한 List의 배열을 Object 배열에 할당합니다. 35 | 배열은 공변이니 아무 문제가 없습니다. (4)는 (2)에서 생성한 List의 인스턴스를 Object 배열의 첫 원소로 저장합니다. 제네릭은 소거 방식으로 구현되어서 이 역시 성공합니다. 즉, 런타임에는 List 소거 방식으로 이 역시 소거됩니다. 즉 런타임에는 List안스턴스의 타입은 단순히 List가 되고, List[] 인스턴스의 타입은 List[]가 됩니다. 따라서 (4)에서도 ArrayStoreException을 발생시키지 않습니다.
36 | 문제는 List인스턴스만 담겠다고 선언한 stingLists 배열에는 지금 List 인스턴스가 저장돼 있습니다. 결국 (5)에서 ClassCaseExceptionn이 발생하게 됩니다. 37 | 이런 일을 방지하려면 (제네릭 배열이 생성되지 않도록) (1)에서 컴파일 오류를 발생해야 합니다. 38 |
39 | E, List, List 같은 타입을 실체화 불가 타입(non-reifiable type)이라 합니다. 쉽게 말하자면, 실체화되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입입니다. 소거 메커니즘 때문에 매개변수화 타입 가운데 실체화될 수 있는 타입은 List와 Map같은 비한정적 와일드카드 타입뿐입니다. 배열을 비한정적 와일드카드 타입으로 만들 수는 있지만, 유용하게 쓰일 일은 별로 없습니다. 40 |
41 | 42 | ### 정리 43 | - 배열은 공변이고 실체화가 되지만 제네릭은 불공변이고 타입 정보가 소거됩니다. 44 | - 배열은 런타임에는 타입 안전하지만 컴파일타임에는 그렇지 않습니다. 45 | 46 | -------------------------------------------------------------------------------- /chap05/Item29.md: -------------------------------------------------------------------------------- 1 | # [Item 29] 이왕이면 제네릭 타입으로 만들라. 2 | 3 | Item 7에서 다루었던 스택 코드를 제네릭으로 변형한 코드입니다. 4 | ```java 5 | public class Stack { 6 | private E[] elements; 7 | private int size = 0; 8 | private static final int DEFAULT_INITIAL_CAPACITY = 16; 9 | 10 | public Stack() { 11 | elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // 경고 메세지 타입이 안전하지 않음 12 | } 13 | 14 | public void push(E e) { 15 | ensoureCapaciy(); 16 | elements[size++] = e; 17 | } 18 | 19 | public E pop() { 20 | if (size == 0) { 21 | throw new EmptyStackException(); 22 | } 23 | E result = elements[--size]; 24 | elements[size] = null; 25 | return result; 26 | } 27 | ... 중략 28 | } 29 | ``` 30 | 컴파일러는 이 프로그램이 안전한지 증명할 방법은 없지만, 우리는 할 수 있는 한 이 비검사 형변환이 프로그램의 타입 안전성을 해치지 않는지 스스로 확인해야합니다. 31 |
32 | 비검사 형변환이 안전하다는 걸 확인했다면 범위를 최소로 좁혀 @SuppressWarnings("unchecked")를 이용하여 해당 경고를 숨깁시다. 33 | ``` java 34 | // 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다. 35 | // 따라서 타입 안전성을 보장하지만 36 | // 이 배열의 런타임 타입은 E[]가 아닌 Object[]입니다. 37 | @SuppressWarnings("unchecked") 38 | public Stack() { 39 | elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; 40 | } 41 | ``` 42 | 위 방법 말고 다른 방식으로는 elements 필드의 타입을 E[]에서 Object[]로 바꾸는 것입니다. 이렇게 하면 pop메서드 부분을 다음과 같이 수정해줘야 합니다. 43 | ``` java 44 | E result = (E) elements[--size]; 45 | ``` 46 | E는 실체화 불가 타입이므로 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없습니다. 이번에도 직접 증명하고 경고를 숨길 수 있습니다. 47 | ``` java 48 | @SuppressWarnings("unchecked") 49 | E result = (E) elements[--size]; 50 | ``` 51 | 첫 번째 방식은 형변환을 배열 생성시 단 한 번만 해주면 되지만, 두 번째 방식에서는 배열에서 원소를 읽을 때마다 해줘야하므로 첫 번째 방식이 더 자주 사용됩니다. 하지만 52 | (E가 Object가 아닌 한) 배열의 런타임 타입이 컴파일타임 타입과 달라 힙 오염을 일으킵니다.
53 | 사실 제네릭 타입 안에서 리스트를 사용하는 게 항상 가능하더라도, 꼭 더 좋은 건 아닙니다. 자바가 리스트를 기본 타입으로 제공하지 않으므로 ArrayList같은 제네릭 타입도 결국은 기본 타입인 배열을 사용해 구현해야 합니다. 또한 HashMap 같은 제네릭 타입은 성능을 높일 목적으로 배열을 사용하기도 합니다. 54 | ### 정리 55 | - 클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편합니다. 56 | - 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 합시다. 57 | - 기존 타입 중 제네릭이었어야 하는 게 있다면 제네릭 타입으로 변경합시다. 58 | 59 | 60 | -------------------------------------------------------------------------------- /chap05/Item30.md: -------------------------------------------------------------------------------- 1 | # [Item 30] 이왕이면 제네릭 메서드로 만들라 2 | 3 | 제네릭 메서드는 대표적으로 `Collections`의 알고리즘 메서드(binarySearch, sort등)가 있습니다. 사용 방법은 리턴타입 앞에다 타입을 명시해주면 됩니다. 다음은 두 집합의 합집합을 반환하는 문제가 있는 메서드입니다. 4 | ``` java 5 | // raw tyoe 사용 - 수용 불가 6 | public static Set union(Set s1, Set s2) { 7 | Set result = new HashSet(s1); 8 | result.addAll(s2); 9 | return result; 10 | } 11 | ``` 12 | 컴파일은 되지만 경고가 발생합니다. 경고를 없애려면 이 메서드 타입을 안전하게 만들어야 합니다. 다음 코드에서 타입 매개변수 목록은 이고 반환 타입은 Set입니다. 13 | ``` java 14 | public static Set union(Set s1, Set s2) { 15 | Set result = new HashSet<>(s1); 16 | result.addAll(s2); 17 | return result; 18 | } 19 | ``` 20 | 다음 코드는 이 메서드를 사용하는 프로그램입니다. 직접 형변환 하지 않아도 어떤 오류나 경고 없이 컴파일 됩니다. 21 | ``` java 22 | public static void main(Sting[] args) { 23 | Set guys = Set.of("pual", "jin"); 24 | Set stooges = Set.of("jerry", "sia"); 25 | Set aflCio = union(guys, stooges); 26 | System.out.println(aflCio); 27 | } 28 | ``` 29 | 이를 한정적 와일드카드 타입을 사용하면 더 유연하게 개선할 수 있습니다. 30 |
31 | 제네릭은 런타임에 타입 정보가 소거되므로 하나의 객체를 어떤 타입으로든 매개변수화 할 수가 있습니다. 하지만 이렇게 하려면 요청한 타입 매개변수에 맞게 매번 그 객체의 타입을 바꿔주는 정적 팩터리를 만들어야 합니다. 이 패턴을 제네릭 싱글턴 팩터티라 하며, Collections.reverseOrder 같은 함수 객체나 Collections.emptySet 같은 컬렉션용으로 사용합니다. 32 |
33 | 이번에는 항등함수를 담은 클래스를 만들고 싶다고 해봅시다. 자바 라이브러리의 Function.idenify를 사용해도 되지만 직접 작성해 보겠습니다. 항등함수 객체는 상태가 없으니 요청할 때마다 새로 생성하는 것은 낭비입니다. 자바의 제네릭이 실체화된다면 항등함수를 타입별로 하나씩 만들어야 했겠지만, 소거 방식을 사용한 덕에 제네릭 싱글턴 하나면 충분합니다. 34 | 35 | ``` java 36 | private static UnaryOperator IDENTIFY_FN = (t) -> t; 37 | 38 | @SuppressWarnings("unchecked") 39 | public static UnaryOperator identifyFunction() { 40 | return (UnaryOperator) IDENTIFY_FN; 41 | } 42 | ``` 43 | T가 어떤 타입이든 UnaryOperator를 사용해도 type safe합니다.
44 | 상대적으로 드물긴 하지만 자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정할 수 있습니다. 바로 재귀적 타입 한정이라는 개념입니다. 주로 타입의 자연적 순서를 정하는 `Comparable` 인터페이스와 함께 쓰입니다. 45 | 46 | ``` java 47 | public interface Comparable { 48 | int compareTo(T o); 49 | } 50 | ``` 51 | 타입 매개변수 T는 `Comparable`를 구현한 타입이 비교할 수 있는 원소의 타입을 정의합니다. 실제로 거의 모든 타입은 자신과 같은 타입의 원소와만 비교할 수 있습니다. 따라서 String은 Comparable을 구현하고 Integer는 Comparable를 구현한 식입니다. 52 | 53 | ``` java 54 | public static > E max(Collection c); 55 | ``` 56 | 타입 한정인 >는 "모든 타입 E는 자신과 비교할 수 있다"라고 해석할 수 있습니다. 57 |
58 | 다음은 방금 선언한 메서드의 구현입니다. 컬렉션에 담긴 원소의 자연적 순서를 기준으로 최댓값을 계산하며, 컴파일 오류나 경고는 발생하지 않습니다. 59 | 60 | ``` java 61 | public static > E max(Collection c) { 62 | if (c.isEmpty()) { 63 | throw new IllegalArgumentException("컬렉션이 비어 있습니다."); 64 | } 65 | 66 | E result = null; 67 | for (E e : c) { 68 | if (result == null || e.compareTo(result) > 0) { 69 | result = Objects.requireNonNull(e); 70 | } 71 | } 72 | return result; 73 | } 74 | ``` 75 | ### 정리 76 | - 제네릭 타입과 마찬가지로, 클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다 제네릭 메서드가 더 안전하며 사용하기도 쉽습니다. -------------------------------------------------------------------------------- /chap05/Item31.md: -------------------------------------------------------------------------------- 1 | # [Item 31] 한정적 와일드카드를 사용해 API 유연성을 높여라 2 | 3 | 때론 불공변 방식보다 유연한 무언가가 필요할 때가 있습니다. 아이템 29의 Stack 클래스를 떠올려보면 4 | 5 | ``` java 6 | public class Stack { 7 | public Stack(); 8 | public void push (E e); 9 | public E pop(); 10 | public boolean isEmpty(); 11 | } 12 | ``` 13 | 여기서 일련의 원소를 스택에 넣는 메서드를 추가한다고 하면 14 | ``` java 15 | public void pushAll(Iterable src) { 16 | for (E e : src) { 17 | push(e); 18 | } 19 | } 20 | ``` 21 | Iterable src의 원소 타입의 스택의 원소 타입과 일치하면 잘 작동합니다. 하지만 Stack로 선언한 후 pushAll(intVal)을(Iteger 타입) 호출하면 오류가 뜹니다. 매개변수 타입이 불공변이기 떄문입니다.
22 | 이러한 상황에서는 한정된 와일드카드(unbounded wildcard)를 이용해서 해결할 수 있습니다. pushAll의 입력 매개변수 타입은 'E의 iterable'이 아니라 'E의 하위타입 Iterable'이어야 하며, 와일드 카드 Iterable가 정확히 이런뜻입니다. 23 | ``` java 24 | public void pushAll(Iterable src) { 25 | for (E e : src) { 26 | push(e); 27 | } 28 | } 29 | ``` 30 | 위와 같이 수정할 수 있습니다. 이번에는 popAll 와일드카드 타입을 사용하지 않은 메서드를 작성해 보겠습니다. 31 | 32 | ``` java 33 | // 와을드카드 타입을 사용하지 않은 메서드 - 결함이 있습니다. 34 | public void popAll(Collection dst) { 35 | while (!isEmpty()) { 36 | dst.add(pop()); 37 | } 38 | } 39 | ``` 40 | 41 | ``` java 42 | Stack numberStack = new Stack<>(); 43 | Collection objects = ...; 44 | numberStack.popAll(objects); 45 | ``` 46 | 컴파일 하면 "Collection는 Collection의 하위 타입이 아니다"라는 오류가 발생합니다. 이번에는 반대로 'E의 Collection'이 아니라 'E의 상위 타입의 Collection'이어야 합니다. 47 | 48 | ``` java 49 | // 와을드카드 타입을 사용하지 않은 메서드 - 결함이 있습니다. 50 | public void popAll(Collection dst) { 51 | while (!isEmpty()) { 52 | dst.add(pop()); 53 | } 54 | } 55 | ``` 56 | 위와 같이 수정할 수 있습니다. 메세지는 분명합니다. 유연성을 극대화하려면 원소의 생산자나 소비자용 매개변수에 와일드 카드를 사용합시다. 한편,입력 매개변수가 생상자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을 게 없습니다. 타입을 정확히 지정해야 하는 상황으로, 이때는 와일드카드 타입을 쓰지말아야합니다.
57 | 다음 공식을 외워두면 어떤 와일드 카드 타입을 써야 하는지 도움이 될 것입니다. 58 | > PECS: producer-extends, consumer-super 59 | 60 | 즉 매개변수화 타입 T가 생성자라면 를 사용하고, 소비자라면 를 사용합시다. **클래스 사용자가 와일드카드 타입을 신경 써야 한다면 그 API에 무슨 문제가 있을 가능성이 큽니다.** 61 | 62 | ``` java 63 | // Item 30에서 사용했던 코드 64 | public static > E max(List c); 65 | ``` 66 | 이 코드를 와일드카드 타입을 사용해 다듬은 모습입니다. 67 | ``` java 68 | // Item 30에서 사용했던 코드 69 | public static > E max(List c); 70 | ``` 71 | 72 | 위 코드는 PECS 공식을 두 번 적용했습니다. 입력 매개변수에서는 E 인스턴스를 생산하므로 원래의 List를 List로 수정했습니다. 원래 선언에서는 E가 Comparale를 확장한다고 정의헸는데, 이때 Comparable 73 | 는 E 인스턴스를 소비합니다.(그리고 선후 관계를 뜻하는 정수를 생산합니다) 그래서 매개변수화 타입 Comparable는 E 한정적 와일드카드 타입 Comparable로 대체 했습니다. Comparable은 언제나 소비자이므로, 일반적으로 Comparable, 일반적으로 Comparable보다는 Comparable를 사용하는 편이 낫습니다.
74 | Comparator도 마찬가지입니다. 일반적으로 Comparator보다는 Comparator를 사용하는 편이 낫습니다.
75 | 와일드카드와 관련해 논의해야 할 주제가 더 있습니다. 타입 매개변수와 와일드카드에 공통되는 부분이 있어서, 메서드를 정의할 때 둘 중 어느것을 사용해도 괜찮을 때가 많습니다. 76 | ``` java 77 | public static void swap(List list, int i, int j); 78 | public static swap(List list, int i, int j); 79 | ``` 80 | public API라면 간단한 두 번째가 낫습니다. 어떤 리스트든 이 메서드에 넘기면 명시한 인덱스의 원소들을 교환해 줄 것입니다. 신경 써야 할 타입 매개변수도 없습니다.
81 | 기본 규칙은 이렇습니다. **메서드에 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하라.** 이때 비한정적 타입 매개변수라면 비한정적 와일드카드로 바꾸고, 한정적 타입 매개변수라면 한정적 와일드카드로 바꾸면 됩니다.
82 | 하지만 두 번째 swap 선언에는 문제가 하나 있는데, 다음과 같이 직관적으로 구현한 코드가 컴파일 되지 않는다는 것입니다. 83 | ``` java 84 | public static swap(List list, int i, int j) { 85 | list.set(i, list.set(j, list.get(i))); 86 | } 87 | // 방금 꺼낸 원소를 리스트에 다시 넣을 수 없습니다. 88 | ``` 89 | 원인 리스트의 타입이 List인데, List에는 null 외에는 어떤 값도 넣을 수 없기 때문입니다. 90 | ``` java 91 | public static swap(List list, int i, int j) { 92 | swapHelper(list, i, j); 93 | } 94 | 95 | // 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드 96 | public static void swapHelper(List list, int i, int j) { 97 | list.set(i, list.set(j, list.get(i))); 98 | } 99 | ``` 100 | `swapHelper` 메서드는 리스트가 List임을 알고 있습니다. 즉, 이 리스트에서 꺼낸 값은 항상 E이고, E 타입의 값이라면 이 리스트에 넣어도 안전함을 알고 있습니다. 다소 복잡하지만 덕분에 외부에서는 와일드카드 기반의 멋진 선언을 유지할 수 있습니다. 즉 swap 메서드를 호출 하는 클라이언트는 복잡힌 swapHelper의 존재를 모른 채 그 혜택을 누리는 것입니다. -------------------------------------------------------------------------------- /chap05/Item32.md: -------------------------------------------------------------------------------- 1 | # [Item 32] 제네릭과 가변인수를 함께 쓸 때는 신중하라. 2 | 3 | 가변인수와 제네릭은 자바 5에 함께 추가되었는데 이 둘은 서로 어울리지 않습니다.
4 | 가변인수(varargs)란 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있게 해주는 것입니다. 구현 방식에 허점이 있습나다. 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어집니다. 그런데 내부로 감춰야 했을 이 배열을 그만 클라이언트에 노출하는 문제가 생겼습니다. 그 결과 verargs 매개변수에 제네릭이나 매개변수화 타입이 포함되면 알기 어려운 컴파일 경고가 발생합니다. 5 |
6 | 실체화 불가 타입은 런타임에 컴파일보다 타입 관련 정보를 적게 담고 있습니다. 그리고 거의 모든 제네릭과 매개변수화 타입은 실체화되지 않습니다. 메서드 선언할 때 실체화 불가 타입으로 varargs 매겨변수를 선언하면 컴파일러가 경고를 보냅니다. 가변인수 메서드를 호출할 때도 varags 매개변수가 실체화 불가 타입으로 추론되면, 그 호출에 대해서도 경고를 냅니다. 7 |
8 | 매개변수화 타입의 변수가 타입이 다른 다른 객체를 참조하면 힙 오염이 발생합니다. 이렇게 다른 타입 객체를 참조하는 상황에서는 컴파일러가 자동 생성한 형변환이 실패할 수 있으니, 제네릭 타입 시스템이 약속한 타입 안전성의 근간이 흔들려버립니다. 다음 메서드를 예로 생각해봅시다. 9 | 10 | ``` java 11 | static void dangerous(List... stringList) { 12 | List integerList = Collections.singletonList(42); 13 | Object[] objects = stringList; 14 | objects[0] = integerList; // 힙 오염 15 | String s = stringList[0].get(0); // ClassCaseException 16 | } 17 | ``` 18 | 가변인수와 제네릭을 사용하는 메서드는 대표적으로 Arrys.asList, Collections.addAll등이 있습니다. 자바 7부터는 @SafeVarargs 에너테이션을 사용해서 그 메서드가 타입 안전성을 보장한다는 걸 알려줄 수 있습니다.
19 | 메서드가 이 배열에 아무것도 저장하지 않는다면 괜찮지만 아무것도 저장하지 않고도 타입 안전성을 깨뜨릴 수 있습니다. 20 | 21 | ``` java 22 | static T[] toArrays(T... args) { 23 | return args; 24 | } 25 | ``` 26 | 이 메서드가 반환하는 타입은 이 메서드에 인수를 넘기는 컴파일타임에 결정되는데, 그 시점에는 컴파일러에게 충분한 정보가 주이지지 않아 타입을 잘못 판단할 수 있습니다. 따라서 varargs 매개변수 배열을 그대로 반환하면 힙 오염을 이 메서드를 호출한 쪽의 콕스 택으로 까지 전이할 수도 있습니다. 27 | 28 | ```java 29 | static T[] pickTwo(T a, T b, T c) { 30 | switch (ThreadLocalRandom.current().nextInt(3)) { 31 | case 0: return toArrays(a, b); 32 | case 1: return toArrays(a, c); 33 | case 2: return toArrays(c, b); 34 | } 35 | throw new AssertionError(); 36 | } 37 | ``` 38 | toArray 메서드가 돌려준 이 배열이 그대로 pickTwo를 호출한 클라이언트까지 전달되는데 항상 Object[] 타입 배열을 반환하게 됩니다. 39 | ``` java 40 | String[] attributes = pickTwo("좋은", "빠른", "저렴한"); 41 | ``` 42 | 런타임시 `ClassCastException`을 던집니다. pickTwo에서 Object[]을 반환하는데 String[]으로 변환하려고 해서 예외가 발생합니다.
43 | 배열 내용의 일부 함수를 호출하는(varargs를 받지 않는)일반 메서드에 넘기는 것은 안전합니다. 44 | 45 | ``` java 46 | @SafeVarargs 47 | static List flatten(List... lists) { 48 | List result = new ArrayList<>(); 49 | for (List list : lists) { 50 | result.addAll(list); 51 | } 52 | return result; 53 | } 54 | ``` 55 | 임의 개수의 리스트를 받아서, 순서대로 그 안의 모든 원소를 하나의 리스트로 옮겨 반환하는 메서드입니다. @SafeVarargs 애너테이션은 안전하지 않은 varargs 메서드에는 절대 작성해서는 안 됩니다. 힙 오염 경고가 뜨면 무조건 검증을 해야합니다. 그리고 재정의할 수 없는 메서드에만 달아야 합니다. 재정의한 메서드도 안전할지는 보장할 수 없기 때문입니다. 56 | 57 | ``` java 58 | static List flatten(List> lists) { 59 | List result = new ArrayList<>(); 60 | for (List list : lists) { 61 | result.addAll(list); 62 | } 63 | return result; 64 | } 65 | ``` 66 | 위와 같이 @SafeVarargs 애너테이션을 사용하지 않고 verargs 매개변수를 List 매개변수로 바꿀 수도 있습니다. 67 | ### 정리 68 | - 가변인수와 제네릭은 궁합이 좋지 않습니다. 69 | - 가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못하고, 배열과 제네릭의 타입 규칙이서 서로 다르기 때문입니다. 70 | - 제네릭 verargs 매개변수는 type safe하지 않지만, 허용됩니다. 71 | - verargs 매개변수를 사용하려면 타입이 안전한지 확인하고 @SafeVarargs 애너테이션을 이용합시다. 72 | -------------------------------------------------------------------------------- /chap05/Item33.md: -------------------------------------------------------------------------------- 1 | # [Item 33] 타입 안전 이종 컨테이너를 고려하라 2 | 타입 안전 이종 컨테이너 패턴이란 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하는 방식입니다. 3 | ``` java 4 | // 타입 안전 이종 컨테이너 패턴 - API 5 | public class Favorites { 6 | public void putFavorite(Class type, T instance); 7 | public getFavorite(Class type) 8 | } 9 | ``` 10 | 다음은 Favorite 클래스를 사용하는 예시입니다. 11 | ``` java 12 | // 타입 안전 이종 컨태이너 패턴 - 클라이언트 13 | Favorites f = new Favorites(); 14 | 15 | f.putFavorite(String.class, "JAVA"); 16 | f.putFavorite(Integer.class, 0xcafebabe); 17 | f.putFavorite(Class.class, Favorite.class); 18 | 19 | String favoriteString = f.getFavorite(String.class); 20 | Integer favoriteInteger = f.getFavorite(Integer.class); 21 | Class favoriteClass = f.getFavorite(Class.class); 22 | ``` 23 | favorite 인스턴스는 type safe합니다. String을 요청했는데 Integer를 반환할 일이 없습니다. 24 | ``` java 25 | // 타입 안전 이종 컨태이너 패턴 - 구현 26 | public class Favorites { 27 | private Map, Object> favorite = new HashMap<>(); 28 | 29 | public void putFavorite(Class type, T instance) { 30 | favorite.put(Objects.requireNonNull(type), instance); 31 | } 32 | 33 | public T getFavorite(Class type) { 34 | return type.cast(favorites.get(type)); 35 | } 36 | } 37 | 38 | ``` 39 | Map, Object>에서 비한정적 와일드카드 타입을 사용해서 값을 아무것도 넣을 수 없을 거라고 생각할 수 있지만, 맵이 아니라 키가 와일드카드 타입이라서 값을 넣을 수 있습니다.
40 | 지금 만든 `Favorites` 클래스에 주의점이 두 가지가 있습니다.
41 | 첫 번째는 Class 객체를 raw type으로 넘기면 Favorites 인스턴스의 타입 안전성이 쉽게 깨집니다. Favorites가 타입 불변식이 어기는 일이 없도록 보장하려면 다음과 같이 수정할 수 있습니다. 42 | 43 | ``` java 44 | public void putFavorite(Class type, T instance) { 45 | favorites.put(Objects.requireNonNull(type, type.cast(instance))); 46 | } 47 | ``` 48 | java.util.Collections에는 `checkedSet`, `checkedList`, `checkedMap` 가 대표적으로 이 방식을 적용한 메서드입니다. 49 |
50 | 두 번째는 실체화 불가 타입에는 사용할 수 없다는 것입니다. String이나 String[]는 저장할 수 있어도 List은 저장할 수 없습니다. List이나 List는 List.class라는 같은 Class 객체를 공유하기 때문입니다. 51 | ### 정리 52 | - 컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있습니다. 53 | - 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있습니다. 54 | - 타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 합니다. -------------------------------------------------------------------------------- /chap06/Item34.md: -------------------------------------------------------------------------------- 1 | # [Item 34] int 상수 대신 열거 타입을 사용하라. 2 | 3 | 열거 타입이란 일정 개수의 상수 값을 정의하고 그 이외 값들은 허용하지 않는 타입입니다. 대표적으로 사계절, 요일, 태양계의 행성 등이 있습니다. 4 | 5 | ### 열거 타입을 지원하기 전 코드 6 | 7 | ``` java 8 | // 정수 열거 패턴 - 상당히 취약하다. 9 | public static final int EAST = 0; 10 | public static final int WEST = 1; 11 | public static final int SOUTH = 2; 12 | public static final int NORTH = 3; 13 | ``` 14 | 이 코드는 타입의 안전성을 보장할 수 없고 표현력도 좋지 않습니다.
15 | 정수 열거 패턴을 사용한 프로그램은 깨지기가 쉽습니다. 단지 상수를 나열한 것 뿐이라 컴파일하면 그 값이 클라이언트 파일에 그대로 새겨집니다. 따라서 상수의 값이 바뀌면 클라이언트도 수정해줘야 합니다. 16 |
17 | 정수 상수는 그 값을 출력하거나 디버거로 살펴보면 특별한 의미를 나타내는 것이 아니라 단지 숫자로만 보여서 썩 도움이 되지 않습니다. 정수 대신 문자열 상수를 사용하는 방법도 있지만 이 변형 역시 단점이 많습니다. 상수의 의미를 내포할 수 있지만, 문자열 값 그대로 하드코딩 해야하기 때문에 힘듭니다. 이렇게 힘들게 하드코딩한 문자열이 오타가 있어도 컴파일러는 확인할 수 없습니다.
18 | 19 | ### 열거 타입 20 | ``` java 21 | // 단순한 열거 타입 22 | public enum DIRECTION { EAST, WEST, SOUTH, NORTH } 23 | ``` 24 | 25 | 열거 타입 자체는 클래스이며, 상수 하나당 자신의 인스턴스를 하나씩 만들어 **public static final** 필드로 공개합니다. 열거 타입은 컴파일 타임 타입 안전성을 제공합니다. `DIRECTION` 열거 타입을 매개변수로 받는 메서드를 선언 했다면, 건네받은 참조는 null 혹은 DIRECTION의 네 가지 값 중 하나 입니다. 다른 타입의 값을 넘기려 하면 컴파일 오류가 납니다. 26 | ``` java 27 | DIRECTION south = "Sounth" // 컴파일 오류 !!! 28 | ``` 29 | 열거타입에 상수를 추가하거나 순서를 변경해도 다시 컴파일 하지 않아도 됩니다. 공개되는 것이 오직 필드의 이름 뿐이라, 정수 열거 패턴과 달리 상수 값이 클라이언트로 컴파일되어 각인되지 않기 때문입니다. 이처럼 열거 타입은 정수 열거 타입의 단점들을 해소해줍니다. 30 |
31 | 열거 타입에는 메서드나 필드를 추가할 수도 있고 임의의 인터페이스를 구현하게 할 수도 있습니다. `Object`, `Comparable`, `Serializable`을 구현 했으며, 그 직렬화 형태도 웬만큼 변형을 가해도 문제없이 동작하게끔 구현해놨습니다. 32 | 33 | ### 메서드와 필드를 추가한 enum 예제 34 | ``` java 35 | public enum Planet { 36 | MERCURY(3.302e+23, 2.439e6), 37 | VENUS(4.869e+24, 6.052e6), 38 | EARTH(5.975e+24, 6.378e6), 39 | MARS(6.419e+23, 3.393e6), 40 | JUPITER(1.899e+27, 7.149e7), 41 | SATURN(5.685e+26, 6.027e7), 42 | URANUS(8.683e+25, 2.556e7), 43 | NEPTUNE(1.024e+26, 2.477e7); 44 | 45 | private final double mass; // 질량(단위: 킬로그램) 46 | private final double radius; // 반지름(단위: 미터) 47 | private final double surfaceGravity; // 표면 중력(단위: m / s^2) 48 | 49 | // 중력 상수(단위: m^3 / kg s^2) 50 | private static final double G = 6.67300E-11; 51 | 52 | // Constructor 53 | Planet(double mass, double radius) { 54 | this.mass = mass; 55 | this.radius = radius; 56 | surfaceGravity = G * mass / (radius * radius); 57 | } 58 | 59 | public double mass() { 60 | return mass; 61 | } 62 | 63 | public double radius() { 64 | return radius; 65 | } 66 | 67 | public double surfaceGravity() { 68 | return surfaceGravity; 69 | } 70 | 71 | public double surfaceWeight(double mass) { 72 | return mass * surfaceGravity; // F = ma 73 | } 74 | } 75 | ``` 76 | 열거 타입을 선언한 클래스 혹은 그 패키지에서만 유용한 기능은 private이나 package-private 메서드로 구현하는 게 좋습니다. 일반 클래스와 마찬가지로, 그 기능을 클라이언트에 노출해야 할 합당한 이유가 없다면 private 혹은 package-private으로 선언하라(Item 15)랑 같습니다. 77 |
78 | 79 | ### 추천하지 않는 코드 80 | 81 | ``` java 82 | public enum Operation { 83 | 84 | PLUS, MINUS, TIMES, DIVIDE; 85 | 86 | // 상수가 뜻하는 연산을 수행한다. 87 | public double apply(double x, double y) { 88 | switch (this) { 89 | case PLUS: return x + y; 90 | case MINUS: return x - y; 91 | case TIMES: return x * y; 92 | case DIVIDE: return x / y; 93 | } 94 | throw new AssertionError("알 수 없는 연산: " + this); 95 | } 96 | } 97 | ``` 98 | 동작은 하지만 맹점이 있습니다. 예컨대 새로운 상수를 추가하면 해당 case 문도 추가해야 합니다. 열거 타입에 apply를 이용하여 코드를 보완할 수 있습니다. 99 | ### 상수별 메서드 구현을 활용한 열거 타입 100 | ``` java 101 | // 상수별 메서드 구현 102 | public enum Operation { 103 | 104 | PLUS { 105 | public double apply(double x, double y) { 106 | return x + y; 107 | } 108 | }, MINUS { 109 | public double apply(double x, double y) { 110 | return x - y; 111 | } 112 | }, TIMES { 113 | public double apply(double x, double y) { 114 | return x * y; 115 | } 116 | }, DIVIDE { 117 | public double apply(double x, double y) { 118 | return x / y; 119 | } 120 | }; 121 | 122 | public abstract double apply(double x, double y); 123 | } 124 | ``` 125 | 보시다시피 apply 메서드가 상수 선언 바로 옆에 붙어 있으니 새로운 상수를 추가할 때 apply도 재정의해야 한다는 사실을 깜빡하기는 어려울 것입니다. 그 뿐만 아니라 apply 메서드가 추상 메서드이므로 재정의 하지 않았다면 컴파일 오류로 알려줍니다. 상수별 메서드 구현을 상수별 데이터와 결합할 수도 있습니다. 126 | 예컨대 다음은 Operation의 toString을 재정의해 해당 연산을 뜻하는 기호를 반환하도록 하는 예제 코드입니다. 127 | 128 | ``` java 129 | public enum Operation { 130 | PLUS("+") { 131 | public double apply(double x, double y) { 132 | return x + y; 133 | } 134 | }, 135 | MINUS("-") { 136 | public double apply(double x, double y) { 137 | return x - y; 138 | } 139 | }, 140 | TIMES("*") { 141 | public double apply(double x, double y) { 142 | return x * y; 143 | } 144 | }, 145 | DIVIDE("/") { 146 | public double apply(double x, double y) { 147 | return x / y; 148 | } 149 | }; 150 | private final String symbol; 151 | 152 | Operation(String symbol) { 153 | this.symbol = symbol; 154 | } 155 | 156 | @Override 157 | public String toString() { 158 | return symbol; 159 | } 160 | 161 | public abstract double apply(double x, double y); 162 | } 163 | ``` 164 | 165 | 열거 타입에는 상수 이름을 입력받아 그 이름에 해당하는 상수를 반환하는 valueOf(String) 메서드가 자동 생성됩니다. 166 |
167 |
168 | 상수별 메서드 구현에는 열거 타입 상수끼리 코드를 공유하기 어렵다는 단점이 있습니다. 다음은 급명세서에서 쓸 요일을 표현하는 로직을 포함한 열거 타입 예제입니다. 169 | 170 | ``` java 171 | // 위험한 코드 172 | 173 | enum PayrollDay { 174 | MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, 175 | SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND); 176 | 177 | private final PayType payType; 178 | 179 | PayrollDay(PayType payType) { 180 | this.payType = payType; 181 | } 182 | 183 | PayrollDay() { 184 | this(PayType.WEEKDAY); 185 | } // Default 186 | 187 | int pay(int minutesWorked, int payRate) { 188 | return payType.pay(minutesWorked, payRate); 189 | } 190 | 191 | // The strategy enum type 192 | private enum PayType { 193 | WEEKDAY { 194 | int overtimePay(int minsWorked, int payRate) { 195 | return minsWorked <= MINS_PER_SHIFT ? 0 : 196 | (minsWorked - MINS_PER_SHIFT) * payRate / 2; 197 | } 198 | }, 199 | WEEKEND { 200 | int overtimePay(int minsWorked, int payRate) { 201 | return minsWorked * payRate / 2; 202 | } 203 | }; 204 | 205 | abstract int overtimePay(int mins, int payRate); 206 | 207 | private static final int MINS_PER_SHIFT = 8 * 60; 208 | 209 | int pay(int minsWorked, int payRate) { 210 | int basePay = minsWorked * payRate; 211 | return basePay + overtimePay(minsWorked, payRate); 212 | } 213 | } 214 | } 215 | ``` 216 | 하지만 기존 열거 타입에 상수별 동작을 혼합해 넣을 때는 switch 문이 좋은 선택이 될 수도 있습니다. 217 | 218 | ``` java 219 | // switch문을 이용해 원래 열거 타입에 없는 기능을 수행 220 | public static Operation inverse(Operation op) { 221 | switch(op) { 222 | case PLUS: return Operation.MINUS; 223 | case MINUS: return Operation.PLUS; 224 | case TIMES: return Operation.DIVIDE; 225 | case DIVIDE: return Operation.TIMES; 226 | default: thrownewAssertionError("알 수 없는 연산: "+op); 227 | } 228 | } 229 | ``` 230 | 추가하려는 메서드가 의미상 열거 타입에 속하지 않는다면 이 방식을 적용하는 게 좋습니다. 열거 타입은 메모리에 올라가는 공간과 초기화하는 시간이 좀 들긴 하지만 크게 체감될 정도는 아닙니다. -------------------------------------------------------------------------------- /chap06/Item35.md: -------------------------------------------------------------------------------- 1 | # [Item 35] ordinal 메서드 대신 인스턴스 필드를 사용하라 2 | 3 | 대부분 열거 타입 상수는 자연스럽게 하나의 정숫값에 대응됩니다. 모든 엵 ㅓ타입은 해당 상수가 그 열거 타입에서 몇 번째 위치인지를 반환하는 ordinal 이라는 메서드를 제공합니다. 4 | 5 | ``` java 6 | // ordinal을 잘못 사용한 예 7 | public enum FRUITS { 8 | 9 | APPLE, BANANA, ORANGE; 10 | 11 | public int numberOfFruits() { 12 | return ordinal() + 1; 13 | } 14 | } 15 | 16 | ``` 17 | 상수 선언 순서를 바꾸는 순간 numberOfFruits는 오작동하며 이미 사용중인 정수와 값이 같은 상수는 추가할 수 없고 중간에 값을 비울 수도 없습니다.
18 | 해결책은 매우 간단합니다. ordinal 메서드를 이용하지 않고 인스턴스 필드에 저장하면 됩니다. 19 | ``` java 20 | public enum FRUITS { 21 | APPLE(1), BANANA(2), ORANGE(3); 22 | 23 | private final int numberOfFruits; 24 | 25 | FRUITS(int size) { 26 | this.numberOfFruits = size; 27 | } 28 | 29 | public int getNumberOfFruits() { 30 | return numberOfFruits; 31 | } 32 | } 33 | ``` 34 | Enum의 API 문서에는 ordinal에 대해서 "대부분 프로그래머는 이 메서드를 쓸 일이 없습니다. 이 메서드는 EnumSet과 EnumMap 같이 열거 타입 기반의 범용 자료구조에 쓸 목적으로 설계되었습니다." -------------------------------------------------------------------------------- /chap06/Item36.md: -------------------------------------------------------------------------------- 1 | # [Item 36] 비트 필드 대신 EnumSet을 사용하라. 2 | 3 | 열거한 값들이 집합으로 사용될 경우, 예전에는 상수에 서로 다른 2의 거듭제곱 값을 할당한 정수 열거 패턴을 사용해왔습니다. 4 | 5 | ``` java 6 | // 비트 필드 열거 상수 - 구닥다리 기법 7 | public class Text { 8 | public static final int STYLE_BOLD = 1 << 0; // 1 9 | public static final int STYLE_ITALIC = 1 << 1; // 2 10 | public static final int STYLE_UNDERLINE = 1 << 2; // 4 11 | public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8 12 | 13 | // 매개변수 styles는 0개 이상의 STYLE_ 상수를 비트별 OR한 값입니다. 14 | public void applyStyles(int styles) { ... } 15 | } 16 | ``` 17 | 다음과 같은 식으로 비트별 OR를 사용해 여러 상수를 하나의 집합으로 모을 수 있으며, 이렇게 만들어진 집합을 비트 필드라고 합니다. 18 | ``` java 19 | text.applyStyles(Text.STYLE_BOLD | Text.STYLE_ITALIC); 20 | ``` 21 | 비트 필드를 사용하면 비트별 연산을 사용해 집합 연산을 효율적으로 사용할 수 있지만, 정수 열거 상수와 같은 단점을 지니고 있으며 추가로 다음과 같은 단점들이 있습니다. 22 |
23 | 1. 비트 필드 값이 그대로 출력되면 정수 열거 상수를 출력할 때보다 해석하기가 훨씬 어렵습니다. 24 | 2. 비트 필드 하나에 녹아 있는 모든 원소를 순회하기도 까다롭습니다. 25 | 3. 최대 몇 비트가 필요한지를 API 작성 시 미리 예측하여 적절한 타입을 선택해야 합니다. API를 수정하지 않고는 비트 수를 더 늘릴 수 없기 때문입니다. 26 |
27 | `EnumSet`을 이용하면 이러한 단점을 보완하여 사용할 수 있습니다. EnumSet의 내부는 비트 벡터로 구현되어있고, 원소가 총 64개 이하라면 EnumSet 전체를 long 변수 하나로 표현하여 비트 필드에 비견되는 성능을 보여줍니다. 28 | 29 | ``` java 30 | public class Text { 31 | public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH } 32 | 33 | // 어떤 Set을 넘겨도 됩지만 EnumSet이 가장 좋습니다. 34 | public void applyStyles(Set