요약
메서드 매개변수 수가 같을 때는 다중정의(overloading)는 되도록 하지 말자
(사용자 입장에서 어느 메서드를 사용하는지 혼란을 줄 수 있기 때문이다).
만약 다중정의를 해야한다면
1)형변환을 하거나
2) 인수 포워드 등을
통해 같은 객체를 받는 다중 정의 메서드들이 모두 동일하게 동작하도록 만들어야 한다.
1. 다중정의의 오류
1-1. 다중정의 메서드는 정적 선택
public class CollectionClassifier {
public static String classify(Set<?> c) {
return "집합";
}
public static String classify(List<?> c) {
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));
}
}
}
위 코드 main()을 실행하면 ’집합’, ‘리스트’, ‘그 외’가 출력될 것이라 예상하지만 실제로는 ’그 외’, ‘그 외’, ‘그 외’가 출력된다. 그 이유는 다중정의 메서드 중 어떤 메서드를 호출할지가 컴파일 타임에, 매개변수의 컴파일타임 타입에 의해서만 정해지기 때문이다. 컴파일타임에는 for 문 안의 c는 항상 Collection<?> 타입이다. 그렇기에 세번째 classify()가 호출되어 모두 ‘그 외’가 출력이 되는 것이다.
1-2. 해결책
위 코드를 의도한대로 출력하게 하기 위해서는 모든 classify 메서드를 하나로 합친 후, instanceof로 명시적으로 검사하면 해결된다.
public static String classify(Collection<?> c) {
return c instanceof Set ? "집합":
c instanceof List ? "리스트" : "그 외";
}
1-3. 재정의 메서드는 동적 선택
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> windList = List.of(
new Wine(), new SparklingWine(), new Champagne());
for (Wine wine : windList) {
System.out.println(wine.name());
}
}
}
반면 재정의(override) 메서드는 런타임에서 동적으로 선택되기 때문에 위 예시에서는 “포도주”, “발포성 포도주”, “샴페인”으로 예상한 값과 동일하게 출력된다.
2. 다중정의 사용법
위의 내용으로 다중정의 메서드 작성 시 헷갈릴 수 있는 코드는 작성하지 않는 게 좋다.
특히 오픈 API라면 더욱 신경 써야 한다. API 사용자가 매개변수를 넘기면서 어떤 다중정의 메서드가 호출될지를 모른다면 프로그램이 오작동하기 쉬워 런타임에 이상하게 행동해 API 사용자로 하여금 혼란을 줄 수 있다.
2-1. 가이드라인
안전하고 보수적으로 가려면 다음과 같은 가이드라인을 따르는 것을 권장한다.
- 매개변수 수가 같은 다중정의는 만들지 말자
- 가변인수(varargs)를 사용하는 메서드라면 다중정의를 아예 하지 말아야 한다
- 되도록이면 다중정의 대신 메서드 이름을 다르게 짓는 것도 방법이 될 수 있다
- 예시로는 ObjectOutputStream이 있다. write 메서드의 매개변수에 따라 writeBoolean(boolean), writeInt(int), readBoolean(), readInt() 형식으로 메서드를 구분한다.
2-2. 생성자
생성자 같은 경우는 이름을 다르게 지을 수 없으니 두 번째 생성자부터는 무조건 다중정의가 된다.
이 문제는 1)정적팩터리를 사용하거나 2) 매개변수간 형변환을 할 수 없는 다중정의 메서드(예시로 ArrayList에는 ArrayList(int) 생성자와 ArrayList(<Collection>) 생성자가 있다. int와 Collection은 완전히 다르므로 메서드 호출에 혼동이 생길 일은 없다)를 만들어서 해결할 수 있다.
2-3. 인수 포워드
인수 포워드는 상대적으로 더 특수한 다중정의 메서드에서 더 일반적인 다중정의 메서드로 일을 넘겨버리는 것(forward)을 의미한다.
public boolean contentEquals(StringBuffer sb) {
return contentEquals((CharSequence) sb);
}
3. 자바5 버전 이후 다중 정의 문제
3-1. 제네릭, 오토박싱 으로 인한 문제
public class SetList {
public static void main(String[] args) {
Set<Integer> set = new TreeSet<>();
List<Integer> list = new ArrayList<>();
// -3 ~ 2 까지 정수 삽입
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]
// 실제 출력 결과
[-3, -2, -1] [-2, 0, 2]
예상과 다른 출력이 발생한 이유는 list.remove()의 다중정의 때문이다. set.remove(i)의 시그니처는 remove(Object)인 반면, list.remove(i)의 시그니처는 다중정의된 remove(int index)(List<E> 인터페이스는 remove(Object)와 remove(int)를 다중정의 함)를 선택한다. 따라서 list의 경우 각 단계마다 0번째, 1번째, 2번째의 원소를 제거한다. 따라서 [-3, -2, -1] 이 아닌 [-2, 0, 2]가 출력된다.
- 해결
for (int i = 0; i < 3; i++) { set.remove(i); list.remove((Integer) i); // 혹은 list.remove(Integer.valueOf(i)) }
- 위 문제 해결은 list.remove()에서 int를 Integer로 형변환하면 된다.
3-2. 람다, 메서드 참조로 인한 문제
// 1번. Thread 생성자 호출
new Thread(System.out::println).start();
// 2번. ExecutorService의 submit 메서드 호출
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);
// 결과 : 1번은 정상작동 하지만, 2번은 정상 작동하지 않는다.
각 함수에 넘겨진 인수는 System.out::println으로 동일하고, 양쪽 모두 Runnable을 받는 형제 메서드를 다중정의하고 있다. 그런데 왜 2번만 실패하는 것일까? 이유는 submit도 다중정의(submit 다중 메서드 중에는 Callable<T>를 받는 메서드가 있기 때문)되어 있고, println도 다중정의(적절한 다중정의 메서드를 찾는 알고리즘) 되어 있기에 다중정의 해소 알고리즘이 우리의 기대(println은 void를 반환하니, 반환값이 있는 Callable과 헷갈리지 않을 것이기에 정상동작할 것이다.)처럼 동작하지 않기 때문이다.
→ 다중정의된 메서드(혹은 생성자)들이 함수형 인터페이스를 인수로 받을 때, 비록 서로 다른 함수형 인터페이스라도 인수 위치가 같으면 혼란이 생긴다는 것이다.
→ 따라서, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안 된다.(서로 다른 함수형 인터페이스라도 서로 근본적으로 다르지 않다는 뜻)
4. 예외
다중정의된 메서드라도 같은 동작을 하면 상관없지만, 예외가 있는데 String 클래스의 valueOf(char[])과 valueOf(Object)는 같은 객체를 건네더라도 전혀 다른 일을 수행한다.
'Dev Language > EffectiveJava' 카테고리의 다른 글
[EffectiveJava] 지역변수의 범위를 최소화하라 (0) | 2025.03.30 |
---|---|
[EffectiveJava] 가변인수는 신중히 사용하라 (0) | 2025.03.23 |
[EffectiveJava] 적시에 방어적 복사본을 만들라 (0) | 2025.03.09 |
[EffectiveJava] 스트림 병렬화는 주의해서 적용하라 (0) | 2025.03.03 |
[EffectiveJava] 스트림에서는 부작용 없는 함수를 사용하라 (0) | 2025.02.23 |