public class CollectionClassifier {
public static String classify(Set<?> set) {
return "Set";
}
public static String classify(List<?> list) {
return "List";
}
public static String classify(Collection<?> collection) {
return "Others";
}
}
public static void main(String[] args) {
Collection<?>[] collections = {
new HashSet<String>(),
new ArrayList<Long>(),
new HashMap<Long, String>().values()
};
Arrays.stream(collections).forEach(c -> System.out.println(classify(c)));
}
// 실행결과는 모두 Others 출력
Others만 세 번 연달아 출력하는 이유는 아래와 같다.
다중정의된 메서드 사이에서는 객체의 런타임 타입은 전혀 중요치 않다. 선택은 컴파일 타임에, 오직 매개변수의 컴파일타임 타입에 의해 이뤄진다. 위의 문제는 (정적 메서드를 사용해도 좋다면) 아래와 같이 해결할 수 있다.
// 모든 classify 메서드를 하나로 합친 후
// instanceof로 명시적으로 검사하면 말끔히 해결된다.
public static String classify(Collection<?> c) {
return c instanceof Set ? "Set" :
c instanceof List ? "List" : "Others";
}
이번에는 ObjectOutputStream 클래스를 살펴보자. 이 클래스는 write를 overloading 할만도한데, 모든 메서드에 다른 이름을 지었다. 이 방식이 overloading보다 좋은 다른 점은 read 메서드의 이름과 짝을 맞추기 좋다는 것이다.
// 명확히 구분되지 않는 예시.
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);
}
}
이 예가 혼란스러웠던 이유는 List 인터페이스가 remove(Object)와 remove(int)를 overloading했기 때문이다. 즉, 자바 언어에 제네릭과 오토박싱을 더한 결과 List 인터페이스가 취약해졌다.(자바 4 까지는 Integer와 int가 다르게 취급됨 - 자바5부터 오토박싱이 도입된 결과)
// 1번. Thread의 생성자 호출
new Thread(System.out::println).start();
// 2번. ExecutorService의 submit 메서드 호출
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);
2번의 경우 컴파일 오류가 난다. 원인은 submit의 overloading 메서드 중에는 Callable를 받는 메서드도 있다는 데 있다. 하지만 모든 println이 void를 반환하니, 반환값이 있는 Callable과 헷갈릴 리는 없다고 생각할지도 모르겠다. 합리적인 추론이지만, overloading resolution(overloading 메서드 찾는 알고리즘)은 이렇게 동작하지 않는다. 만약 println이 overloading 없이 단 하나만 존재했다면 submit() 호출이 제대로 컴파일됐을 것이다. 지금은 참조된 메서드(println)와 호출한 메서드(submit) 양쪽 다 overloading되어서 기대처럼 작동하지 않는 상황이다.
원인의 핵심은 다중정의된 메서드(혹은 생성자)들이 함수형 인터페이스를 인수로 받을 때, 비록 서로 다른 함수형 인터페이스라도 인수 위치가 같으면 혼란이 생긴다는 것이다. 따라서 메서드를 다중정의할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안된다.
자바 라이브러리는 이번 아이템의 정신을 지켜내려 애쓰고 있지만, 실패한 클래스도 몇 개 있다. 예컨대 String의 valueOf(char[])과 valueOf(Object)는 같은 객체를 건네더라도 전혀 다른 일을 수행한다.