다중정의(overloading)
: 같은 이름의 메소드를 중복하여 정의하는 것을 말한다.
어느 메서드를 호출할 지 컴파일타임에 정해진다. → 정적으로 메서드 선택
즉, 객체의 런타임 타입은 전혀 중요하지 않다.
오직 매개변수의 컴파일타임 타입에 의해 이뤄진다.
재정의(overriding)
: 상위 클래스가 정의한 것과 똑같은 시그니처의 메서드를 하위 클래스에서 다시 정의한 것을 말한다.
런타임 타입에 따라 어떤 메서드를 호출할지 정해진다. → 동적으로 메서드 선택
public class Collectionclassifier {
public static String classify(Set<?> s) {
return "집합";
}
public static String classify(List<?> 1st) {
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 메서드를 하나로 합친 후 instanceof로 명시적으로 검사하면 해결된다.
public static String classify(Collection<?> c) {
return c instanceof Set ? "집합" :
c instanceof List ? "리스트" : "그 외";
}
위처럼 헷갈릴 수 있는 코드는 작성하지 않는 게 좋다!
특히 공개 API라면 더욱 신경 써야 한다.
API 사용자가 매개변수를 넘기면서 어떤 다중정의 메서드가 호출될지를 모른다면 프로그램이 오동작하기 쉽다.
또한 가변인수(varargs)를 사용하는 메서드라면 다중정의를 아예 하지 말아야 한다.
안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 말자.
다중정의하는 대신 메서드 이름을 다르게 지어주자.
한편, 생성자는 이름을 다르게 지을 수 없으니 두 번째 생성자부터는 무조건 다중정의가 된다.
매개변수 중 하나 이상이 "근본적으로 다르다(radically different)"면 헷갈릴 일이 없다!
즉, 두 타입의 (null이 아닌) 값을 서로 어느 쪽으로든 형변환할 수 없다는 뜻이다.
위 조건만 충족하면 어느 다중정의 메서드를 호출할지가 매개변수들의 런타임 타입만으로 결정된다.
ex) ArrayList에는 int를 받는 생성자와 Collection을 받는 생성자가 있는데, 어떤 상황에서든 두 생성자 중 어느 것이 호출될 지 헷갈릴 일은 없을 것이다.
자바 4까지는 모든 기본 타입이 모든 참조 타입과 근본적으로 달랐지만,
자바 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. removed);
list.remove(i);
}
System.out.println(set + " " + list);
}
}
→ “[-3, -2, -1] [-3, -2, -1]"이 아닌 "[-3, -2, -1] [-2, 0, 2]"를 출력한다.
set.remove(i)의 시그니처는 remove(Object)이다.
한편, list.remove(i)는 remove(int index)를 선택하면서 문제가 발생한 것이다.
이는 밑의 코드로 list.remove의 인수를 Integer로 형변환하여 올바른 다중정의 메서드를 선택하게 하면 해결된다.
for (int i = 0; i < 3; i++) {
set. remove(i);
list.remove((Integer) i); // 혹은 remove(Integer.valueOf(i))
}
제네릭이 도입되기 전인 자바 4까지의 List에서는 Object와 int가 근본적으로 달라서 문제가 없었다.
하지만 제네릭과 오토박싱이 등장하면서 더는 근본적으로 다르지 않게 되었다.
// 1번. Thread의 생성자 호출
new Thread(System.out::printIn).start();
// 2번. ExecutorService의 submit 메서드 호출
ExecutorService exec = Executors.newCachedThreadPool();
exec. submit(System. out:: printIn);
다중정의된 메서드(혹은 생성자)들이 함수형 인터페이스를 인수로 받을 때,
서로 다른 함수형 인터페이스라도 인수 위치가 같으면 혼란이 생긴다.
따라서 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안된다!
프로그래밍 언어가 다중정의를 허용한다고 해서 다중정의를 꼭 활용하란 뜻은 아니다. 일반적으로 매개변수 수가 같을 때는 다중정의를 피하는 게 좋다. 상황에 따라, 특히 생성자라면 이 조언을 따르기가 불가능할 수 있다. 그럴 때는 헷갈릴 만한 매개변수는 형변환하여 정확한 다중정의 메서드가 선택되도록 해야 한다. 이것이 불가능하면, 예컨대 기존 클래스를 수정해 새로운 인터페이스를 구현해야 할 때는 같은 객체를 입력받는 다중정의 메서드들이 모두 동일하게 동작하도록 만들어야 한다. 그렇지 못하면 프로그래머 들은 다중정의된 메서드나 생성자를 효과적으로 사용하지 못할 것이고, 의도대로 동작 하지 않는 이유를 이해하지도 못할 것이다.
'Effective Java' 카테고리의 다른 글
[Effective Java] 아이템 54 - null이 아닌, 빈 컬렉션이나 배열을 반환하라 (0) | 2022.05.04 |
---|---|
[Effective Java] 아이템 53 - 가변인수는 신중히 사용하라 (0) | 2022.05.04 |
[Effective Java] 아이템 51 - 메서드 시그니처를 신중히 설계하라 (0) | 2022.04.05 |
[Effective Java] 아이템 50 - 적시에 방어적 복사본을 만들라 (0) | 2022.04.05 |
[Effective Java] 아이템 49 - 매개변수가 유효한지 검사하라 (0) | 2022.04.05 |