52. Overloading은 신중히 사용하라

연어는결국강으로·2023년 3월 21일
0

이펙티브자바

목록 보기
7/7

1. 안좋은 예시

  • 다음은 컬렉션을 집합, 리스트, 그 외로 구분하고자 만든 프로그램이다.
public class CollectionClassifier {

    public static String classify(Set<?> set) {
        return "Set";
    }

    public static String classify(List<?> list) {
        return "List";
    }

    public static String classify(Collection<?> collection) {
        return "Others";
    }
}
public static void main(String[] args) {
    Collection<?>[] collections = {
            new HashSet<String>(),
            new ArrayList<Long>(),
            new HashMap<Long, String>().values()
    };

    Arrays.stream(collections).forEach(c -> System.out.println(classify(c)));
}

// 실행결과는 모두 Others 출력

Others만 세 번 연달아 출력하는 이유는 아래와 같다.

  1. overloading된 세 classify 메서드 중 어떤 걸 호출할 지 컴파일타임에 정해지기 때문이다.
  2. 컴파일 타임에는 for 문 안의 c는 항상 Collection<?> 타입이다.
  3. 런타임에는 타입이 매번 달라지지만, 호출할 메서드를 선택하는 데는 영향을 주지 못한다.
  4. 따라서 컴파일타임의 매개변수 타입을 기준으로 세 번째 classify만 호출한다.

다중정의된 메서드 사이에서는 객체의 런타임 타입은 전혀 중요치 않다. 선택은 컴파일 타임에, 오직 매개변수의 컴파일타임 타입에 의해 이뤄진다. 위의 문제는 (정적 메서드를 사용해도 좋다면) 아래와 같이 해결할 수 있다.

// 모든 classify 메서드를 하나로 합친 후 
// instanceof로 명시적으로 검사하면 말끔히 해결된다.
public static String classify(Collection<?> c) {
        return c instanceof Set ? "Set" :
                    c instanceof List ? "List" : "Others";
    }
  • 프로그래머에게는 재정의가 정상적인 동작 방식이고, 다중정의가 예외적인 동작으로 보일 것이다. 그러니 다중정의가 혼동을 일으키는 상황을 피해야한다.
  • 정확히 어떻게 사용했을 때 다중정의가 혼란을 주느냐에 대해서는 논란의 여지가 있다. 안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 말자.
  • varargs(가변인수)를 사용하는 메서드라면 다중정의를 아예 사용하지 말아야 한다.(예외는 Item 53) 다중정의하는 대신 메서드 이름을 다르게 지어주는 길도 항상 열려있으니 말이다.

2. 좋은 예시

이번에는 ObjectOutputStream 클래스를 살펴보자. 이 클래스는 write를 overloading 할만도한데, 모든 메서드에 다른 이름을 지었다. 이 방식이 overloading보다 좋은 다른 점은 read 메서드의 이름과 짝을 맞추기 좋다는 것이다.

3. 생성자의 Overloading - 특히 같은 수의 매개변수

  • 한편 생성자는 두 번째부터는 overloading이 된다. 하지만 정적 팩터리를 활용할 수 있는 경우가 많다. 또한 생성자는 재정의할 수 없어서 overriding과 overloading이 혼용되지 안는다.
  • 매개변수 수가 같은 다중정의 메서드가 많더라도, 그중 어느 것이 주어진 매개변수 집합을 처리할지가 명확히 구분된다면 헷갈릴 일은 없을 것이다.
  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++) {
            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] list.remove(i) 의 i가 index이기 때문 (Integer) i 로 형변환하면 기대한대로 출력됨

이 예가 혼란스러웠던 이유는 List 인터페이스가 remove(Object)와 remove(int)를 overloading했기 때문이다. 즉, 자바 언어에 제네릭과 오토박싱을 더한 결과 List 인터페이스가 취약해졌다.(자바 4 까지는 Integer와 int가 다르게 취급됨 - 자바5부터 오토박싱이 도입된 결과)

  1. 두 번째 예시 - 자바 8에서 도입한 람다와 메서드 참조
// 1번. Thread의 생성자 호출
new Thread(System.out::println).start();

// 2번. ExecutorService의 submit 메서드 호출
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);

2번의 경우 컴파일 오류가 난다. 원인은 submit의 overloading 메서드 중에는 Callable를 받는 메서드도 있다는 데 있다. 하지만 모든 println이 void를 반환하니, 반환값이 있는 Callable과 헷갈릴 리는 없다고 생각할지도 모르겠다. 합리적인 추론이지만, overloading resolution(overloading 메서드 찾는 알고리즘)은 이렇게 동작하지 않는다. 만약 println이 overloading 없이 단 하나만 존재했다면 submit() 호출이 제대로 컴파일됐을 것이다. 지금은 참조된 메서드(println)와 호출한 메서드(submit) 양쪽 다 overloading되어서 기대처럼 작동하지 않는 상황이다.

  • 원인 기술적으로 말하면 System.out::println은 부정확한 메서드 참조(inexact method reference)다. 또한 암시적 타입 람다식(implicitly typed lamda expression)이나 부정확한 메서드 참조 같은 인수 표현식은 목표 타입이 선택되기 전에는 그 의미가 정해지지 않기 떄문에 적용성 테스트(applicability test) 때 무시된다. 이것이 문제의 원인이다. - 컴파일러 제작자를 위한 설명이니 이해되지 않더라도 그냥 넘어가자.

원인의 핵심은 다중정의된 메서드(혹은 생성자)들이 함수형 인터페이스를 인수로 받을 때, 비록 서로 다른 함수형 인터페이스라도 인수 위치가 같으면 혼란이 생긴다는 것이다. 따라서 메서드를 다중정의할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안된다.

  • Object 외의 클래스 타입과 배열 타입은 근본적으로 다르다.
  • Serializable 과 Cloneable 외의 인터페이스 타입과 배열 타입도 근본적으로 다르다.
  • 관련 없는 클래스들끼리도 근본적으로 다르다.
    • String과 Throwable처럼 상/하위 관계가 아닌 두 클래스는 ‘unrelated(관련 없다)’라고 한다.
    • 그리고 어떤 객체도 관련 없는 두 클래스의 공통 인스턴스가 될 수 없다.
    • 어렵게 말했는데 쉽게 말하면 List가 input일 때 ArrayList이런건 들어갈 수 있지만 String이 들어가진 않는다 이 말입니다.

4. 예외

  • String은 자바 4 시절부터 contentEquals(StringBuffer) 메서드를 가지고 있었다.
  • 그런데 자바 5부터 StringBuffer, StringBuilder, CharBuffer, String 등의 비슷한 부류의 타입을 위한 공통 인터페이스로 CharSequence가 등장하였고, String에도 CharSequence를 받은 contentEquals가 다중정의되었다.
  • 다행히 이 두 메서드는 같은 객체를 입력하면 완전히 같은 작업을 수행해주니 해로울 건 전혀 없다. - 기능이 똑같다면 신경 쓸 게 없다.
  • 이렇게 하는 가장 일반적인 방법은 상대적으로 더 특수한 overloading 메서드에서 덜 특수한 overloading 메서드로 일을 넘겨버리는(forward) 것이다.

5. 실패한 예

자바 라이브러리는 이번 아이템의 정신을 지켜내려 애쓰고 있지만, 실패한 클래스도 몇 개 있다. 예컨대 String의 valueOf(char[])과 valueOf(Object)는 같은 객체를 건네더라도 전혀 다른 일을 수행한다.

0개의 댓글