item 52. 다중 정의는 신중히 사용해라
다중 정의는 모두 아시죠? 다른 말로는 오버로딩이라는 친숙한 이름을 갖고있다.
오버로딩이 뭐였더라?
서로 다른 시그니처를 갖는 여러 메소드들이 같은 이름으로 정의되는 것 이다.
즉, 메서드명은 같지만 매개변수의 개수, 타입, 순서들이 다른 메서드들을 의미한다.
다중 정의된 메소드는 어느 메서드를 호출할 지가 컴파일 타임에 정해진다.
public class item52 {
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(item52.classify(c));
}
}
public static String classify(Set<?> s) {
return "집합";
}
public static String classify(List<?> lst) {
return "리스트";
}
public static String classify(Collection<?> c) {
return "그 외";
}
}
/*
그 외
그 외
그 외
*/
??
Set, List, Map을 출력했는데 '그 외'만 3번 출력된다.
이 이상한 현상이 바로 '오버로딩 에서는 어느 메서드를 호출할 지가 컴파일 타임에 정해지기 때문'에 생기는 일이다.
컴파일타임에서 c는 항상 Collection<?>타입이다.
런타임에서야 매번 타입이 달라지지만, 메서드를 선택할 때는 영향을 주지 못한다.
따라서 컴파일 타임 매개변수를 기준으로 '그 외'만 선택되는 것이다.
재정의(오버라이딩)한 메소드는 동적으로 선택되고, 다중정의(오버로딩)한 메서드는 정적으로 선택된다.
만약 너가 재정의를 했다면 런타임의 타입을 기준으로 메서드가 호출되었겠지만, 메서드 재정의란 상위 클래스가 정의한 것과 똑같은 시그니처의 메서드를 하위 클래스에서 재정의한 것인데, 아니지 않느냐?? 그니까 컴파일 타입으로 호출된 것이다.
public class item52 {
public static void main(String[] args) {
List<Soju> sojuList = List.of(new Soju(), new LittleSoju(), new BigSoju());
for(Soju alcohol : sojuList){
System.out.println(alcohol.name());
}
}
public static class Soju{
String name(){
return "참이슬";
}
}
public static class LittleSoju extends Soju{
@Override
String name() {
return "이슬톡톡";
}
}
public static class BigSoju extends Soju{
@Override
String name() {
return "빨간뚜껑";
}
}
}
/*
참이슬
이슬톡톡
빨간뚜껑
*/
한번 재정의를 해봤는데, 다 잘 나온다.
for문에서의 컴파일 타임 타입이 모두 소주지만 '가장 하위에서 정의한' 재정의 메서드가 실행되기 때문에, 각 이름들이 잘 나온다.
다중정의된 메서드 사이에서는 객체의 런타임 타입은 전혀 중요하지 않다. 선택은 컴파일 타임에 오직 매개변수의 컴파일 타임 타입에 의해 이루어진다.
그럼 위에 classify가 잘 나오도록 하려면 어째야할까?
//변경 전
public static String classify(Set<?> s) {
return "집합";
}
public static String classify(List<?> lst) {
return "리스트";
}
public static String classify(Collection<?> c) {
return "그 외";
}
//변경 후
public static String classify(Collection<?> c) {
return c instanceof Set ? "집합" :
c instanceof List ? "리스트" : "그 외";
}
이런식으로 classify method를 하나로 합친 후 instanceof로 명시적으로 검사하면 된다.
근데 사실 이렇게 헷갈릴 수 있는 코드는 작성하지 말아라.
몇 가지 대안을 소개하겠습니다.~~
다중 정의가 혼동을 일으킬 수 있는 상황은 피하는게 좋다.
-> 그러나 생성자는 이름을 다르게 지을 수가 없으니, 두 번째 생성자부터는 무조건 다중 정의가 되는 경우가 많다.
이런 경우에는 '정적 팩터리'라는 대안을 사용할 수 있다. 또한, 생성자는 재정의가 안되니까 다중정의랑 재정의를 헷갈릴 필요도 없다.
안전하고 보수적으로 가려면 매개변수 수가 같은 다중 정의는 안만드는 것이 좋다.
-> 다중정의 하는 대신, 메서드 이름을 다르게 만들어 주면 되잖아??!!
만약, 매개변수 개수가 같은 오버로딩을 피할 수 없다면?
-> 매개변수 중 하나 이상이 서로 어느쪽으로든 형변환 할 수 없으면, 그나마 괜찮다. 그냥 오버로딩 해라.
ex) ArrayList의 인자가 1개인 생성자는 ArrayList(int initialCapacity)와 ArrayList(Collection<? extends E> c)가 있지만 int와 Collection은 근본적으로 다르므로 괜찮다.
하지만 매개변수가 근본적으로 달라도 항상 안전한 것은 아니다.
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
list.remove(3); //Object 3인지? index 3인지? 내가 어케암?
- 혼란스럽다.
- List<E>인터페이스가 remove(int index) 와 remove(Object o) 를 오버로딩했기 때문이다.
- 자바 4까지는 모든 기본 타입이 모든 참조 타입과 근본적으로 달랐지만, 자바 5부터는 제네릭과 오토박싱이 도입되면서, 기본타입과 참조 타입이 더는 근본적으로 다르지 않게 되었다.
- 제네릭과 오토박싱이 등장하면서 int를 Integer로 자동 변환 해주니까, 이 둘은 근본적으로 달라지지 않게 된 것이다.
오버로딩할때 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안된다.
new Thread(System.out::println).start(); //컴파일 성공
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println); //컴파일 에러
- 넘겨진 인수는 모두 System.out::println 로 똑같고, 둘 다 Runnable을 받는 형제 메서드를 오버로딩 하고 있다.
- 자바 8에서 도입된 람다와 메서드 참조가 다중 정의시의 혼란을 키웠다.
- 서로 다른 함수형 인터페이스라도 인수 위치가 같으면 혼란이 발생한다. (사실 함수형 인터페이스들은 근본적으로 같다.)
- 따라서 같은 위치 인수로 받지 말자.
핵심 정리
자바의 특징 중 다형성은 오버로딩과 오버라이딩이다.
그러나 이말이 다중정의를 꼭 활용하라는 뜻은 아니다.
일반적으로 매개변수 수가 같을 때는 다중 정의를 피하자.
생성자의 경우 이 조언을 따르기 힘든 경우가 있으므로, 이럴 때는 헷갈릴만한 매개변수를 형변환해 정확한 다중정의 메서드가 선택되도록 하자.
파이팅!