아이템 52. 다중정의(overloading)는 신중히 사용하라. - ksw6169/effective-java GitHub Wiki
public class CollectionClassifier {
public static String classify(Set<?> s) { return "집합"; }
public static String classify(List<?> list) { 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));
}
}
위 코드를 실행해보면 "집합", "리스트", "그 외"를 차례로 출력할 것 같지만 실제로 수행해보면 "그 외"만 세 번 연달아 출력한다.
// 출력 결과
그 외
그 외
그 외
이렇게 출력된 이유는 다중정의(overloading) 된 세 classify()
메소드 중 어느 메소드를 호출할지가 컴파일 타임에 정해지기 때문이다. 컴파일 타임에는 for문 안의 c는 항상 Collection<?>
타입이다.
for (Collection<?> c : collections)
System.out.println(classify(c));
-
Collection<?>
c는 런타임에 타입이 매번 달라지지만, 호출할 메소드를 선택하는 데는 영향을 주지 못한다. 따라서 컴파일 타입의 매개변수 타입을 기준으로 항상 세 번째 메소드인classify(Collection<?>)
만 호출된다. - 정리하면 재정의한 메소드는 동적으로 선택되고, 다중정의한 메소드는 정적으로 선택된다.
- 즉, 메소드를 재정의했다면 해당 객체의 런타임 타입이 어떤 메소드를 호출할지의 기준이 되지만 다중정의했다면 해당 객체의 컴파일타임 타입이 어떤 메소드를 호출할지의 기준이 된다.
다음 코드에서는 객체의 런타임 타입에 따라 재정의된 메소드의 호출이 달라지는 것을 확인할 수 있다.
public class Overloading {
public static void main(String[] args) {
Stream<Wine> stream = Stream.of(new Wine(), new SparklingWine(), new Champagne());
stream.forEach(wine -> System.out.println(wine.name()));
}
}
class Wine {
String name() { return "포도주"; }
}
class SparklingWine extends Wine {
@Override String name() { return "발포성 포도주"; }
}
class Champagne extends SparklingWine {
@Override String name() { return "샴페인"; }
}
// 출력 결과
포도주
발포성 포도주
샴페인
한편, 다중정의된 메소드 사이에서는 객체의 런타임 타입은 전혀 중요치 않다. 선택은 컴파일타임에, 오직 매개변수의 컴파일타임 타입에 의해 이뤄진다. 위에서 작성했던 코드를 아래와 같이 수정해보자.
public static void main(String[] args) {
System.out.println(classify(new HashSet<>()));
System.out.println(classify(new ArrayList<BigInteger>()));
System.out.println(classify(new HashMap<String, String>().values()));
}
컴파일타임 타입은 Collection
이 아니므로 의도한 결과가 출력된다.
// 출력 결과
집합
리스트
그 외
API 사용자가 매개변수를 넘기면서 어떤 다중정의 메소드가 호출될지를 모른다면 프로그램이 오동작하기 쉽다. 그러니 다중정의가 혼동을 일으키는 상황을 피해야 한다. 이를 위해서 다음 사항들을 준수하자.
- 안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 말자.
- 가변인수(varargs)를 사용하는 메소드라면 다중정의를 아예 하지 말아야 한다.
- 다중정의를 하는 대신 메소드 이름을 다르게 지어질 수도 있다.
ObjectOutputStream
의 write()
메소드는 다중정의 대신 모든 메소드에 다른 이름을 지어주는 길을 택했다.
public void writeBoolean(boolean val);
public void writeByte(int val);
public void writeInt(int val);
public void writeLong(long val);
public void writeDouble(double val);
...
- 생성자는 이름을 다르게 지을 수 없으니 두 번째 생성자부터는 무조건 다중정의가 된다.
- 하지만 대신 정적 팩토리라는 대안을 활용할 수 있는 경우가 많다.
매개변수 수가 같은 다중정의 메소드가 많더라도 매개변수 중 하나 이상이 근본적으로 다르다(radically different, 두 타입의 값을 서로 어느 쪽으로든 형변환할 수 없다.) 면 헷갈릴 일은 없을 것이다. 이 조건만 충족하면 어느 다중정의 메소드를 호출할지가 매개변수들의 런타임 타입만으로 결정된다. 따라서 컴파일타임 타입에는 영향을 받지 않게 되고, 혼란을 주는 주된 원인이 사라진다.
// 매개변수의 타입이 상호변환 될 수 없다면 두 생성자 중 어느 것이 호출될지 헷갈릴 일은 없다.
public class User {
public User(String s) { }
public User(List<String> list) { }
}
자바 5에서 오토박싱이 도입되면서 기본 타입이 박싱된 기본 타입과 근본적으로 다르지 않게 되었다.
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);
}
}
[-3, -2, -1] [-3, -2, -1]
이 출력될 것 같지만 실제는 list.remove(i)
가 다중정의된 remove(int index)
를 호출하여 아래와 같은 결과를 출력한다.
// 출력 결과
[-3, -2, -1] [-2, 0, 2]
따라서 의도한대로 동작시키기 위해서는 다음과 같이 명시적 형변환을 해줘야 한다.
for (int i=0; i<3; i++) {
set.remove(i);
list.remove((Integer) i); // 혹은 remove(Integer.valueOf(i))
}
자바 8에서 도입한 람다와 메소드 참조 역시 다중정의 시의 혼란을 키웠다.
// 1번. Thread의 생성자 호출
new Thread(System.out::println).start();
// 2번. ExecutorService의 submit 메소드 호출
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);
1번과 2번의 모습은 비슷하지만, 2번만 컴파일 오류가 난다. 원인은 ExecutorService.submit()
메소드가 각각 Runnable
과 Callable<T>
을 받도록 다중정의되어 있기 때문이다.
Future<?> submit(Runnable task);
<T> Future<T> submit(Callable<T> task);
다중정의된 메소드(혹은 생성자)들이 함수형 인터페이스를 인수로 받을 때, 서로 다른 함수형 인터페이스라 하더라도 인수 위치가 같으면 혼란이 생긴다. 따라서 메소드를 다중정의할 때 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안된다.
- Effective Java 3/E