ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 아이템 52. 다중정의는 신중히 사용하라
    책/이펙티브자바 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 인터페이스가 취약해졌다.

    결론

    • 다중정의 사용을 조심하자.
    • 매개변수 수가 같을때는 다중정의를 피하자.
    • 헷갈릴 만한 매개변수는 형변환하여 정확한 다중정의 메서드가 선택되도록 하자.
     
킹수빈닷컴