public class CollectionClassifier {
public static String classify(Set<?> s) {
return "집합";
}
public static String classify(List<?> l) {
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));
}
}
위 코드는 "집합", "리스트", "그 외" 를 출력할 것 같지만 실제로 수행해보면 "그 외", "그 외", "그 외" 를 출력한다. 그 이유는 다중정의된 세 classfiy 중 어느 메서드를 호출할 지 컴파일 시점에 정해지기 때문이다.
런타임에는 타입이 매번 달라지지만 컴파일 타임에는 for문 안의 c는 항상 Collection<?> 타입이다. 따라서 호출할 메서드를 선택하는데 영향을 주지 못한다. 즉, 재정의한 메서드는 동적으로 선택되고, 다중정의한 메서드는 정적으로 선택되기 때문이다.
메서드를 재정의한 다음 하위 클래스의 인스턴스에서 그 메서드를 호출하면 재정의한 메서드가 실행된다. 컴파일 타임에 그 인스턴스의 타입이 무엇이었냐는 상관없다. 다음 코드를 보자.
public class CollectionClassifier {
public static class Wine {
String name() {
return "포도주";
}
}
public static class SparklingWine extends Wine {
@Override
String name() {
return "발포성 포도주";
}
}
public static class Champagne extends Wine {
@Override
String name() {
return "샴페인";
}
}
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());
}
}
위 코드는 우리가 예상한대로 "포도주", "발포성 포도주", "샴페인"을 차례로 출력한다. for 문에서 컴파일 타입이 모두 Wine인 것과는 무관하게 가장 하위에서 정의한 재정의 메서드가 실행되는 것이다.
다중정의 메서드는 프로그래머에게 예외적인 동작으로 보일 수 있다. API 사용자가 매개변수를 넘기면서 어떤 다중정의 메서드가 호출될지 모른다면 프로그램이 오동작하기 쉽다. 런타임에 발생한 오류는 원인을 파악하기가 힘들기 때문에 다중정의가 혼동을 일으키는 상황은 피하는 것이 좋다.
안전하고 보수적으로 가기 위해서는 매개변수 수가 같은 다중정의 메서드는 만들지 말자. 가변인수(varargs)를 사용하는 메서드라면 다중정의를 아예 사용하지 말아야 한다. 이 규칙만 따른다면 어떤 다중정의 메서드가 호출될지 헷갈릴 일은 없을 것이다.
ObjectOutputSream
클래스는 write
메서드는 모든 기본 타입과 일부 참조 타입용 변형을 가지고 있다. 하지만 다중정의가 아닌 모든 메서드의 다른 이름을 지어주는 길을 택했다. writeBoolean(boolean)
, writeInt(int)
식이다.
한편, 생성자는 이름을 다르게 지을 수 없기 때문에 두 번째 생성자부터는 무조건 다중정의가 된다. 다행히 정적 팩터리라는 대안을 활용할 수 있는 경우가 많다. 또한 생성자는 재정의가 불가능하기 때문에 다중정의와 재정의가 혼용될 걱정은 하지 않아도 된다.
매개변수의 수가 같은 생성자라도 매개변수 중 하나 이상이 근본적으로 다르다
면 헷갈릴 일이 없다. 근본적으로 다르다는 것은 두 타입의 값을 서로 어느 쪽으로든 형변환할 수 없음을 의미한다.
이 조건만 충족한다면 런타임 시점에도 어떤 메서드를 호출할 지 명확해진다. 예를 들어 ArrayList
에 int
를 받는 생성자와 Collection
을 받는 생성자는 런타임 시점에서도 서로 명확히 구분된다.
List<E>
인터페이스는 remove(int index)
와 remove(Object)
를 다중정의하기 때문에 사용자가 의도한대로 동작하지 않을 수 있다. 기대한 동작을 사용하기 위해 매개변수를 형변환해주는 과정을 거쳐야한다.
다른 예시를 보자.
// 1. Thread의 생성자 호출
new Thread(System.out::println).start();
// 2. ExecutorService의 submit 메서드 호출
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);
위 두 메서드는 서로 모습을 동일하지만 2번 메서드에서는 컴파일 에러가 난다. 양쪽 모두 Runnable
을 받는 형제 메서드를 다중정의하고 있다. 문제는 submit
의 다중정의 메서드 중에서는 Callable<T>
를 받는 메서드도 있다는 데 있다. 놀라운 사실은 println
이 다중정의 없이 단 하나만 존재했다면 이 submit
메서드는 문제없이 컴파일됐을 거라는 사실이다. 지금은 호출한 메서드submit
와 참조된 메서드println
이 양쪽 다 다중정의되어서 다중정의 해소 알고리즘이 우리의 기대처럼 동작하지 않는 상황이다.
위의 Runnable
같은 함수형 인터페이스를 인수로 받을 때, 비록 서로 다른 함수형 인터페이스라도 인수 위치가 같다면 혼란이 생긴다. 따라서 메서드를 다중정의할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안된다. 이 말은, 서로 다른 함수형 인터페이스라도 서로 근본적으로 다르지 않다는 뜻이다.