Item52. 다중정의는 신중히 사용하라!
다중정의(overloading)
- 메서드 명은 같지만 매개변수의 개수, 타입, 순서를 다르게 하여 선언하는 것을 말한다.
메서드 실행 시점
오버로딩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CollectionClassifier {
public static String classify(Set<?> s) {
return "Set";
}
public static String classify(List<?> lst) {
return "List";
}
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 타입이다. 따라서 컴파일타임의 매개변수 타입을 기준으로 항상 세 번째 메서드가 호출된다.
- 이처럼 직관에 어긋나는 이유는 재정의한 메서드는 동적으로 선택되고, 다중정의한 메서드는 정적으로 선택되기 때문이다.
1
2
3
4
public static String classify(Collection<?> c) {
return c instanceof Set ? "Set" :
c instanceof List ? "List" : "Unknown Colleciton";
}
- classify 메서드를 하나로 합치고, instanceof로 명시적 형검사를 통해 해결할 수 있다.
오버라이딩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Wine {
String name() { return "와인"; }
}
class SparklingWine extends Wine {
@Override String name() { return "스파클링 와인"; }
}
class Champagne extends Wine {
@Override String name() { return "샴페인"; }
}
public class Overridings {
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());
}
}
- 어느 메서드를 호출할지가
런타임
에 정해진다. - ‘와인’, ‘스파클링 와인’, ‘샴페인’이 차례대로 출력된다.
다중정의 시 주의할 점
- 다중정의가 혼동을 일으키는 상황을 피해야 한다.
되도록 매개변수 개수가 같은 다중정의는 만들지 말자.
- 가변인수(varargs)를 사용하는 메서드라면 다중정의를 아예 하지 말아야 한다.
- 이 규칙을 따른다면 어떤 다중정의 메서드가 호출될지 헷갈릴 일은 없을 것이다.
- 다중정의 대신 메서드 이름을 다르게 지어주는 길도 항상 열려있다.
매개변수 개수가 같은 다중정의를 피할 수 없는 경우
- 생성자는 이름을 다르게 지을 수 없다.
- 두 번째 생성자부터는 무조건 다중정의가 된다.
- 정적 팩터리라는 대안을 활용할 수 있는 경우가 많다.
- 여러 생성자가 같은 수의 매개변수를 받아야 하는 경우를 완전히 피해갈 수는 없다. 안전 대책을 세워야 한다.
- 매개변수 수가 같은 다중정의 메서드가 많더라도, 그중 어느 것이 주어진 매개변수 집합을 처리할지가 명확히 구분된다면 헷갈릴 일은 없을 것이다.
- 즉, 매개변수 중 하나 이상이 ‘근본적으로 다르다(radically different)’면 헷갈릴 일이 없을 것이다.
- 근본적으로 다르다는 것? 두 타입의 (null이 아닌) 값을 서로 어느 쪽으로든 형변환할 수 없다는 뜻.
매개변수가 근본적으로 달라도 안전하지 않은 경우
1
2
List<Integer> list = Arrays.asList(1, 2, 3);
list.remove(1);
List
인터페이스가 remove(int index)와 remove(Object o)를 오버로딩 했다. - remove(1)은 1번 index의 값을 지우라는 것일까? 아님 1인 값을 지우라는 것일까? 애매하다.
제네릭과 오토박싱이 등장하면서 Object와 int 타입이 근본적으로 다르지 않게 되었다.
위 예제의 경우 remove(int index)를 선택하게 된다.
만약 1을 지우고 싶다면 명시적으로 형변환하여 처리하면 된다.
1
list.remove((Integer) 1);
다중정의된 메서드(혹은 생성자)들이 함수형 인터페이스를 인수로 받을 때, 비록 서로 다른 함수형 인터페이스라도 인수 위치가 같으면 혼란이 생긴다.
오버로딩 시, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안 된다.
- 서로 다른 함수형 인터페이스라도 서로 근본적으로 다르지 않다는 것이다.
1
2
3
4
new Thread(System.out::println).start(); // 컴파일 성공
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println); // 컴파일 에러
- 넘겨진 인수는 System.out::println으로 서로 같다. 둘 다 Runnable을 받는 형제 메서드를 오버로딩 하고 있다.
- 다중정의 해소가 되지 않아 컴파일 에러가 난다.
- System.out::println는 부정확한 메서드 참조다.
핵심 정리
- 프로그래밍 언어가 다중정의를 허용한다고 해서 다중정의를 꼭 활용하란 뜻은 아니다.
- 일반적으로 매개변수 수가 같을 때는 다중정의를 피하는 게 좋다.
- 상황에 따라, 특히 생성자라면 이러한 조언을 따르기 힘들 수 있는데, 그럴 때는 헷갈릴 만한 매개변수는 형변환하여 정확한 다중정의 메서드가 선택되도록 하자.
- 이것이 불가능하다면, 예를 들어 기존 클래스를 수정해 새로운 인터페이스를 구현해야 하는 경우, 같은 객체를 입력받는 다중정의 메서드들이 모두 동일한 동작을 하도록 구성하자.
- 그렇지 못하면 프로그래머는 다중정의 메서드 혹은 생성자를 효과적으로 쓰지 못할 것이고, 오동작의 이유도 이해하지 못할 것이다.
Comments powered by Disqus.