2023. 12. 29. 13:42ㆍBook/이펙티브 자바
다음 컬렉션 분류기 코드를 보자
// 코드 52-1 컬렉션 분류기 - 오류! 이 프로그램은 무엇을 출력할까? (312쪽)
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 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));
}
}
=>
그 외
그 외
그 외
"집합", "리스트", "그 외"를 차례로 출력할 것 같지만 "그 외"만 세 번 출력된다.
다중정의된 세 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());
}
}
=>
포도주
발포성 포도주
샴페인
for문의 컴파일타임 타입이 Wine이지만 가장 하위에서 정의한 재정의 메서드가 실행된다.
다중정의된 메서드 사이에서는 객체의 런타임 타입은 전혀 중요하지 않는다. 선택은 컴파일타입에, 매개변수의 컴파일타임 타입에 의해 이뤄진다.
이 문제는 모든 classify 메서드를 하나로 합친 후 instanceof로 명시적으로 검사하면 해결할 수 있다.
public static String classify(Collection<?> c) {
return c instanceof Set ? "집합" :
c instanceof List ? "리스트" : "그 외";
}
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));
}
=>
집합
리스트
그 외
안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 말자 가변인수를 사용하는 메서드라면 다중정의를 아예 하지 말아야 한다. 다중정의하는 대신 메서드 이름을 다르게 지어주 방법도 있으니 말이다.
하지만 생성자는 이름을 다르게 지을 수 없으니 두 번재 생성자부터는 무조건 다중정의가 된다. 하지만 정적 팩터리라는 대안을 활용할 수 있는 경우가 많다. 그래도 여러 생성자가 같은 수의 매개변수를 받아야 하는 경우를 완전히 피해갈 수는 없으니 안전대책을 배워두자
매개변수 수가 같은 다중정의 메서드가 많더라도, 그중 어느 것이 주어진 매개변수 집합을 처리할지가 명확히 구분된다면 헷갈릴 일은 없다. 구분된다는 말은 두 타입의 값을 서로 어느쪽으로든 형변환 할 수 없다는 말이다. 이 조건만 충족된다면 어느 다중정의 메서드를 호출할지가 매개변수들의 런타임 타입으로 결정된다.
예)
List<Integer> list1 = new ArrayList(1);
Queue<Integer> queue = new LinkedList<>();
queue.add(1);
queue.add(1);
queue.add(1);
List<Integer> list2 = new ArrayList(queue);
다중정의가 문제를 일으키는 다른 예도 보자
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] [-2, 0, 2]
위 출력문에서 [-3, -2, -1] [-3, -2, -1] 이 출력될 것 같지만 그렇지 않다.
set.remove(i) 의 시그니처는 remove(Object o)다. 다중정의된 다른 메서드 없이 입력받은 원소를 Set 컬렉션에서 제거한다.
list.remove(i) 는 다중정의된 remove(int index)를 선택한다. 그래서 0, 1, 2 번째 원소를 차례대로 제거하고 -2, 0, 2 가 출력되는 것이다.
이 문제는 list.remove 의 인수를 Integer로 형변환하여 올바른 다중정의 메서드를 선택하게 하면 된다.
그러니 일반적으로 매개변수 수가 같을 때는 다중정의를 피하는 게 좋다.
'Book > 이펙티브 자바' 카테고리의 다른 글
Item55 - 옵셔널 반환은 신중히 하라 (1) | 2024.01.03 |
---|---|
가변인수는 신중히 사용하라 (0) | 2023.12.31 |
Item51 - 메서드 시그니처를 신중히 설계하라 (0) | 2023.12.28 |
Item50 - 적시에 방어적 복사본을 만들라 (0) | 2023.12.27 |
Item49 - 매개변수가 유효한지 검사하라 (1) | 2023.12.26 |