Skip to content

Commit

Permalink
[6회차] 아이템 39, 40 - 폰드(권규택) (#62)
Browse files Browse the repository at this point in the history
* 아이템6: 불필요한 객체 생성을 피하라

* 아이템3: private 생성자나 열거 타입으로 싱글턴임을 보증하라

* 아이템13: clone 재정의는 주의해서 진행하라

* 아이템12: toString을 항상 재정의하라

* 아이템 21 : 인터페이스는 구현하는 쪽을 생각해 설계하라

* 아이템 22 : 인터페이스는 타입을 정의하는 용도로만 사용하라

* 아이템 26 : 로 타입은 사용하지 말라

* 아이템 33 : 타입 안전 이종 컨테이너를 고려하라

* 아이템 39 : 명명 패턴보다 애너테이션을 사용하라

* 아이템 40 : @OverRide 애너테이션을 일관되게 사용하라
  • Loading branch information
tackyu committed May 24, 2024
1 parent b0521f7 commit bb7e161
Show file tree
Hide file tree
Showing 6 changed files with 653 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
## 싱글턴
*인스턴스를 오직 하나만 생성할 수 있는 클래스*
- 무상태 객체(stateless)
- 설계상 유일해야 하는 시스템 컴포넌트

>클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다.
> > 타입을 인터페이스로 정의한 다음 그 인터페이스를 구현해서 만든 싱글턴이 아니라면, 가짜 구현으로 대체할 수 없기 때문.
### private 생성자 사용
private을 통해 생성자를 숨김.
인스턴스에 접근할 유일한 수단: public static 멤버
#### public static 멤버가 final 필드인 방식
```java
public class Coffee {
public static final Coffee INSTANCE = new Coffee();

private Coffee() {
if (INSTANCE != null) throw new IllegalStateException(); //리플렉션 API를 통한 생성자 호출 방지
}
}
```
생성자는 Coffee.INSTANCE를 초기화 할 때 한 번만 호출된다.
인스턴스가 하나뿐임이 보장.
- 해당 클래스가 싱글턴임이 명백히 드러남.
- 간결하다.
#### public static 멤버가 정적 팩터리 메서드인 방식
```java
public class Coffee {
private static final Coffee INSTANCE = new Coffee();

private Coffee() {
}

public static Coffee getInstance() {
if (INSTANCE == null) {//리플렉션 API를 통한 생성자 호출 방지
return new Coffee();
}
return INSTANCE;
}
}
```
항상 같은 객체의 참조를 반환하므로 인스턴스가 하나뿐임이 보장.
- API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있음.
- 제네릭 싱글턴 팩토리로 만들 수 있다.
- 정적 팩터리의 메소드 참조를 supplier로 사용할 수 있다.
- ```java
Supplier<Coffee> supplier=Coffee::getInstance;
```
위 장점들이 굳이 필요하지 않다면 public static 멤버가 final 필드인 방식이 낫다.
> 예외
> AccessibleObject.setAccessible() 사용해서 private 생성자에 접근하는 경우
>> 두 번째 인스턴스가 생성되지 않도록 방어 가능

**역직렬화 문제**
- 직렬화: 자바에서 사용되는 Object 또는 Data를 다른 컴퓨터의 자바 시스템에서도 사용할 수 있도록 바이트 스트림 형태의 연속적인 데이터로 변환하는 포맷 변환 기술
- 역직렬화: 바이트로 변환 데이터를 원래대로 변환하는 기술

직렬화된 싱글턴 인스턴스를 송신자가 역직렬화 하면 새로운 인스턴스가 생성됨.
>모든 인스턴스 필드에 transient를 추가하고, readResolve 메서드 추가해서 해결 가능
> ```java
> private Object readResolve() {
> return INSTANCE;
> }
>```


### 원소가 하나인 열거타입 선언(바람직)
```java
public enum Coffee {
INSTANCE;
}
```
- 간결함
- 역직렬화 문제 없음.
- 리플렉션 문제 없음.
- 대부분의 상황에서 가장 좋은 방법이다.
단, 다른 클래스를 상속 받아야 한다면 이 방법은 사용 불가.
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
**불필요한 객체** 생성을 피하라
### 똑같은 기능의 객체
**똑같은 기능의 객체**를 매번 생성하기보다는 객체 하나를 재사용하는 것을 고려한다.
불변 객체는 언제든 재사용 가능

- String Literal
```java
🤮String s= new String("abc"); //Heap 영역에 새로 생성
```
```java
String s= "abc"; //String Constant Pool 에서 재사용
```
리터럴로 선언된 문자열을 사용함으로서 쓸데없는 문자열 인스턴스 생성을 방지한다.
<br><br>
- 정적 팩토리 메서드
```java
🤮Boolean b = new Boolean("false"); //자바 9에서 deprecated API 지정
```
```java
Boolean b = Boolean.valueOf("true");
//정적 팩토리 메서드
public static Boolean valueOf(String s) {
return parseBoolean(s) ? TRUE : FALSE;
}
//미리 생성된 객체
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
```
Boolean은 valueOf 메서드를 통해 미리 생성된 객체를 재활용한다.



### 생성 비용이 비싼 객체
- 캐싱
```java
static boolean isRomanNumeral(String s) {
return s.matches("정규표현식");
}//String.matches 메서드 사용의 반복이 성능에 영향을 줄 수 있다.

public boolean matches(String regex) {
return Pattern.matches(regex, this);
}

public static boolean matches(String regex, CharSequence input) {
🤮Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(input);
return m.matches();
}
```
matches 내부에서 생성되는 Pattern 인스턴스(불변)는 한 번 쓰고 버려져, GC 대상이 된다.
*Pattern은 정규표현식에 해당하는 유한 상태 머신을 만들기 때문에 인스턴스 생성 비용이 높다*
```java
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile("정규표현식");

static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}
```
정규표현식을 표현하는 Pattern 인스턴스를 정적초기화 과정에서 캐싱해두고, 나중에 문자열 형태를 확인하는 메서드가 호출 될 때마다 이 인스턴스를 재사용한다.
### 어댑터
객체 하나당 어댑터 하나씩만 만들어지면 충분하다.

```java
Map<Integer, String> map = new HashMap<>();
map.put(1,"a");
map.put(2,"b");

Set<Integer> keys1=map.keySet();
🤮Set<Integer> keys2=map.keySet();
```
keySet을 호출 할 때마다 새로운 Set 인스턴스가 만들어지는 것이 아님.
모두가 똑같은 Map 인스턴스를 대변하기 떄문에 여러개 만들 필요가 없다.

### 오토 박싱
```java
private static long sum() {
🤮Long sum = OL;
for (long i= 0; i <= Integer.MAX_VALUE; i++){
sum += i; //불필요한 Long 인스턴스 생성
}
return sum; }
```
의도치 않은 오토박싱이 들어가지 않도록 주의

### 주의사항
- 객체 생성은 비싸니 피해야한다는 의미가 아님. 요즘 JVM에서 작은 객체를 생성하고 회수하는 일은 큰 부담이 아니다.
- 아주 무거운 객체가 아니라면, 객체 생성을 피하기 위해 객체 풀을 만들지 말자.
일반적으로 코드를 헷갈리게 만들고 메모리 사용량을 늘림.
- 방어적 복사가 필요한 상황에서 객체를 재사용했을 때의 피해 >> 불필요한 객체를 반복 생성했을 때의 피해
53 changes: 53 additions & 0 deletions 03장/아이템_12/toString을_항상_재정의하라.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
**toString을 항상 재정의하라**

### toString의 규약
- 간결하면서 사람이 읽기 쉬운 형태의 유익한 정보 반환
- 모든 하위 클래스에서 이 메서드를 재정의하라

### 당위성
- toString을 잘 구현한 클래스는 **디버깅**하기 쉽다
- 객체를 println,printf,문자열 연결 연산자, assert구문에 넘길 때, 디버거가 객체를 출력할 때 자동으로 불린다.
- 직접 호출하진 않더라도 다른 어딘가에서 쓰일 테니 재정의 하라.

### 사용 방법
- 객체가 가진 주요 정보 모두를 반환하는 게 좋다.
- 객체가 너무 거대하거나, 객체의 상태가 문자열로 표현하기 적합하지 않다면 요약 정보를 담는다.

### 문서화
- 반환값의 포맷을 **문서화** 할지 정한다.
- 포맷에 맞는 문자열과 객체를 상호 전환 할 수 있는 정적 팩터리나 생성자를 함께 제공해주면 좋다.
- 포맷을 한번 명시하면, 포맷 평생 얽매이게 된다는 단점도 있다.
- 포맷을 명시하든 아니든 의도는 명확히 밝혀야 한다.
```java
포맷 명시
/**
* 이 전화번호의 문자열 표현을 반환한다.
* 이 문자열은 "XXX-YYY-ZZZZ" 형태의 12글자로 구성된다.
* XXX는 지역 코드, YYY는 프리픽스, ZZZZ는 가입자 번호다.
* 각각의 대문자는 10진수 숫자 하나를 나타낸다.
*
* 전화번호의 각 부분의 값이 너무 작아서 자릿수를 채울 수 없다면,
* 앞에서부터 0으로 채워나간다. 예컨대 가입자 번호가 123이라면
* 전화번호의 마지막 네 문자는 "0123"이 된다.
*/
@Override public String toString() {
return String.format("%03d-%03d-%04d",
areaCode, prefix, lineNum);
}

포맷 명시 x
/**
* 이 약물에 대한 대략적인 설명을 반환한다
* 다음은 이 설명의 일반적인 형태이나,
* 상세 형식은 정해지지 않았으며 향후 변경될 수 있다.
*
* "[약물 #9: 유형=사랑, 냄새=테레빈유, 겉모습=먹물]"
*/
@Override public String toString() { ... }
```
- 포맷 명시 여부와 상관없이 toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공하자.
- 제공되지 않는다면, 반환값을 파싱해서 얻어야 하므로, 성능도 나빠지고 불필요한 작업을 하게 된다.

### 기타
- 정적 유틸리티 클래스는 toString을 제공할 이유가 없다.
- 열거 타입은 이미 완벽한 toString을 제공.
148 changes: 148 additions & 0 deletions 03장/아이템_13/clone_재정의는_주의해서_진행하라.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
**clone 재정의는 주의해서 진행하라**
## clone 메서드 특징
- Cloneable 인터페이스: 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스
- Object에 선언: clone 메서드는 Cloneable 인터페이스에 선언 되어 있지 않다.
- native 코드로, 소스코드는 jvm.cpp에 위치
```java
public interface Cloneable {} //마커 인터페이스
```
```java
public class Object {
...
protected native Object clone() throws CloneNotSupportedException;// 실제 선언된 위치
...
}
```
- protected 접근 제어자
## clone 사용 방법
### 동작 방식
- Cloneable 구현 후, clone()을 호출하면, 객체의 필드들을 하나하나 복사(얕은 복사)한 객체를 반환
- Cloneable을 구현하지 않고, clone() 호출 시 CloneNotSupportedException 발생
>인터페이스를 이례적으로 사용한 방법🤮
### 사용 방법
실무에서는 clone메서드가 public으로 제공되고, 인스턴스 복제가 제대로 이뤄지길 기대함.
해당 기대를 만족하기 위한 모순적인 메카니즘(생성자 호출 없이 객체 생성)과 허술한 규약을 지켜야 함.
>x.clone() != x //참
x.clone().getClass() == x.getClass() //super.clone()을 통해 객체를 얻는다는 관례를 지킨다면 참
x.clone().equals(x) // 일반적으로 참이지만, 필수 아님.

**올바른 동작을 위한 clone 재정의**
- 접근제어자 public 변경
- super.clone()을 통한 객체 호출
- try-catch 블록으로 검사 예외(checked exception) 처리
throws절을 없애줘, 사용하기 편하게 만들어 준다.
- 공변 반환 타이핑으로 클라이언트가 형변환 하지 않게끔 구현.
- 모든 필드가 기본 타입이거나, 불변 객체 참조라면 더 수정할 것이 없다.
```java
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
```
**가변 객체를 참조한다면?**

```java
public class Stack implements Cloneable{
private Object[] elements;🤮
private int size=0;
...
}
```
- **clone의 재귀적 호출**
clone을 사용한다면, 원본 elements를 똑같이 참조하기 때문에 원본과 복제본이 서로 영향을 받음.👎

```java
@Override
public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
}catch(CloneNotSupportedException e){
throw new AssertionError();
}
}
```
스택 내부 정보를 복사→elements 배열의 clone을 재귀적으로 호출해서 해결
>**배열의 clone**
>형변환 필요 없음
런타임 타입과 컴파일 타임 타입 모두가 원본 배열과 똑같은 배열을 반환
> 배열 복제할 때는 clone 사용 권장
>
elements가 final이었다면 앞선 방식은 작동하지 않음.
Cloneable 아키텍처는 '가변 객체를 참조하는 필드는 final로 선언하라'는 일반 용법과 충돌🤮
때문에 일부 필드에서 final 한정자를 제거해야 할 수도 있다.
- **clone의 재귀적 호출 만으로 충분하지 않을 때**
```java
public class HashTable implements Cloneable {
private Entry[] buckets =...;

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

Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
}

@Override
public HashTable clone(){
try{
HashTable result = (HashTable) super.clone();
result.buckets = buckets.clone();🤮
return result;
} catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
}
```
Stack에서처럼 bucket 배열의 clone을 재귀적으로 호출하면, **원본과 같은 연결리스트를 참조하는 문제** 발생
**깊은 복사**를 지원해서 각 bucket을 구성하는 연결리스트를 복사해야 한다.
```java
@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();
}
}
```
- 재귀
- 반복자로 순회

### 주의 사항
- clone에서는 하위 클래스에서 재정의 될 수 있는 메서드를 호출하지 않아야 한다.
원본과 복제본의 상태가 달라질 가능성이 크다.(동적 바인딩)
- clone 메서드는 적절히 동기화해줘야 한다.

## 더 나은 객체 복사 방식
### 복사 생성자, 복사 팩터리
- 모순적인 객체 생성 메커니즘 x
- 엉성한 문서 규약 x
- 불필요한 검사 예외 x
- 형변환 필요 x
- 인터페이스 타입의 인스턴스를 인수로 받을 수 있다.
```java
public TreeSet(Collection<? extends E> c) {
this();
addAll(c);
}
```
Loading

0 comments on commit bb7e161

Please sign in to comment.