kingsubin

아이템 52. 다중정의는 신중히 사용하라 본문

책/이펙티브자바

아이템 52. 다중정의는 신중히 사용하라

kingsubin 2021. 9. 16. 15:33

다중 정의 (Overroading)

// 52-1. BAD - 컬렉션 분류기
public class CollectionClassifier {
  public static String classify(Set<?> s) {
    return "Set";
  }
  public static String classify(List<?> l) {
    return "List";
  }
  public static String classify(Collection<?> c) {
    return "Collection";
  }

  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));
    }
  }
}

// Collection
// Collection
// Collection

Set, List, Collection 을 출력하길 기대하고 작성했지만 실제로는 Collection만 3번 출력한다.

왜냐면 다중정의 (overloading)된 세 classify 중 어느 메서드를 호출할지는 컴파일 타임에 정해지기 때문이다.

컴파일 타임에는 for 문 안의 c 는 항상 Collection<?> 타입이다.

런타임에는 매번 달라지지만, 호출할 메서드를 선택할때는 영향을 주지 못한다.

따라서 컴파일타임 기준으로 세 번째 메서드임 classify(Collection<?>) 만 호출하는 것이다.

오버로딩 메서드는 정적으로 선택한다.

// GOOD ?
public static String classify(Collection<?> c) {
  return c instanceof Set ? "Set" :
          c instanceof List ? "List" : "Collection";
}

52-1 의 코드를 사용하려면 이런식으로 명시적으로 검사해서 사용해야 한다.

재정의 (Override)

// 52-2. 재정의는 동적 선택
public class Item52 {
  static class Wine {
    String name() {
      return "포도주";
    }
  }

  static class SparklingWine extends Wine {
    @Override
    String name() {
      return "발포성 포도주";
    }
  }

  static class Champagne extends Wine {
    @Override
    String name() {
      return "샴페인";
    }
  }

  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());
    }
  }
}

// 포도주
// 발포성 포도주
// 샴페인

가장 하위에서 정의한 재정의 메서드가 실행된다.

오버라이딩 메서드는 동적으로 선택한다.

다중정의를 조심해서 사용하자.

  • 다중정의가 혼동을 일으키는 상황을 피하자.
  • 안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 말자.
  • 가변인수(varargs)를 사용하는 메서드라면 다중정의를 아예 하지말자. (Item53)
  • 다중정의 대신 메서드 이름을 다르게 지어주는 길도 항상 열려 있음을 잊지말자.

메서드에 다른 이름을 주는 방식

java.io.ObjectOutputStream 클래스를 보면 write 메서드를 다중 정의가 아닌 메서드에 다른 이름 주어주는 방법을 택했다.

// ObjectOutputStream 
public void writeBoolean(boolean val) throws IOException {
  bout.writeBoolean(val);
}   
public void writeByte(int val) throws IOException  {
  bout.writeByte(val);
}
public void writeShort(int val)  throws IOException {
  bout.writeShort(val);
}
public void writeChar(int val)  throws IOException {
  bout.writeChar(val);
}

...

이런식으로 이름을 다르게 지어줬는데 이 방식이 다중정의보다 나은점은 read 메서드의 이름과 짝을 맞추기 좋다는 것이다. 예를 들어 readBoolean(), readInt(), readLong() 같은 식이다.

생성자 다중정의

한편, 생성자는 이름을 다르게 지을 수 없으니 두 번째 생성자부터는 무조건 다중정의가 된다.

하지만 정적 팩터리라는 대안을 사용할 수 있다.

그래도 어쩔수 없이 같은 수의 매개변수를 받아야 하는 경우가 있을 수 있는데 그 때를 대비해서 안전대책을 배워보자.

 

다중정의 메서드가 많더라도, 그중 어느 것이 주어진 매개변수 집합을 처리할지가 명확히 구분된다면 헷갈릴 일은 없을 것이다. 즉, 매개변수 중 하나 이상이 "근본적으로 다르다"면 헷갈릴 일이 없다.

여기서 근본적으로 다르다라는 말은 두 타입의 값을 서로 어느 쪽으로든 형변환 할 수 없다는 뜻이다.

이 조건만 충족하면 어느 다중정의 메서드를 호출할지가 매개변수의 런타임 타입만으로 결정된다.

 

근본적으로 다르다의 예시) ArrayList의 인자가 1개인 생성자는 ArrayList(int initialCapacity)와 ArrayList(Collection<? extends E> c)가 있지만 int와 Collection은 근본적으로 다르므로 괜찮다

List 인터페이스에서의 다중정의

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 까지 정수를 넣고, 0 ~ 2를 제거해 [-3, -2, -1] 가 출력되길 기대했지만 다른 값이 나온다.

Set 에서는 실제 정수 [0, 1, 2] 를 제거했고 List 에서는 인덱스를 제거 했다.

 

set.remove(i) 는 remove(Object) 이다.

다중정의된 다른 메서드가 없으니 기대한 대로 동작하여 [0, 1, 2] 제거했다.

한편, list.remove(i)는 다중정의된 remove(Object o), remove(int index) 중에서 remove(int index)를 선택해서 이런 결과가 나온 것이다.

list.remove(Integer.valueOf(i));

list.remove 부분에서의 인자를 Integer 로 바꿔서 remove(Object o)가 호출되게끔 하면 기대한대로 동작한다.

 

제네릭이 도입되기 전인 자바 4 까지는 List에서 Obejct와 int 가 근본적으로 달라서 문제가 없었지만 제네릭과 오토박싱이 등장하면서 매개변수 타입이 더는 근본적으로 다르지 않게 되었다.

정리하자면, 제네릭과 오토박싱을 더한 결과 List 인터페이스가 취약해졌다.

결론

  • 다중정의 사용을 조심하자.
  • 매개변수 수가 같을때는 다중정의를 피하자.
  • 헷갈릴 만한 매개변수는 형변환하여 정확한 다중정의 메서드가 선택되도록 하자.
 
0 Comments
댓글쓰기 폼