다중정의를 잘못한 컬렉션 분류기
다음 컬렉션 분류기는 "집합", "리스트", "그 외"를 차례로 출력할 것 같지만, "그 외"만 세번 연달아 출력한다.
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));
}
}
그 이유는 다중정의(오버로딩) 된 세 classify 중 어느 메서드를 호출할지가 컴파일타임에 정해지기 때문이다. 컴파일타임에는 for 문 안의 c는 항상 Collection<?>타입이다. 이처럼 직관과 어긋나는 이유는 재정의한 메서드는 동적으로 선택되고, 다중정의한 메서드는 정적으로 선택되기 때문이다.
재정의한 컬렉션 분류기
public static String classify(Collection<?> c) {
return c instanceof Set ? "집합" :
c instanceof List ? "리스트" : "그 외";
}
프로그래머에게는 재정의가 정상적인 동작 방식이고, 다중정의가 예외적인 동작으로 보인다. API가 사용자가 매개변수를 넘기면서 어떤 다중정의 메서드가 호출될지 모른다면 프로그램이 오동작하기 쉽다.
다중정의가 혼란을 주는 상황을 방지하자
- 안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 말자. 가변인수를 사용하는 메서드라면 다중정의를 아예 하지 말아야 한다.
- 다중정의하는 대신 메서드 이름을 다르게 지어주자.
ex ) writeBoolean, writeInt, writeLong과 같은 형태의 메서드 이름은 read 메서드의 이름과 짝을 맞추기 좋다. readBoolean, readInt 등 - 생성자의 경우 정적 팩터리를 사용할 수 있다.
-> 생성자는 이름을 다르게 지을 수 없으므로 두 번째 생성자부터는 무조건 다중정의가 된다. 이때, 정적 팩터리 메서드를 이용할 수 있다.
오토박싱의 도입과 다중정의
다음은 오토박싱의 도입으로 기존에 존재하던 다중정의 메서드가 문제가 된 케이스이다.
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 + " " + list);
}
}
set.remove(i)의 시그니처는 remove(Object)이지만 list.remove(i)는 다중정의된 remove(int index)를 선택한다. 이를 해결하기 위해서는 Integer로 형변환하여 올바른 다중정의 메서드를 선택하게 하면 해결된다.
list.remove((Integer) i);
제네릭이 도입되기 전인 자바 4까지의 List에서는 Object와 int가 근본적으로 달라서 문제가 없었지만 제네릭과 오토박싱이 등장하면서 두 메서드의 매개변수 타입이 더는 근본적으로 다르지 않게 되었다.
람다와 메서드 참조 그리고 다중정의
// 1번. Thread의 생성자 호출
new Thread(System.out::println).start();
// 2번. ExecutorService의 submit메서드 호출
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);
1번과 2번이 모습은 비슷하지만, 2번만 컴파일 오류가 난다. 원인은 submit 다중정의 메서드 중에 Callable를 받는 메서드가 있기 때문이다. 이 경우는 참조된 메서드(println)과 호출한 메서드(submit) 양쪽 다 다중정의되어, 다중정의 해소 알고리즘이 우리의 기대처럼 동작하지 않은 상황이다.
핵심은 다중정의된 메서드(혹은 생성자)들이 함수형 인터페이스를 인수로 받을 때, 비록 서로 다른 함수형 인터페이스라도 인수 위치가 같으면 혼란이 생긴다. 따라서 메서드를 다중정의할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안된다.
예외
어떤 다중정의 메서드가 불리는지 몰라도 기능이 똑같다면 신경 쓸 게 없다. 이렇게 하는 가장 일반적인 방법은 상대적으로 더 특수한 다중정의 메서드에서 덜 특수한 다중정의 메서드로 일을 넘겨버리는(forward)것이다.
정리
- 일반적으로 매개변수가 수가 같을 때는 다중정의를 피하는 게 좋다.
- 1번이 불가능하다면 헷갈릴 만한 매개변수는 형변환하여 정확한 다중정의 메서드가 선택되도록 해야 한다.
- 2번이 불가능하다면 기존 클래스를 수정해 새로운 인터페이스를 구현해야 할 때는 같은 객체를 입력받는 다중정의 메서드들이 모두 동일하게 동작하도록 만들어야 한다.