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 stativ 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));
}
}
}위 코드는 "집합", "리스트", "그 외"를 차례로 출력할 것 같아 보이지만, 실제로 수행해 보면 "그 외"만 세 번 연달아 출력한다.
다중정의(overloading, 오버로딩)된 세 classify() 중 어느 메서드를 호출할지가 컴파일 타임에 정해지기 때문이다. 컴파일 타임에는 for 문 안의 c는 항상 Collection<?> 타입이다. 런타임에는 타입이 매번 달라지지만, 호출할 메서드를 선택하는 데는 영향을 주지 못한다.
이처럼 직관과 어긋나는 이유는 재정의한 메서드는 동적으로 선택되고, 다중정의한 메서드는 정적으로 선택되기 때문이다. 메서드를 재정의했다면 해당 객체의 런타임 타입이 어떤 메서드를 호출할지의 기준이 된다. 컴파일 타임에 그 인스턴스의 타입이 무엇이었냐는 상관없다.
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());
}
}
}
// "포도주", "스파클링 와인", "샴페인"한편, 다중정의된 메서드 사이에서는 객체의 런타임 타입은 전혀 중요하지 않다. 선택은 오직 매개변수의 컴파일 타임 타입에 의해 이뤄진다.
안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 않는다. 가변인수를 사용하는 메서드라면 다중정의를 아예 하지 말아야 한다. 다중정의하는 대신 메서드 이름을 다르게 지어 주는 길도 항상 열려 있다.
ObjectOutputStream 클래스의 write 메서드는 다중정의가 아닌, 모든 메서드에 다른 이름을 지어주는 것을 택했다. writeBoolean(), writeInt(), writeLong() 같은 식이다. 이 방식이 다중정의보다 나은 또 다른 점은 read 메서드의 이름과 짝을 맞추기 좋다는 것이다.
매개변수 수가 같은 다중정의 메서드가 많더라도, 그중 어느 것이 주어진 매개변수 집합을 처리할지가 명확히 구분된다면 헷갈릴 일은 없을 것이다. 즉, 매개변수 중 하나 이상이 근본적으로 다르다(radically different)면 헷갈리지 않는다. 근본적으로 다르다는 건 두 타입의 (null이 아닌) 값을 서로 어느 쪽으로든 형변환할 수 없다는 뜻이다. 이 조건만 충족되면 어느 다중정의 메서드를 호출할지가 매개변수들의 런타임 타입만으로 결정된다.
public class SetList {
public static void main(String[] args) {
Set<Integer> set = new TreeSet<>();
List<Integer> list = new ArrayList<>();
for (int i = -1; 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);
}
}이 프로그램은 "[-3, -2, -1] [-3, -2, -1]"을 출력하리라 기대되지만, 실제로는 "[-3, -2, -1] [-2, 0, 2]"를 출력한다. set.remove(i)의 시그니처는 remove(Object)다. 한편, list.remove(i)는 다중정의된 remove(int index)를 선택한다. 이 remove()는 '지정한 위치'의 원소를 제거하는 기능을 수행한다.
이 문제는 list.remove()의 인수를 Integer로 형변환하여 올바른 다중정의 메서드를 선택하게 하면 해결된다.
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove((Integer) i);
}람다와 메서드 참조 역시 다중정의 시의 혼란을 키웠다.
new Thread(Syetem.out::println).start();
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(Syetem.out::println);1번과 2번의 모습은 비슷하지만 2번만 컴파일 오류가 난다. 원인은 submit() 다중정의 메서드 중에는 Callable<T>를 받는 메서드도 있다는 데 있다. 하지만 모든 println()이 void를 반환하니, 반환값이 있는 Callable과 헷갈릴 리는 없다고 생각할 수도 있다. 합리적인 추론이지만, 다중정의 해소는 이렇게 동작하지 않는다.
핵심은 다중정의된 메서드(혹은 생성자)들이 함수형 인터페이스를 인수로 받을 때, 비록 서로 다른 함수형 인터페이스라도 인수 위치가 같으면 혼란이 생긴다는 것이다. 따라서 메서드를 다중정의할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안 된다.