Skip to content

Commit a3e9f9f

Browse files
authored
Merge pull request #19 from HD-Malrang/김수빈
docs: Update to item34-43
2 parents 47a2d24 + 01dc709 commit a3e9f9f

File tree

4 files changed

+511
-0
lines changed

4 files changed

+511
-0
lines changed

김수빈/6장/item34.md

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
## item34 예제, 열거 타입의 제약
2+
3+
ex 1. 데이터와 메서드를 갖는 열거 타입
4+
5+
public enum Planet { MERCURY(3.302e+23, 2.439e6), VENUS (4.869e+24, 6.052e6), EARTH (5.975e+24, 6.378e6), MARS (
6+
6.419e+23, 3.393e6), JUPITER(1.899e+27, 7.149e7), SATURN (5.685e+26, 6.027e7), URANUS (8.683e+25, 2.556e7), NEPTUNE(
7+
1.024e+26, 2.477e7);
8+
9+
```java
10+
private final double mass; // 질량(단위: 킬로그램)
11+
private final double radius; // 반지름(단위: 미터)
12+
private final double surfaceGravity; // 표면중력(단위: m / s^2)
13+
14+
// 중력상수(단위: m^3 / kg s^2)
15+
private static final double G=6.67300E-11;
16+
17+
// 생성자
18+
Planet(double mass,double radius){
19+
this.mass=mass;
20+
this.radius=radius;
21+
surfaceGravity=G*mass/(radius*radius);
22+
}
23+
24+
public double mass(){return mass;}
25+
public double radius(){return radius;}
26+
public double surfaceGravity(){return surfaceGravity;}
27+
28+
public double surfaceWeight(double mass){
29+
return mass*surfaceGravity; // F = ma
30+
}
31+
}
32+
```
33+
34+
```java
35+
public class WeightTable {
36+
public static void main(String[] args) {
37+
double earthWeight = Double.parseDouble(args[0]);
38+
double mass = earthWeight / Planet.EARTH.surfaceGravity();
39+
for (Planet p : Planet.values())
40+
System.out.printf("%s에서의 무게는 %f이다.%n",
41+
p, p.surfaceWeight(mass));
42+
}
43+
}
44+
```
45+
46+
열거 타입은 자신 안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메서드인 values를 제공.
47+
48+
값들은 선언된 순서로 저장된다. (그렇다고 이 순서에 의존하는 코드를 작성하는 것은 매우 위험)
49+
50+
ex 2. 값에 따라 분기하는 열거 타입
51+
52+
```java
53+
public enum Operation {
54+
PLUS, MINUS, TIMES, DIVDE;
55+
56+
public double apply(double x, double y) {
57+
switch (this) {
58+
case PLUS:
59+
return x + y;
60+
case MINUS:
61+
return x - y;
62+
case TIMES:
63+
return x * y;
64+
case DIVDE:
65+
return x / y;
66+
}
67+
throw new AssertionError("알 수 없는 연산:" + this);
68+
}
69+
}
70+
```
71+
72+
-> 깨지기 쉬운 코드 : 새로운 상수 추가시 해당 case문도 추가해야한다. 혹시라도 깜빡하게 되면 오류가 발생한다.
73+
74+
ex 3. 상수별 클래스 몸체와 데이터를 사용한 열거 타입
75+
76+
- 열거 타입에 apply라는 추상 메서드를 선언하고 각 상수별 클래스 몸체, 즉 각 상수에서 자신에 맞게 재정의하는 방법.
77+
78+
-> apply가 추상 메서드이므로 재정의하지 않았다면 컴파일 오류로 알려준다. (깜빡할 수가 없다.)
79+
80+
```java
81+
public enum Operation {
82+
PLUS("+") {
83+
public double apply(double x, double y) {
84+
return x + y;
85+
}
86+
},
87+
MINUS("-") {
88+
public double apply(double x, double y) {
89+
return x - y;
90+
}
91+
},
92+
TIMES("*") {
93+
public double apply(double x, double y) {
94+
return x * y;
95+
}
96+
},
97+
DIVDE("/") {
98+
public double apply(double x, double y) {
99+
return x / y;
100+
}
101+
}
102+
}
103+
```
104+
105+
```java
106+
private final String symbol;
107+
108+
Operation(String symbol){
109+
this.symbol=symbol;
110+
}
111+
112+
@Override
113+
public String toString(){
114+
return symbol;
115+
}
116+
117+
public abstract double apply(double x,double y);
118+
}
119+
```
120+
121+
ex 4. 열거 타입용 fromString 메서드 구현하기
122+
123+
- toString 재정의시 toString이 반환하는 문자열을 해당 열거 타입 상수로 변환해주는 fromString 메서드 구현도 고려.
124+
125+
```java
126+
private static final Map<String, Operation> stringToEnum=
127+
Stream.of(Operation.values())
128+
.collect(Collectors.toMap(Operation::toString,operation->operation));
129+
// {문자열, 열거 타입 상수}
130+
131+
//Optional로 반환하여 값이 존재하지않을 상황을 클라이언트에게 알린다.
132+
public static Optional<Operation> fromString(String symbol){
133+
return Optional.ofNullable(stringToEnum.get(symbol));
134+
}
135+
136+
```
137+
138+
열거 타입의 제약
139+
140+
1. 생성자에서 접근할 수 있는 것은 상수 변수뿐이다.
141+
142+
- 열거 타입의 생성자가 실행되는 시점에는 정적 필드가 초기화되기 전이기 때문에 생성자에서 정적 필드를 참조하려고 시도하면 컴파일 에러가 발생한다.
143+
144+
따라서 자신의 인스턴스를 추가하지 못하게 하는 제약이 존재.
145+
146+
(It is illegal to access static member 'ENUM' from enum constructor or instance initializer)
147+
148+
열거 타입 상수 생성 순서
149+
150+
2. 열거 타입 상수끼리 코드를 공유하기 어렵다.
151+
152+
-> 전략 열거 타입 패턴 (내부에 또 다른 enum을 정의해서 선택하도)
153+
154+
```java
155+
public enum PayrollDay {
156+
MONDAY(PayType.WEEKDAY),
157+
TUESDAY(PayType.WEEKDAY),
158+
WEDNESDAY(PayType.WEEKDAY),
159+
THURSDAY(PayType.WEEKDAY),
160+
FRIDAY(PayType.WEEKDAY),
161+
SATURDAY(PayType.WEEKEND),
162+
SUNDAY(PayType.WEEKEND);
163+
164+
private final PayType payType;
165+
166+
PayrollDay(PayType payType) {
167+
this.payType = payType;
168+
}
169+
170+
int pay(int minutesWorked, int payRate) {
171+
return payType.pay(minutesWorked, payRate);
172+
}
173+
174+
private enum PayType {
175+
WEEKDAY {
176+
int overtimePay(int minutesWorked, int payRate) {
177+
return minutesWorked <= MINS_PER_SHIFT ?
178+
0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
179+
}
180+
},
181+
WEEKEND {
182+
int overtimePay(int minutesWorked, int payRate) {
183+
return minutesWorked * payRate / 2;
184+
}
185+
};
186+
187+
abstract int overtimePay(int minutesWorked, int payRate);
188+
189+
private static final int MINS_PER_SHIFT = 8 * 60;
190+
191+
int pay(int minutesWorked, int payRate) {
192+
int basePay = minutesWorked * payRate;
193+
return basePay + overtimePay(minutesWorked, payRate);
194+
}
195+
}
196+
}
197+
```
198+
199+
3. 코드를 수정 할 수 없는 기존 열거 타입에 상수별 동작을 넣을 때는 switch문이 좋은 선택이 될 수 있다.
200+
201+
```java
202+
public static Operation inverse(Operation operation){
203+
switch(operation){
204+
case PLUS:
205+
return Operation.MINUS;
206+
case MINUS:
207+
return Operation.PLUS;
208+
case TIMES:
209+
return Operation.DIVDE;
210+
case DIVDE:
211+
return Operation.TIMES;
212+
}
213+
throw new AssertionError("알 수 없는 연산 : "+operation);
214+
}
215+
```

김수빈/6장/item37.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
## ordinal 인덱싱 대신 EnumMap을 사용하라
2+
3+
```java
4+
class Plant {
5+
enum LifeCycIe {AMMUAL, PERENNIAL, BIEMMIAL}
6+
7+
final String name;
8+
final LifeCycIe lifeCycle;
9+
10+
Plant(String name, LifeCycIe lifeCycle) {
11+
this.name = name;
12+
this.lifeCycle = lifeCycle;
13+
}
14+
15+
@Override
16+
public String toString() {
17+
return name;
18+
}
19+
}
20+
```
21+
22+
### ordinal() 을 배열 인덱스로 사용한 예 - 따라하면 안 됨
23+
24+
```java
25+
public static void usingOrdinalArray(List<Plant> garden){
26+
// 1. 배열과 제네릭의 호환성 문제
27+
Set<Plant>[]plantsByLifeCycle=(Set<Plant>[])new Set[LifeCycle.values().length];
28+
for(int i=0;i<plantsByLifeCycle.length;i++){
29+
plantsByLifeCycle[i]=new HashSet<>();
30+
}
31+
32+
for(Plant plant:garden){
33+
// 2. 열거 타입의 ordinal 사용
34+
plantsByLifeCycle[plant.lifeCycle.ordinal()].add(plant);
35+
}
36+
37+
for(int i=0;i<plantsByLifeCycle.length;i++){
38+
// 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 한다
39+
System.out.printf("%s : %s%n",LifeCycle.values()[i],plantsByLifeCycle[i]);
40+
}
41+
}
42+
```
43+
44+
제네릭 배열을 생성하지 못하는 이유
45+
46+
- ordinal() 사용시 : 정수는 열거 타입과 달리 타입 안전하지 않기때문에 정확한 정수값을 사용하는지 개발자가 직접 보증해야 한다. 잘못된 값을 사용하여 잘못된 동작을 하더라도 에러가 발생하지 않을 수
47+
있다.
48+
49+
### EnumMap 사용
50+
51+
```java
52+
public static void usingEnumMap(List<Plant> garden){Map<LifeCycle, Set<Plant>>plantsByLifeCycle=new EnumMap<>(
53+
LifeCycle.class);
54+
55+
for(LifeCycle lifeCycle:LifeCycle.values()){
56+
plantsByLifeCycle.put(lifeCycle,new HashSet<>());
57+
}
58+
59+
for(Plant plant:garden){
60+
plantsByLifeCycle.get(plant.lifeCycle).add(plant);
61+
}
62+
System.out.println(plantsByLifeCycle);
63+
64+
}
65+
```
66+
67+
- 더 짧고 명료하고 안전하고 성능도 원래 버전과 비등
68+
69+
: 내부적으로 배열을 사용하므로 배열의 성능을 유지하면서도, 열거 타입의 타입 안정성을 활용
70+
71+
- 안전하지 않은 형변환은 쓰지 않고, 맵의 키인 열거 타입이 그 자체로 출력용 문자열을 제공
72+
73+
### 스트림 사용
74+
75+
```java
76+
public static void streamEx1(List<Plant> garden){Map plantsByLifeCycle=garden.stream().collect(
77+
Collectors.groupingBy(plant->plant.lifeCycle));System.out.println(plantsByLifeCycle);}
78+
79+
// 성능 개선을 위해 EnumMap과 Set 사용 public static void streamEx2(List<Plant> garden) { Map plantsByLifeCycle = garden.stream()
80+
.collect(Collectors.groupingBy(plant->plant.lifeCycle,
81+
()->new EnumMap<>(LifeCycle.class),Collectors.toSet()));System.out.println(plantsByLifeCycle);}
82+
83+
```
84+
85+
- EnumMap 버전은 언제나 식물의 생애주기당 하나씩의 중첩 맵을 만들지만, 스트림 버전에서는 해당 생애주기에 속하는 식물이 있을 때만 만든다.
86+
87+
### 정리
88+
89+
배열의 인덱스를 얻기 위해 ordinal을 쓰는 것은 일반적으로 좋지 않으니, 대신 EnumMap을 사용하라.
90+
91+
다차원 관계는 EnumMap<..., EnumMap<...>> 으로 표현하라

김수빈/6장/item40.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
## @Override 애너테이션을 일관되게 사용하라
2+
3+
### @Override가 없을 때 하기 쉬운 실수
4+
5+
```java
6+
public class Item40Test {
7+
static class Bigram {
8+
private final char first;
9+
private final char second;
10+
11+
public Bigram(char first, char second) {
12+
this.first = first;
13+
this.second = second;
14+
}
15+
16+
public boolean equals(Bigram b) {
17+
return b.first == first && b.second == second;
18+
}
19+
20+
public int hashCode() {
21+
return 31 * first + second;
22+
}
23+
}
24+
25+
@Test
26+
public void bigramTest() {
27+
Set<Bigram> s = new HashSet<>();
28+
for (int i = 0; i < 10; i++) {
29+
for (char ch = 'a'; ch <= 'z'; ch++) {
30+
s.add(new Bigram(ch, ch));
31+
}
32+
}
33+
34+
Assertions.assertEquals(26, s.size()); // 실제 값 260
35+
}
36+
}
37+
```
38+
39+
- bigramTest() 가 원한 결과는 s.size()가 26인 것이지만 실제로는 260이 나왔다.
40+
- equals()에 @Override 애너테이션을 붙이지 않아서 생긴 실수가 있다.
41+
- Object에서 상속받는 equals()는 원래 Object 타입의 파라미터를 받는데, Bigram의 파라미터를 받고 있다.
42+
- 그래서 Set에서 비교에 사용되는 equals()가 제대로 정의되지 않고, 오직 객체 주소의 동치만 비교하는 기본 Object의 equals()가 쓰이고 있던 것이다.
43+
- 따라서 같은 소문자를 소유한 바이그램 10개 각각이 서로 다른 객체로 인식되고, 결국 260을 출력한 것이다.
44+
- 실제 오버라이딩(재정의)된 것이 아니라 오버로딩을 하고 있는 것이다.
45+
46+
### 실수 고치기
47+
48+
```java
49+
@Override
50+
public boolean equals(Object o){
51+
if(!(o instanceof Bigram)){
52+
return false;
53+
}
54+
55+
Bigram b=(Bigram)o;
56+
return b.first==first&&b.second==second;
57+
}
58+
```
59+
60+
- 위와 같이 @Override 애너테이션을 달면, 실제로 상속받은 메서드가 아니면 에러를 내주기 때문에 실수할 확률이 적어진다.
61+
- 잘못한 부분을 명확히 알려주므로 곧장 올바르게 수정할 수 있다.
62+
63+
### 핵심 정리
64+
65+
- 재정의한 모든 메서드에 @Override 애너테이션을 달아 실수를 방지하자.
66+
- 단, 구체 클래스에서 상위 클래스의 추상 메서드를 재정의한 경우엔 이 애너테이션을 달지 않아도 된다.

0 commit comments

Comments
 (0)