다중정의(오버로딩)는 신중히 사용하라
프로그래밍 언어가 오버로딩을 허용한다고 해서 다중정의를 꼭 활용하라는 뜻은 아니다. 보통 파라미터 수가 같을 때는 오버로딩을 피하는 게 좋다. 메서드명을 달리 한다거나의 방법으로 말이다.
그런데 생성자의 경우 메서드명이 정해져 있는데 어떡하나? 이럴 경우 헷갈릴 만한 파라미터를 형변환하여 명확한 오버로딩 메서드가 선택되도록 하면 된다.
자세한 내용은 아래 참고하자.
혼란을 주는 오버로딩 메서드 예시
다음과 같이 classify 메서드가 3개로 오버로딩되어있다.
아래 예시는
─ 집합, 리스트, 그 외 컬렉션 타입 객체를 순서대로 classify()를 통해 호출해보는 코드다.
─ 그럼 '집합', '리스트', '그 외'가 순서대로 출력되어야 할 것 같지만, '그 외', '그 외', '그 외'가 출력된다.
이유는??
"컴파일 시점"에 for문 안 c가 항상 Collection<?> 타입이기 때문이다.
어떤 오버로딩 메서드를 호출할지는 컴파일 시점에 결정된다.
이렇게 컴파일타임에 정해진 매개변수 타입인 Collection으로 기준이 생겨버려서 항상 세번째 메서드를 호출하게 된다.
public class CollectionClassifier {
public static String classify(Set<?> s) {
return "집합";
}
public static String classify(List<?> s) {
return "리스트";
}
public static String classify(Collection<?> s) {
return "그 외";
}
public static void main(String[] args) {
// 집합, 리스트, 그 외 컬렉션을 요소로 담는다.
Collection<?>[] collections = {
new HashSet<String>(),
new ArrayList<BigInteger>(),
new HashMap<String, String>().values()
};
// 각 요소를 classify()로 호출해본다.
// 과연 '집합', '리스트', '그 외' 순서대로 출력될까?
for (Collection<?> c : collections)
System.out.println(classify(c));
}
}
위와 같이, 오버로딩 메서드는 정적(컴파일시점의 타입)으로 선택된다.
* 더보기: 재정의한 메서드는 동적(런타임의 타입)으로 선택됨
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()));
// 오버라이딩된 메서드 name()을 호출해볼 것이다.
// Wine 타입으로 for문을 돌더라도
for (Wine wine : wineList)
System.out.println(wine.name()); // 재정의된 메서드 name()이 호출된다! 런타임 타입이 기준이 되기 때문이다.
}
}
그렇다면, 오버로딩된 메서드를 동적으로 선택하려면 어떻게 해야 하는지?
오버로딩 메서드를 동적으로 선택하기
원래 정적(컴파일타임 기준)으로 선택되던 오버로딩 메서드를 동적으로 판단하고 싶다면?
메서드를 하나만 두고 그 속에서 instanceof() 를 통해 명시적으로 검사하면 깔끔하다.
public static String classify(Collection<?> c) {
return c instanceof Set ? "집합" :
C instanceof List ? "리스트" : "그 외";
}
혼동되는 오버로딩 코드는 작성하지 말자
예시로 든 상황은 '개발자가 의도하지 않은 오버로딩 동작'을 보여준다.
헷갈릴 수 있는 코드는 작성하지 않는게 좋으며, 특히 공개 API는 더 그렇다.
왜? API 사용자가 매개변수를 넘기면서 어떤 오버로딩 메서드가 호출될 지 모르면 오동작하기 쉽다.
애초에 오버로딩이 혼동을 일으키는 상황을 피하자.
안전하게 가려면 이렇다.
1. "매개변수 수가 같은 다중정의는 만들지 말자"
2. "가변인수를 사용하는 메서드라면 다중정의를 아예 하지 말자" (예외: Item53에 기술)
3. "다중정의하는 대신 메서드 이름을 다르게 지어주는 것도 생각"
3번의 좋은 예가 ObjectOutputStream 클래스이다.
이 클래스에서는 write()를 오버로딩하고 있지 않고, 모든 메서드에 다른 이름을 붙여주었다.
writeBoolean(boolean), writeInt(int), writeLong(long) ... 처럼!
그렇게 하면, 각각의 read() 메서드와 짝을 이룰 수 있다는 것도 좋은 점이다.
readBoolean(), readInt(), readLong() ...
생성자는 어쩔 수 없지 않나요?
생성자는 이름이 무조건 정해져 있으니, 여러 생성자를 두는 경우 무조건 오버로딩이 된다.
이때 정적 팩터리라는 대안이 있다(Item1)
뭐 그냥 생성자의 오버로딩을 사용하더라도 안전한 경우도 있다.
매개변수 수가 같은 오버로딩 메서드를 두더라도, 매개변수 중 하나가 근본적으로 다르면 혼동될 일이 없다!
즉, 근본적으로 타입이 달라서 오버로딩 메서드 파라미터간 타입 캐스팅이 불가능하다면, 런타임에서 혼동될 일 없이 "정해진 오버로딩 메서드"를 호출하게 된다. 개발자가 혼란스러워질 요인이 없다는 말씀!
└ex: ArrayList에는 int를 받는 생성자와 Collection을 받는 생성자가 있다. 두 타입은 근본적으로 다르다.
오버로딩 파라미터에서 기본타입과 참조타입의 혼동1
─ Object인 Integer, 그리고 int
자바5에서 도입된 오토박싱은 문제가 될 수 있다.
자바4까지는 모든 기본 타입과 모든 참조 타입이 근본적으로 달랐다. 예를 들면 Object와 int는 무조건 달랐다.
하지만 제네릭과 오토박싱이 등장한 이후, Object와 int가 근본적으로 다르지 않아졌다.
*오토 박싱: 컴파일러에 의해 기본타입이 래퍼 클래스로 자동 변환 되는 것
만약 두개의 오버로딩 메서드 파라미터 타입이 각각 Integer타입과 int타입으로 있다면,
개발자는 혼동할 수 있다.
다음 코드로 그러한 예시를 보자.
-3부터 2까지의 정수를 set과 list에 각각 쭈루룩 넣은 후
숫자 0, 1, 2에 대한 remove 메서드를 호출한다.
그러면 당연히 set과 list 모두 [-3, -2, -1]을 출력하리라 기대한다.
그치만 실제로는! 아니! 그렇지 않다!
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++){ // -3부터 2까지 정수를 add
set.add(i);
list.add(i);
}
for(int i=0; i<3; i++){ // 0, 1, 2를 remove
set.remove(i);
list.remove(i);
}
System.out.println(set + " " + list); // 결과는?
}
}
set에서는 [-3, -2, -1] 을 출력하지만,
list에서는 [-2, 0, 2]를 출력한다.
오이잉? 뭐얏 이 괴상한..!
이유는 명확하다.
Set의 remove()는 remove(Object)다.
다중정의된 다른 메서드는 없다.
List의 remove()는, remove(Object)와 remove(int)로 오버로딩 되어있다.
그 중 remove(int index)가 선택되어서 0, 1, 2 인덱스를 지웠던 것이다!
개발자의 의도대로 0, 1, 2라는 정수 요소를 지우려면 어떡할까?
Object를 받도록 Integer로 형변환해주면 된다. 이 경우 Integer 클래스로 전달해야 한다는 말이다.
for(int i=0; i<3; i++){
set.remove(i);
list.remove((Integer) i); // 혹은 remove(Integer.valueOf(i))
}
위와 같이, List 인터페이스의 remove는 제네릭과 오토박싱의 등장 이후 취약해졌다고 볼 수 있다.
다중정의시 주의해야 할 이유를 충분히 보여주고 있다.
오버로딩 파라미터에서 기본타입과 참조타입의 혼동2
─ 람다와 메서드의 등장
자바8에서는 람다와 메서드 참조가 등장했다.
이 또한 오버로딩시 혼란을 야기할 수 있다.
∨ 부정확한 메서드 참조 문제
아래의 1번과 2번은 유사한 코드다.
- System.out::println를 인수로 받음
- Thread(), submit() 둘 다, Runnable을 받는 형제 메서드를 오버로딩하고 있음
2번만 컴파일 오류가 난다.
// 1번. Thread의 생성자 호출
new Thread(System.out::println).start();
// 2번. ExecutorService의 submit 메서드 호출
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);
이유는? submit()의 오버로딩 메서드 중에는, Runnable<T> 뿐만 아니라 Callable<T>를 받는 메서드도 있기 때문이다. 파라미터의 println은 void를 호출하는데, 그렇다면 두 오버로딩 메서드 중 어느 쪽을 선택해야 할까? 모른다.
즉 파라미터의 System.out::println은 "부정확한 메서드 참조(inexact method reference)" 컴파일 에러가 난다.
∨ 람다식
메서드 오버로딩시 "서로 다른 함수형 인터페이스라도 같은 위치의 파라미터로 받아서는 안 된다"
─ 서로 다른 함수형 인터페이스라도 서로 근본적으로 다르지 않다는 뜻이다.
─ 오버로딩 메서드가 람다식(함수형 인터페이스)을 파라미터로 받을 때, 비록 서로 다른 함수형 인터페이스라도 파라미터 위치가 같으면 혼란이 생긴다.
*참고: "암시적 타입 람다식(implicitly typed lambda expression)이나 "부정확한 메서드 참조(inexact method reference)" 같은 인수 표현식은 목표 타입이 선택되기 전에는 그 의미가 정해지지 않기 때문에 적용성 테스트(applicability test)때 무시된다고 한다. ← 컴파일러 제작자를 위한 설명이므로 이해되지 않으면 넘어가라고 책에 쓰여 있다.
참고
오버로딩 메서드 중 하나를 선택하는 규칙은 매우 복잡하며, 자바 버전업될수록 더 복잡해지고 있다.
이 모두를 이해하고 사용하는 프로그래머는 극히 드물 것이다.
'JAVA > Effective Java' 카테고리의 다른 글
[이펙티브 자바] null이 아닌, 빈 컬렉션이나 배열을 반환하라 ─ 8장:메서드:Item54 (0) | 2023.10.18 |
---|---|
[이펙티브 자바] 가변인수는 신중히 사용하라 ─ 8장:메서드:Item53 (0) | 2023.10.18 |
[이펙티브 자바] 메서드 시그니처를 신중하게 설계하라 ─ 8장:메서드:Item51 (0) | 2023.10.17 |
[이펙티브 자바] 적시에 방어적 복사본을 만들라 ─ 8장:메서드:Item50 (1) | 2023.10.17 |
[이펙티브 자바] 매개변수가 유효한지 확인하라 ─ 8장:메서드:Item49 (2) | 2023.10.16 |