Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions 전두이/4장/item23.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 태그 달린 클래스보다는 클래스 계층구조를 활용하라

열거 타입 선언, 태그 필드, switch 문 등 여러 구현이 한 클래스에 혼합되어 있는 코드는 가독성이 나쁘다.

1. **태그 달린 클래스의 문제점**:
- 불필요한 코드와 데이터 필드가 추가되어 코드가 복잡하고 장황해짐
- 타입별로 조건문(`switch`, `if-else`)을 사용해야 하며, 새로운 타입을 추가할 때 실수로 조건문을 빼먹으면 런타임 오류가 발생할 가능성이 있음
- 확장성과 유연성이 떨어짐. 다른 개발자가 새로운 타입을 추가하거나 유지보수하기 어려움
2. **클래스 계층구조의 장점**:
- **간결하고 명확함**: 각 의미를 독립된 클래스로 분리해 관련 없는 데이터 필드를 제거
- **타입 안전성**: 각 클래스의 생성자가 모든 필드를 초기화하고, 추상 메서드를 구현하도록 컴파일러가 보장
- **유연성과 확장성**: 루트 클래스의 코드를 변경하지 않고도 계층구조를 확장할 수 있음
- **컴파일 타임 타입 검사**: 특정 타입(예: `Rectangle`, `Circle`)만 매개변수로 제한하거나 명시적으로 다룰 수 있음
- **자연스러운 계층 관계 반영**: 예를 들어, 정사각형(`Square`)이 사각형(`Rectangle`)의 특별한 형태임을 간단히 표현 가능
3. **리팩터링 권장**:
- 새로운 클래스를 작성하는데 태그 필드가 등장한다면, 태그를 없애고 **계층구조**로 대체하는 방법을 고려하라
- 기존 클래스에 태그 필드가 있다면, **계층구조로 리팩터링**하는 것을 고민하라

---

### **결론**

> 태그 달린 클래스는 코드가 복잡하고 유지보수가 어렵기 때문에, **클래스 계층구조로 설계**하여 간결하고 확장 가능한 구조를 만들어야 한다.
>
47 changes: 47 additions & 0 deletions 전두이/4장/item24.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 멤버 클래스는 되도록 static으로 만들어라

## 중첩 클래스란?

다른 클래스 안에 정의된 클래스

- 정적 멤버 클래스
- (비정적) 멤버 클래스
- 익명 클래스
- 지역 클래스

### 정적 멤버 클래스

- `static` 키워드가 붙어있으며, 바깥 클래스의 **private 멤버**에도 접근 가능
- **바깥 클래스와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 사용**
- 메모리와 생성 시간이 절약된다
- **주 용도**: 바깥 클래스와 함께 사용되는 **public 도우미 클래스**로 활용
- 예: 계산기의 연산(Operation)을 정의하는 열거 타입

### 비정적 멤버 클래스

- `static`이 없으며, **바깥 클래스의 인스턴스와 암묵적으로 연결**됨
- 바깥 클래스의 **인스턴스 없이는 생성할 수 없음**
- 인스턴스 간 연결 정보가 메모리를 차지하며 생성 비용이 더 듦
- **주 용도**: 바깥 클래스의 인스턴스에 접근해야 할 때
- 예: 컬렉션의 뷰(Map의 keySet, entrySet 등)나 반복자(iterator) 구현

### 익명 클래스와 지역 클래스

- **익명 클래스**: 메서드 안에서 사용하며, **짧고 간결하게 표현할 때** 적합
- **지역 클래스**: 한 메서드 안에서만 쓰이며, 익명 클래스로 표현하기 어려울 때 사용

### 권장 사항

외부 클래스의 인스턴스에 접근할 필요가 없다면 무조건 `static` 멤버 클래스로 사용

- 메서드 밖에서도 사용되거나 길다면 **멤버 클래스**로 만든다.
- 바깥 클래스의 인스턴스를 참조하면 **비정적**, 참조하지 않으면 **정적**으로 만든다.
- 한 메서드 안에서만 쓰이고, 생성 지점이 단 한 곳이라면 **익명 클래스**.
- 익명 클래스로 적합하지 않다면 **지역 클래스**로 만든다.

---

## 결론

> 중첩 클래스를 사용할 때는 쓰임새를 명확히 구분하고, **바깥 클래스의 인스턴스를 참조할 필요가 없는 경우 반드시 `static`을 붙여 정적 멤버 클래스로 사용**하라.
>
10 changes: 10 additions & 0 deletions 전두이/4장/item25.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# 톱레벨 클래스는 한 파일에 하나만 담으라

**소스 파일 하나에 톱레벨 클래스(또는 인터페이스)를 하나만 담아야 한다.**

이 규칙을 지키면 다음과 같은 문제가 방지된다:

1. **컴파일러가 클래스 정의를 여러 개 생성**하는 문제
2. **컴파일 순서에 따라 프로그램 동작이 달라지는 문제**

이 규칙을 따르면 프로그램의 동작이 일관되며, 유지보수성과 안정성이 높아진다.
29 changes: 29 additions & 0 deletions 전두이/5장/item26.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 로 타입은 사용하지 말라

## 로 타입이란

- **로 타입(raw type)**은 **제네릭 타입의 타입 매개변수를 지정하지 않은 상태**를 말한다.
- 예를 들어, 제네릭 타입 `Set<E>`의 **로 타입**은 그냥 `Set`이다.
- 로 타입은 **제네릭이 도입되기 이전 코드와의 호환성**을 위해 제공되었지만, 사용하면 여러 문제가 발생할 수 있다.

### **왜 로 타입을 사용하면 안 될까?**

1. **타입 안전성이 없다**
- `Set<Object>`나 `Set<?>`은 타입 시스템의 보호를 받지만, 로 타입인 `Set`은 그렇지 않음
- 잘못된 타입의 객체를 저장하거나 가져올 때 **런타임 오류**가 발생할 수 있다.
2. **제네릭의 이점을 잃는다**
- 타입 검사를 컴파일 시점에 하지 않으므로 코드의 안정성과 가독성이 떨어진다.


### **대안**

- `Set<Object>`: 어떤 타입의 객체도 저장할 수 있는 **매개변수화 타입**
- `Set<?>`: 특정 타입이지만 **구체적으로 알 수 없는 타입**만 저장할 수 있는 **와일드카드 타입**

---

## 결론

- **로 타입은 사용하지 말아야 한다**.
- 제네릭을 사용하여 **컴파일 시점에 타입 검사를 받고** 런타임 오류를 방지하자.
- 로 타입은 과거 코드와의 호환성을 위해 존재할 뿐, 현대 코드에서는 사용하지 않는 것이 좋다.
47 changes: 47 additions & 0 deletions 전두이/5장/item27.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 비검사 경고를 제거하라

## 비검사 경고(Unchecked Warning)란

- **비검사 경고**는 Java에서 **제네릭 사용 시 타입 안전성(type safety)이 보장되지 않을 때** 발생하는 컴파일 경고이다.
- 컴파일러가 "이 코드가 올바르게 작동할지 확신할 수 없다"고 판단할 때 경고를 표시한다.
- **런타임에 ClassCastException이 발생할 가능성**이 있는 코드를 경고하는 것이다.

### **왜 비검사 경고를 제거해야 할까?**

1. **타입 안전성을 위반할 가능성**
- 비검사 경고가 있는 코드는 런타임에 **ClassCastException**을 발생시킬 위험이 있다.
2. **코드의 신뢰성과 유지보수성 저하**
- 경고를 무시하면 코드가 제대로 작동하지 않을 가능성이 높아지고, 추후 문제를 찾기가 어려워진다.

---

### **비검사 경고를 없애는 방법**

1. **경고를 완전히 제거하기**
- 제네릭 타입을 제대로 사용하여 **타입 안전성을 보장한다.**
- 예:

```java
List rawList = new ArrayList(); // 비검사 경고 발생
List<String> safeList = new ArrayList<>(); // 타입 안전
```

2. **경고를 제거할 수 없는 경우**
- 해당 코드가 타입 안전함을 **직접 증명한다.**
- 경고를 숨기기 위해 `@SuppressWarnings("unchecked")` 애너테이션을 사용한다.
- 반드시 코드에 **주석을 남겨 근거를 기록하자**
- 예:

```java
@SuppressWarnings("unchecked") // 이 캐스트는 타입 안전성을 검증했음
List<String> list = (List<String>) someMethod();
```


---

## 결론

- 비검사 경고는 **무시하지 말고 제거하라**
- **타입 안전성을 보장**할 수 있도록 코드를 수정하거나 제네릭을 올바르게 사용하라
- 경고를 없앨 방법이 없을 경우, **안전성을 증명한 후** `@SuppressWarnings("unchecked")`를 사용해 숨기고, **주석으로 근거를 남겨라**
133 changes: 133 additions & 0 deletions 전두이/5장/item28.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# 배열보다는 리스트를 사용하라

배열과 제네릭 타입의 차이점 2가지

## 1. 공변(Covariant) / 불공변(Invariant)

공변성(Covariance)이란

**타입의 상속 관계에서 상위 타입 변수에 하위 타입 객체를 할당할 수 있는 특성을 뜻한다.**

→ 즉, 자식 클래스의 객체는 부모 클래스의 타입 변수에 할당될 수 있을 때, 공변성을 가진다고 말한다.

### 배열은 공변적이다

```java
Object[] objectArray = new Long[1]; // 정상
objectArray[0] = "String"; // 런타임 오류 (ArrayStoreException)
```

상위 타입 Object[] 배열에 하위 클래스인 Long[] 배열을 할당할 수 있다.

하지만, objectArray에 문자열 데이터를 넣으려고 하면 `ArrayStoreException`이 **런타임 시점에 발생**한다.

### 제네릭은 불공변이다 (Invariant)

제네릭 타입은 부모 타입과 자식 타입을 바꿀 수 없다는 의미이다.

```java
List<Object> ol = new ArrayList<Long>(); // 호환되지 않는 타입이다.
ol.add("String"); // 컴파일 오류!
```

List<Object>와 List<Long>은 호환되지 않기 때문에, **컴파일 시점에 타입 오류가 발생**한다.

### 런타임 에러와 컴파일 에러

- `런타임 에러`: 프로그램 실행 시점에 발생 (코드의 논리적 오류)

예시: null 참조, 배열의 인덱스를 잘못 참고, 잘못된 타입의 객체를 처리하려고 할 때

- `컴파일 에러`: 소스 코드가 컴파일될 때 발생

예시: 문법 오류, 변수나 메서드의 호출의 오류, 컴파일러가 코드를 분석하면서 발견할 수 있는 오류들


컴파일 오류는 개발 시점에 미리 발견할 수 있어서 빠르게 수정할 수 있지만, 런타임 오류는 실행 중에 발생하므로 더 큰 문제를 일으킬 수 있다.

컴파일 시 오류를 줄이는 것이 더 안전하고 효율적인 개발을 가능하게 한다.

---

## 2. 실체화(reify) / 소거(erasure)

### 실체화(Reification)

- 타입 정보가 런타임까지 유지되는 것을 의미한다.

배열은 실체화된 타입이다. 즉, 배열은 런타임에서 타입 정보를 유지한다. 예를 들어

```java
String[] strings = new String[10];
Object[] objects = strings;
objects[0] = 1; // 런타임 오류 발생
```

위 코드에서 `objects`는 `Object[]`로 생성되었지만, 실제 배열 타입은 `String[]` 이다. 이로 인해 런타임 검사에서 정수 타입 1을 저장하려고 할 때 `ArrayStoreException`이 발생한다.

이처럼 배열은 타입이 일치하지 않을 경우 런타임 에러를 발생시킨다.

### 타입 소거(Type erasure)

- 제네릭 타입의 정보가 컴파일 시점에서 제거되어 런타임 시점에서는 `원시 타입(raw type)`으로 처리되는 Java의 제네릭 처리 방식이다.
- 제네릭을 도입하기 전에, Java는 원시 타입만을 사용했으므로, 제네릭을 사용하더라도 이전 코드와의 호환성을 유지하기 위해 필요하다.

제네릭은 컴파일 시점에만 타입 검사를 수행하고, 런타임에는 타입 정보가 존재하지 않는다.

```java
// 컴파일 시에는 타입이 다르지만, 런타임에서는 둘 다 List로 취급된다.
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
```

위에서 `List<String>`은 컴파일 시점에는 `List<String>`으로 다루어지지만, 런타임에서는 그냥 `List`로 취급된다.

그래서 `List<String>` 와 `List<Integer>`는 컴파일 시점에는 서로 다른 타입이지만, 런타임에서는 모두 `List`로 처리된다.

### 제네릭 배열을 만들 수 없는 이유

이와 같이 제네릭은 타입 소거가 적용되기 때문에 런타임 시점에는 타입을 구별할 수 없다.

반면, 배열은 런타입 시점에 타입 정보를 검사하므로 만약 제네릭 배열을 허용하게 되면 배열의 런타임 타입 검사와 제네릭의 타입 소거가 충돌하여 타입 안전성이 깨질 수 있기 때문에, 컴파일 시점에 에러가 발생한다.

원본 코드(제네릭 타입 배열 생성 문제) → 컴파일 에러

```java
public class Chooser<T> {
private final T[] choiceArray;

public Chooser(Collection<T> choices) {
choiceArray = choices.toArray(); // 컴파일 에러
}
}
```

- `choices.toArray()` 의 반환 타입은 Object[] 이다. 따라서 T[]에 직접 할당하려고 할 때 타입 불일치 오류가 발생한다.

수정된 코드(배열을 List<T>로 변경) → 타입 안전성 확보

```java
public class Chooser<T> {
private final List<T> choiceList;

public Chooser(Collection<T> choices) {
choiceList = new ArrayList<>(choices);
}

public T choose() {
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.nextInt(choiceList.size()));
}
}
```

- 배열 대신 List<T>를 사용하면 제네릭 타입의 요소들을 유연하게 저장할 수 있게 된다.
- `new ArrayList<>(choices)`를 사용하면 `Collection`의 `choices`를 타입 안전하게 리스트로 복사할 수 있다.

---

## 결론

- 배열은 공변이고 실체화 되는 반면, 제네릭은 불공변이고 타입 정보가 소거된다.
- 이로 인해 배열은 타입 안전성을 보장해줄 수 없어 제네릭 배열을 생성할 수 없다.
- 만약 이 둘을 섞어 쓰다가 오류를 만나면, 가장 먼저 배열을 리스트로 대체하는 방법을 적용해보자.
15 changes: 15 additions & 0 deletions 전두이/5장/item29.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# 이왕이면 제네릭 타입으로 만들라

## 왜 제네릭 타입을 사용하는 것이 좋은가?

- **타입 안전성 보장:** 제네릭을 사용하면 **컴파일 시점**에 타입을 확인하므로 **런타임 오류**를 방지할 수 있다.
- **코드 가독성과 유지보수성 향상:** 제네릭을 사용하면 **형변환이 필요 없으므로** 코드가 간결하고 읽기 쉬워진다.
- **재사용성 증가:** 제네릭 타입은 **다양한 타입**에 대해 하나의 코드로 처리할 수 있어 **재사용성**이 높아진다.

---

## 결론

- **제네릭 타입**은 형변환이 필요 없어서 더 안전하고 사용하기 쉽다.
- **새로운 타입 설계 시** 형변환 없이 사용할 수 있도록 **제네릭 타입을 적극 활용**하라
- 기존 타입도 제네릭이 더 적합하다면 **제네릭 타입으로 변경**해 클라이언트 코드를 편리하게 만들어라
46 changes: 46 additions & 0 deletions 전두이/5장/item30.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# 이왕이면 제네릭 메서드로 만들라

### **왜 제네릭 메서드를 사용하는 것이 좋은가?**

- **타입 안정성 보장:** 입력과 출력 타입을 컴파일 시점에 명확히 지정하므로 **런타임 오류**를 방지할 수 있다.
- **형변환 필요 없음:** 제네릭 메서드는 **자동으로 타입이 지정되므로** 클라이언트가 `형변환(casting)`을 하지 않아도 된다.
- **가독성과 유지보수성 향상:** 코드에서 타입이 명확히 드러나므로 **가독성이 좋고**, 잘못된 타입 사용을 컴파일 시점에 잡아준다.

### 제네릭 활용 예시

**두 집합의 합집합을 반환하는 메서드**

```java
public static Set union(Set s1, Set s2) {
Set result = new HashSet<>();
result.addAll(s2);
return result;
}
```

**제네릭 메서드로 변경한 코드**

```java
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}

public static void main(String[] args) {
Set<String> guys = Set.of("톰", "딕", "해리");
Set<String> stooges = Set.of("래리", "모에", "컬리");
Set<String> aflCio = union(guys, stooges);
System.out.println(aflCio);
}
```

- 기존 union 메서드에서 제네릭으로 변경함으로써 이는 타입 안전하고 직접 형변환 하지 않아도 경고 없이 컴파일 된다.

---

## 결론

- 제네릭 타입처럼, 제네릭 메서드도 형변환 없이 사용할 수 있어 더 안전하고 편리하다.
- 새로운 메서드를 설계할 때는 형변환 없이 사용할 수 있도록 제네릭 메서드로 구현하라.
- 기존 메서드도 제네릭하게 개선하면, 기존 코드는 유지하면서 새로운 사용자를 더 편리하게 지원 가능하다.
Loading