Skip to content
Open
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
85 changes: 85 additions & 0 deletions peng255/src/main/java/item10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
## 10. equals는 일반 규약을 지켜 재정의하라
### **언제 equals를 재정의해야 하는가?**

- 인스턴스마다 고유한 객체(예: Thread 등)는 equals를 재정의하지 않는다.
- 논리적 동등성(값이 같으면 같다고 판단)이 필요한 값 객체(예: Integer, String 등)는 equals를 재정의해야 한다.
- 상위 클래스가 이미 equals를 적절히 재정의했다면 굳이 다시 재정의할 필요 없다.
- private 또는 package-private 클래스이고 equals가 호출될 일이 없다면 재정의하지 않는다.

- equals를 재정의할 땐 5가지 규약(반사성, 대칭성, 추이성, 일관성, null-불일치)을 반드시 지켜야 한다.
- 잘못 구현하면 컬렉션 등에서 예측 불가능한 버그가 발생한다.
- 값 객체라면 equals와 hashCode를 함께 재정의한다.

### **equals 일반 규약**

equals 메서드를 재정의할 때 반드시 다음 5가지 규약을 지켜야 한다

| **규약** | **설명** |
| --- | --- |
| 반사성 | x.equals(x)는 항상 true여야 한다. |
| 대칭성 | x.equals(y)가 true면 y.equals(x)도 true여야 한다. |
| 추이성 | x.equals(y)와 y.equals(z)가 true면 x.equals(z)도 true여야 한다. |
| 일관성 | x.equals(y)는 객체 상태가 변하지 않는 한 여러 번 호출해도 항상 같은 결과를 반환해야 한다. |
| null-불일치 | x.equals(null)은 항상 false여야 한다. |

### **잘못된 예시**

**대칭성 위반 예시**

```java
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) { this.s = s; }

@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
if (o instanceof String) // String과의 비교도 true로 처리
return s.equalsIgnoreCase((String) o);
return false;
}
}
```

위 코드에서`cis.equals("abc")`는 true지만,`"abc".equals(cis)`는 false가 되어 대칭성을 위반한다.

**추이성 위반 예시**

```java
public class Point {
private final int x, y;
// equals: x, y만 비교
}

public class ColorPoint extends Point {
private final Color color;
// equals: x, y, color까지 비교
}
```

Point와 ColorPoint를 섞어서 비교하면 추이성이 깨질 수 있다.

### **올바른 equals 구현 예시**

```java
@Override
public boolean equals(Object o) {
if (this == o) return true; // 반사성
if (!(o instanceof Book)) return false; // 타입 체크 및 null-불일치
Book other = (Book) o;
return this.name.equals(other.name)
&& this.published == other.published
&& this.content.equals(other.content); // 중요한 필드 비교
}
```

- 반드시 Object 타입을 파라미터로 받아야 한다.
- instanceof로 타입 체크를 하고, 필드별 값 비교를 한다.
- null 체크는 instanceof가 대신 처리한다.

### **equals 재정의 시 주의점**

- equals를 재정의하면 반드시 hashCode도 재정의한다.
- 필드 비교는 논리적으로 중요한 필드만 대상으로 한다.
- 성능상, 먼저 다를 확률이 높은 필드부터 비교하는 것이 좋다.
73 changes: 73 additions & 0 deletions peng255/src/main/java/item11.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
## 11. equals를 재정의하려거든 hashCode도 재정의하라
### **왜 hashCode를 재정의해야 하는가?**

- equals만 재정의하고 hashCode를 재정의하지 않으면, HashMap, HashSet 등 컬렉션에서 객체가 제대로 동작하지 않는다.
- hashCode 규약은 다음과 같다:
- 같은 객체에 대해 hashCode를 여러 번 호출해도 값이 변하지 않아야 한다(equals에 사용되는 정보가 변하지 않는 한).
- equals로 비교해 같다고 판단되는 두 객체는 반드시 같은 hashCode 값을 가져야 한다.
- equals로 다르다고 판단되는 두 객체는 hashCode가 다를 수도 같을 수도 있지만, 다르면 성능이 좋아진다.

### **잘못된 예시**

```java
@Override public int hashCode() { return 42; }
```

- 모든 객체가 같은 hashCode를 반환하므로, HashMap/HashSet이 내부적으로 리스트처럼 동작해서 성능이 크게 저하된다.

### **좋은 hashCode 구현법**

1. 의미 있는 필드(=equals 비교에 쓰는 필드)들의 값을 이용해 hashCode를 만든다.
2. 각 필드의 hashCode 값을 31로 곱해가며 누적한다.
3. 최종적으로 result를 반환한다.

### **예시**

```java
@Override
public int hashCode() {
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
```

- 의미 있는 필드가 여러 개라면, 순서대로 31을 곱해가며 합산한다.

### **간단한 방법 (성능이 중요하지 않을 때)**

```java
@Override
public int hashCode() {
return Objects.hash(lineNum, prefix, areaCode);
}
```

- 이 방식은 코드가 짧지만, 내부적으로 배열 생성 등 오버헤드가 있어 성능이 아주 중요할 때는 권장하지 않는다.

### **캐싱 기법**

- 불변 객체에서 hashCode 계산 비용이 크면, hashCode를 필드로 저장해두고 처음 한 번만 계산하는 방법도 있다.

```java
private int hashCode; // 기본값 0

@Override
public int hashCode() {
int result = hashCode;
if (result == 0) {
result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
hashCode = result;
}
return result;
}
```

### **주의점**

- equals에 사용하지 않는 필드는 hashCode 계산에 포함시키지 않는다.
- 성능을 위해 중요한 필드를 빼면 해시 품질이 떨어져서 HashMap/HashSet의 성능이 나빠질 수 있다.
- hashCode 구현 결과값의 구체적인 스펙을 문서로 명시하지 않는다(유연성 확보)
61 changes: 61 additions & 0 deletions peng255/src/main/java/item12.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
## 12. toString을 항상 재정의하라
- Object의 기본 toString은 클래스명@해시코드(16진수) 형태로, 사람이 보기엔 거의 쓸모가 없다.
- toString의 일반 규약은 “**간결하면서도 사람이 읽기 쉬운, 유용한 정보**”를 반환해야 한다.
- toString을 재정의하면 디버깅, 로그, 출력, 에러 메시지 등에서 객체 정보를 쉽게 확인할 수 있다.
- 컬렉션이나 Map에 객체가 들어갈 때도 toString이 잘 구현되어 있으면 전체 구조를 한눈에 파악하기 쉽다.
- toString 구현 시, 객체의 중요한 정보를 모두 포함하는 것이 좋다.

단, 객체가 너무 크거나 복잡하면 요약 정보를 반환한다.


### **예시: PhoneNumber 클래스**

```java
@Override
public String toString() {
return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum);
}
```

- 010-123-4567처럼 사람이 직관적으로 이해할 수 있는 형태로 반환한다.

### **포맷 명시의 장단점**

- **포맷을 명시하면:**
- 표준화된 문자열 표현을 만들 수 있다.
- CSV, 로그, 파일 저장 등에 활용하기 좋다.
- 반대로, 포맷을 바꾸면 기존 사용자 코드가 깨질 수 있으니 신중해야 한다.
- **포맷을 명시하지 않으면:**
- 향후 포맷 변경이 자유롭다.
- 대신, 외부에서 toString 결과를 파싱해서 쓰면 안 된다.

### **포맷 명시 예시 (JavaDoc)**

```java
// "XXX-YYY-ZZZZ" 형태의 전화번호 문자열을 반환한다.
// 각 부분이 자릿수보다 작으면 앞에 0을 붙인다.
@Override
public String toString() {
return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum);
}
```

### **포맷 미명시 예시 (JavaDoc)**

```java
// 이 포션의 간단한 설명을 반환한다.
// 구체적 포맷은 바뀔 수 있다.
// 예: "[Potion #9: type=love, smell=turpentine, look=india ink]"
@Override
public String toString() { ... }
```

### **추가 팁**

- toString이 반환하는 정보는 반드시 getter 등으로도 접근할 수 있게 만든다.

그렇지 않으면, 외부에서 toString 결과를 파싱해서 쓰는 불안정한 코드가 생길 수 있다.

- static 유틸리티 클래스나 대부분의 enum 타입에는 toString을 따로 구현하지 않는다.
- IDE 자동 생성 toString도 쓸 수 있지만, 의미 있는 값 객체라면 직접 포맷을 정의하는 것이 더 좋다.
- toString을 재정의하지 않으면, 로그나 에러 메시지에서 객체 정보를 알 수 없어 디버깅이 불편해진다.
112 changes: 112 additions & 0 deletions peng255/src/main/java/item13.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
## 13. clone 재정의는 주의해서 진행하라
### **Cloneable 인터페이스란**

- Cloneable은 "이 객체는 복제(clone)할 수 있다"는 의도를 나타내는 마커 인터페이스다.
- 메서드는 없고, 단지 Object의 protected clone() 메서드가 예외를 던지지 않게 해준다
- Cloneable을 구현하지 않으면 clone() 호출 시 CloneNotSupportedException이 발생한다.

### **clone 메서드의 동작**

- Cloneable을 구현한 객체에서 Object의 clone()을 호출하면 **필드 단위로 복사한 새 객체**가 만들어진다(기본적으로 shallow copy)
- 하지만, Cloneable 인터페이스 자체에는 clone 메서드가 없으므로 **직접 public clone()을 오버라이드**해야 한다

### **clone 메서드의 일반 규약**

- x.clone() != x (복제 객체는 원본과 다르다)
- x.clone().getClass() == x.getClass() (보통 복제 객체의 타입은 원본과 같다)
- x.clone().equals(x) == true (보통 값이 같지만, 필수는 아니다)
- 복제본은 원본과 독립적이어야 한다. 즉, 내부 상태가 서로 영향을 주지 않아야 한다

### **shallow copy vs deep copy**

| **Shallow Copy** | **Deep Copy** |
| --- | --- |
| 필드 값만 복사, 참조 타입은 같은 객체를 가리킴 | 모든 필드와 참조 객체까지 재귀적으로 복사 |
| 내부에 가변 객체가 있으면 원본/복제본이 서로 영향을 준다 | 완전히 독립적인 객체가 된다 |
| 빠르고 메모리 효율적 | 느리지만 안전하게 분리됨 |
| 불변 객체, 간단한 구조에 적합 | 복잡하거나 가변 객체가 있을 때 필요 |
- clone()의 기본 동작은 shallow copy다.
- 내부에 가변 객체(예: 배열, 컬렉션 등)가 있으면, clone() 오버라이드 시 deep copy를 직접 구현해야 한다

### **잘못된 예시: shallow copy만 하는 경우**

```java
@Override
public Stack clone() {
try {
return (Stack) super.clone(); // elements 배열이 공유됨!
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
```

- 이 경우 복제본과 원본이 같은 배열을 공유해서, 한쪽을 변경하면 다른 쪽에도 영향이 간다.

### **올바른 예시: deep copy 구현**

```java
@Override
public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone(); // 배열 복제
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
```

- elements 배열을 새로 복사해서 원본과 복제본이 완전히 분리된다

### **복잡한 구조(예: 연결리스트)의 deep copy**

```java
private static class Entry {
final Object key;
Object value;
Entry next;

Entry deepCopy() {
return new Entry(key, value, next == null ? null : next.deepCopy());
}
}

@Override
public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = new Entry[buckets.length];
for (int i = 0; i < buckets.length; i++)
if (buckets[i] != null)
result.buckets[i] = buckets[i].deepCopy();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
```

- 각 버킷의 연결리스트까지 재귀적으로 복제한다.

### **Cloneable/clone의 문제점**

- clone은 생성자를 호출하지 않으므로, 객체 불변성/초기화 보장이 어렵다.
- final 필드와 호환이 안 좋다.
- 복잡한 객체에서는 deep copy를 직접 구현해야 해서 실수하기 쉽다.
- 인터페이스임에도 메서드가 없어, 일관된 사용이 어렵다.
- checked exception(throws CloneNotSupportedException) 처리도 번거롭다

### **대안: 복사 생성자/복사 팩토리**

```java
// 복사 생성자
public Stack(Stack original) { ... }

// 복사 팩토리
public static Stack newInstance(Stack original) { ... }
```

- 명확하고, final 필드, 불변성, 상속 등과의 호환성이 좋다.
- 복사 과정이 명시적이라 실수할 여지가 적다
Loading