이펙티브 자바, 쉽게 정리하기 - item 52. 다중정의는 신중히 사용하라
다중정의(Overloading) 문제의 예제 코드
static class CollectionClassifier {
public static String classify(Set<?> s) {
return "집합";
}
public static String classify(List<?> s) {
return "리스트";
}
public static String classify(Collection<?> s) {
return "그 외 컬렉션";
}
}
@Test
public void collectionClassifierTest() {
Collection<?>[] collections = {
new HashSet<>(),
new ArrayList<>(),
new HashMap<>().values()
};
for (Collection<?> collection : collections) {
System.out.println(CollectionClassifier.classify(collection));
}
}
- 실행 결과
"그 외 컬렉션"
만 3번 출력된다. - 오버로드된 메서드 중 어느 메서드를 실행할지는 컴파일타임에 결정된다.
- 컴파일타임의 매개변수를 기준으로 항상
Collection<?>
을 받는 메서드만 호출된다. - 런타임의 타입을 신경쓰지 않는다.
- 컴파일타임의 매개변수를 기준으로 항상
- 재정의(override) 한 메서드는 동적으로 선택되고 다중 정의(overload) 한 메서드는 정적으로 선택되기 때문이다.
다중 정의(Overloading)가 이러한 혼동을 일으키는 상황을 피해야 한다.
재정의(Overriding)의 경우
static class Wine {
String name() { return "포도주"; }
}
static class SparklingWine extends Wine {
@Override String name() { return "발포성 포도주"; }
}
static class Champagne extends SparklingWine {
@Override String name() { return "샴페인"; }
}
@Test
public void wineTest() {
List<Wine> wineList = List.of(new Wine(), new SparklingWine(), new Champagne());
for (Wine wine : wineList) {
System.out.println("wine.name() = " + wine.name());
}
}
- 다중 정의(overload) 처럼 모호한 구석이 없으며, 런타임에 결정된 타입에서 재정의된 메서드가 실행된다.
오버로딩 안쓰고 문제 해결하기
static class CollectionClassifier2 {
public static String classify(Collection<?> s) {
if (s instanceof Set) {
return "집합";
}
else if (s instanceof List) {
return "리스트";
}
return "그 외 컬렉션";
}
}
@Test
public void collectionClassifierTest2() {
Collection<?>[] collections = {
new HashSet<>(),
new ArrayList<>(),
new HashMap<>().values()
};
for (Collection<?> collection : collections) {
System.out.println(CollectionClassifier2.classify(collection));
}
}
- 오버로딩으로 메서드를 따로 만들지 않고
instanceof
연산자를 통해 문제를 해결해도 된다.
다중 정의에서 주의할 점
- 다중 정의가 혼동을 일으키는 상황을 피하자
- 매개변수 수가 같은 다중 정의는 만들지 말자
- 혹여나 가변인수를 사용한다면, 다중정의 자체를 만들지 말자.
- 다중 정의 대신 메서드 이름을 다르게 짓자
ObjectOutputStream
클래스의 경우writeBoolean()
,writeInt()
와 같은 이름의 메서드를 제공한다.- 생성자의 경우엔 1개 이상이라면, 무조건 다중정의가 되어버린다. 이 경우엔 정적 팩터리라는 대안을 이용하여 생성 의도와 코드를 명확하게 할 수 있다.
단, 다중 정의의 매개변수가 명확하게 다른 타입이라면, 괜찮다.
다중 정의의 함정 1: 오토박싱
@Test
public void boxingTest() {
Set<Integer> set = new TreeSet<>();
List<Integer> list = new ArrayList<>();
for (int i = -3; i < 3; i++) {
set.add(i);
list.add(i);
}
System.out.println("set = " + set);
System.out.println("list = " + list);
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove(i);
}
System.out.println("set = " + set);
System.out.println("list = " + list);
}
- 간단히 눈으로 코드를 해석해보면,
-3 ~ 2
까지 숫자를 넣고,0 ~ 2
까지의 숫자를 지우려는 의도가 보인다.
실행 결과
set = [-3, -2, -1, 0, 1, 2]
list = [-3, -2, -1, 0, 1, 2]
set = [-3, -2, -1]
list = [-2, 0, 2]
set
의 결과는 의도와 같은데,list
의 결과는 의도와 다르다.- 그 이유는
list
에는remove(Object element)
와remove(int index)
두가지 메서드가 다중 정의되어 있기 때문이다.- 이 중 우리가 의도한 것은 첫번째 메서드인데, 두번째 메서드가 적용되었다.
- 반면
set
에는remove(Object element)
메서드밖에 존재하지 않아서 우리의 의도대로 메서드가 적용됐다.
정상작동하도록 캐스팅하기
@Test
public void boxingTest2() {
Set<Integer> set = new TreeSet<>();
List<Integer> list = new ArrayList<>();
for (int i = -3; i < 3; i++) {
set.add(i);
list.add(i);
}
System.out.println("set = " + set);
System.out.println("list = " + list);
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove((Integer) i);
}
System.out.println("set = " + set);
System.out.println("list = " + list);
}
Integer
라는 박싱타입으로 캐스팅하여 정상동작하도록 변경했다.
실행 결과
set = [-3, -2, -1, 0, 1, 2]
list = [-3, -2, -1, 0, 1, 2]
set = [-3, -2, -1]
list = [-3, -2, -1]
다중 정의의 함정 2: 람다와 메서드 참조
@Test
public void lambdaTest1() {
new Thread(System.out::println).start();
}
@Test
public void lambdaTest2() {
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);
}
lambdaTest1()
의 경우Thread()
에서 함수형 인터페이스인Runnable
을 받기 때문에, 위와 같이 메서드 참조를 인수로 주었다.lambdaTest2()
의 경우exec.submit()
에서도 함수형 인터페이스인Runnable
을 받는데, 또 다른 함수형 인터페이스인Callable
도 받는다. 그래서 컴파일러는 어떤 것을 선택해야 할 지 알 수 없어진다.
즉, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안 된다.
다중 정의의 함정 피하기 1: 인수 포워드하기
public boolean contentEquals(StringBuffer sb) {
return contentEquals((CharSequence)sb);
}
- 다중 정의로 2개 이상의 타입을 지원할 때, 위와 같이 명시적 캐스팅으로 인수 포워딩하여 정상동작을 유도할 수 있다.
String
클래스의 다중 정의 오류
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
public static String valueOf(char data[]) {
return new String(data);
}
- 같은 객체를 넘기더라도 전혀 다른 일을 수행한다.
- 이렇게할 이유가 없었으므로, 잘못 설계된 사례로 남아있다.
핵심 정리
- 다중 정의를 허용한다고 해서 남발하지 말자.
- 매개변수 수가 같은 다중 정의는 웬만하면 만들지 말자.
- 이래야만 한다면, 형변환하여 정확한 다중정의 메서드가 선택되도록 하자
반응형
'Java > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바, 쉽게 정리하기 - item 54. null 이 아닌, 빈 컬렉션이나 빈 배열을 반환하라 (0) | 2023.06.10 |
---|---|
이펙티브 자바, 쉽게 정리하기 - item 53. 가변 인수는 신중히 사용하라 (0) | 2023.06.09 |
이펙티브 자바, 쉽게 정리하기 - item 51. 메서드 시그니처를 신중히 설계하라 (0) | 2023.06.07 |
이펙티브 자바, 쉽게 정리하기 - item 50. 적시에 방어적 복사본을 만들라 (0) | 2023.06.07 |
이펙티브 자바, 쉽게 정리하기 - item 49. 매개변수가 유효한지 검사하라 (0) | 2023.06.02 |