Skip to content

Commit

Permalink
[7회차] 아이템 51, 52 - 초롱(정회성) (#68)
Browse files Browse the repository at this point in the history
* 아이템 51 : 메서드 시그니처를 신중히 설계하라

* 아이템 52 : 다중 정의는 신중히 사용하라

* 아이템 52 : 다중 정의는 신중히 사용하라
  • Loading branch information
HoeSeong123 authored Jun 6, 2024
1 parent 31820f7 commit a79059b
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# 메서드 시그니처란?
- 자바에서 프로그래머가 디자인한 메서드 구조
- 메서드 이름과 파라미터 리스트로 구성됨

```java
// 모두 다른 메서드 시그니처
public void method() {}
public void method2() {}
public void method(int a) {}
public void method(int a, int b) {}
public void method(boolean a) {}

// 같은 메서드 시그니처
public void method() {}
public int method() {}
public String method() {}
```
# 메서드를 설계할 때 주의사항
## 메서드 이름을 신중히 짓자
- 표준 명명 규칙을 따른다.
- 이해할 수 있게 한다.
- 같은 패키지에 속한 다른 이름들과 일관되게 짓는다.
- 긴 이름은 피한다.
- 애매하면 자바 라이브러리의 API 가이드를 참조한다.

## 편의 메서드를 많이 만들지 말자.
- 편의 메서드란 말 그대로 편의를 위해 존재하는 메서드이다.
- 예를 들면, 헬퍼 클래스인 Collections 안에 있는 모든 메서드를 말한다. (min, max, swap 등등..)
- 메서드가 너무 많은 클래스 또는 인터페이스는 익히고, 사용하고, 문서화하고, 테스트하고, 유지보수하기 어렵다.

## 매개변수 목록은 짧게 유지하자.
- 4개를 넘어가면 매개변수를 기억하기 쉽지 않으므로 4개 이하가 좋다.
- 같은 타입의 매개변수가 여러 개 연달아 나오는 경우는 특히 해롭다.
- 순서를 바꿔 입력해도 그대로 컴파일되고 실행되기 때문이다.
- 매개변수가 길면 아래의 방법으로 줄일 수 있다.

### 여러 메서드로 쪼개기
- 예를 들어 지정된 범위의 부분 리스트에서 인덱스를 찾는 경우를 살펴보자.
- 부분 리스트의 시작, 부분 리스트의 끝, 찾을 원소 이렇게 총 3개의 인자를 넘겨야 한다.
- `subList` 메서드를 사용하여 시작, 끝을 입력하고 `indexOf` 메서드로 찾을 원소를 입력받게 쪼갤 수 있다.

### 도우미 클래스 만들기
- 매개변수 여러 개를 묶어주는 도우미 클래스를 만들어서 매개변수 수를 줄일 수 있다.
- `Card`를 예로 들면, 카드의 숫자와 무늬를 하나로 묶어(래핑하여) 하나의 매개변수로 주고받을 수 있다.

### 빌더 패턴
- 이 방법은 매개변수 중 일부는 생략해도 괜찮을 때 도움이 된다.
- 모든 매개변수를 하나로 추상화한 객체를 정의하고,
- 세터를 이용하여 값을 설정한 다음,
- execute 메서드를 호출하여 유효성 검사를 한 뒤,
- 객체를 넘겨 계산을 수행한다.

## 매개변수의 타입으로는 클래스보다는 인터페이스가 낫다.
- 매개변수 타입으로 인터페이스 타입을 이용하면, 하위타입을 모두 호환할 수 있으므로 클래스보다 유연하게 사용할 수 있다.
- 예를 들어 메서드에 HashMap을 넘기지 말고 Map을 사용하자.
- 그러면 HashMap 뿐 아니라 TreeMap, ConcurrentHashMap 등 어떤 Map 구현체도 인수로 건넬 수 있다.

## boolean보다는 원소 2개짜리 열거 타입이 낫다.
- 열거타입을 사용하면 코드를 읽고 쓰기가 더 쉬워지고, 나중에 선택지를 추가하기도 쉽다.
- 메서드의 이름상 boolean이 더 명확한 상황이 아니라면 열거타입을 고려하자.
Binary file added 08장/아이템_52/img.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added 08장/아이템_52/img_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added 08장/아이템_52/img_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
168 changes: 168 additions & 0 deletions 08장/아이템_52/다중_정의는_신중히_사용하라.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# 다중정의(오버로딩)는 신중히 사용하라.
- 아래의 코드를 보고 무엇을 출력할 지 예상해보자.
```java
class Wine {
String name() {
return "포도주";
}
}

class SparklingWine extends Wine {
@Override
String name() {
return "발포성 포도주";
}
}

class Champagne extends SparklingWine {
@Override
String name() {
return "샴페인";
}
}

public class Overriding {
public static void main(String[] args) {
List<Wine> wineList = List.of(
new Wine(), new SparklingWine(), new Champagne());

for (Wine wine : wineList)
System.out.println(wine.name());
}
}
```
- `"포도주", "발포성 포도주", "샴페인"`


- 이 코드는 어떨까?
```java
public class CollectionClassifier {
public static String classify(Set<?> s) {
return "집합";
}

public static String classify(List<?> lst) {
return "리스트";
}

public static String classify(Collection<?> c) {
return "그 외";
}

public static void main(String[] args) {
Collection<?>[] collections = {
new HashSet<String>(),
new ArrayList<BigInteger>(),
new HashMap<String, String>().values()
};

for (Collection<?> c : collections)
System.out.println(classify(c));
}
}
```

- `"집합", "리스트", "그 외"`를 차례로 출력할 것 같지만, 실제로는 `"그 외, "그 외", "그 외"`가 출력된다.

## 메서드 재정의(오버라이딩)과 다중정의(오버로딩)
### 재정의(오버라이딩)
- 해당 객체의 런타임 타입이 어떤 메서드를 호출할지의 기준이 된다.
- 메서드를 재정의한 다음 '하위 클래스의 인스턴스'에서 그 메서드를 호출하면 재정의한 메서드가 실행된다.
- 컴파일 타임에 그 인스턴스의 타입이 무엇이었냐는 상관없다.

### 다중정의(오버로딩)
- 어느 메서드를 호출할지가 컴파일타임에 정해진다.
- 컴파일타임에는 for문 안의 c는 항상 Collection<?> 타입이다.
- 런타임에는 타입이 매번 달라지지만, 호출할 메서드를 선택하는 데는 영향을 주지 못하낟.
- 따라서 컴파일타임의 매개변수 타입을 기준으로 항상 세 번째 메서드인 `classify(Collection<?>)`만 호출하게 된다.
- 위 문제를 해결하려면 다음과 같이 모든 `classify` 메서드를 하나로 합친 후 `instanceof`로 명시적으로 검사하면 된다.
```java
public static String classify(Collection<?> c) {
if(c instanceof Set) {
return "집합";
}
if(c instanceof List) {
return "리스트";
}
return "그 외";
}
```
- 매개변수가 같은 다중정의는 만들지 말자.
- 가장 간단한 방법은 메서드 이름을 다르게 지어주는 것이다.

## 좋은 예시
- `ObjectOutputStream` 클래스를 살펴보면 모든 `write` 메서드에 다른 이름을 지어주었다.
![img.png](img.png)
- 이 방식의 장점은 `read` 메서드의 이름과 짝을 맞추기 좋다는 것이다.
- 실제로 `ObjectInputStream` 클래스의 `read` 메서드는 다음과 같이 되어 있다.
![img_1.png](img_1.png)

## 생성자
- 생성자는 이름을 다르게 지을 수 없어 무조건 다중정의가 된다.
- 이 경우는 매개변수 중 하나 이상이 "근본적으로 다르다"면 된다.
- 근본적으로 다르다는 것은 두 타입의 값이 서로 어느 쪽으로든 형변환할 수 없다는 뜻이다.
- 이 조건만 충족하면 어느 다중정의 메서드를 호출할지가 매개변수들의 런타임 타입만으로 결정된다.
```java
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: " +
initialCapacity);
}
}

public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
}
```

## 다중정의가 위험한 또다른 이유
### 제네릭과 오토박싱
```java
public class SetList {
public static void main(String[] args) {
Set<Integer> set = new TreeSet<>();
List<Integer> list = new ArrayList<>();

for (int i = -3; i < 3; i++) {
set.add(i);
list.add(i);
}

for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove(i);
}

System.out.println(set);
System.out.println(list);
}
}
```
![img_2.png](img_2.png)

- `set.remove(i)`의 시그니처는 `remove(Object)`이다.
- `list.remove(i)`는 다중정의된 `remove(int index)`를 선택한다.
-`remove`는 '지정한 위치'의 원소를 제거한다.
- 이 문제를 해결하려면 `list.remove`의 인수를 `Integer`로 형변환하면 된다.
- 이러한 문제는 제네릭과 오토박싱이 등장하면서 두 메서드의 매개변수 타입이 근본적으로 다르지 않게 됐기 때문에 발생한 문제이다.

## 결론
- 일반적으로 매개변수 수가 같을 때는 다중정의를 피하라.
- 피할 수 없는 경우 형변환을 통해 정확한 다중정의 메서드가 선택되도록 한다.

0 comments on commit a79059b

Please sign in to comment.