티스토리 뷰

728x90
반응형

다중정의는 신중하게 사용하라


  • 다중정의란 이름이 같은 메서드가 매개변수 타입이나 개수만 다르게 갖는 형태를 다중정의(오버로딩)이라 합니다.

 

💡 다중 정의 메서드(Overload)

  • 예상으로는 Set, List등 매개변수에 맞춰서 결과값이 출력될거라 생각했지만, 실제로 수행해보면 Collection 매개변수 메서드만 실행이 됩니다. 이유는 다중정의된 메서드 중 어떠한 메서드를 호출할지는 컴파일타임에 정해지기 때문입니다. 즉 현재 컴파일 시점으로 보면 for(Collection<?> c : collections)로 Collection 타입이기 때문에 Collection 매개변수를 갖는 메서드가 실행되었습니다.
  • 런타임에는 타입이 매번 달라지겠지만 호출할 메서드를 선택하는데는 아무런 영향을 미치지 못합니다. 따라서 컴파일 시점에 이미 실행될 메서드가 지정되어 있으므로 3번 연속으로 Collection 매개변수가 호출됩니다.
  • 이처럼 직관과 어긋나는 이유는 재정의한 메서드는 동적(런타임)으로 선택되고, 다중정의한 메서드는 정적(컴파일)으로 선택되기 때문입니다.
  • 다중정의된 메서드 사이에서는 객체의 런타임 타입은 전혀 중요치 않습니다. 선택은 컴파일타임에, 오직 매개변수의 컴파일타임 타입에 의해 이루어집니다.
public class CollectionClassifier {

    public static String method(Object o) {
        return "Object 메서드";
    }

    public static String method(Set<?> set) {
        return "Set 매개변수 메서드";
    }

    public static String method(List<?> list) {
        return "List 매개변수 메서드";
    }

    public static String method(Collection<?> collection) {
        return "Collection 매개변수 메서드";
    }
}

public class Example {

    public static void main(String[] args) {
        Collection<?>[] collections = {
                new HashSet<String>(),
                new ArrayList<Integer>(),
                new HashMap<String, String>().values()
        };

        for (Collection<?> c: collections) {
            System.out.println(CollectionClassifier.method(c));
        }
    }
}

 

💡 재정의(Override)

  • 아래 예제를 실행하면 포도주, 발포성 포도주, 샴페인이 차례대로 출력됩니다. for문에서 컴파일타임 타입이 모두 Wine이지만 상속을 통해 재정의한 메서드가 실행이 됩니다.
public class Wine {

    String getName() {
        return "포도주";
    }
}

class SparklingWine extends Wine {

    @Override
    String getName() {
        return "발포성 포도주";
    }
}

class Champagne extends SparklingWine {

    @Override
    String getName() {
        return "샴페인";
    }
}

public class Example {

    public static void main(String[] args) {
        List<Wine> wines = List.of(
                new Wine(), new SparklingWine(), new Champagne()
        );

        for (Wine wine : wines) {
            System.out.println(wine.getName());
        }
    }
}

 

💡 다중정의가 일으킬 수 있는 혼동을 피하자

  • 프로그래머에게는 재정의가 정상적인 동작방식이고, 다중정의가 예외적인 동작으로 보일 것입니다. 즉 재정의한 메서드는 프로그래머가 기대한 대로 동작하지만 다중정의한 메서드는 이러한 기대를 가볍게 무시합니다. 그렇기 때문에 다중정의가 혼동을 일으키는 상황을 피해야 합니다.
  • 안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 말아야 합니다.
  • 다중정의하는 대신 메서드 이름을 다르게 지어주는 길도 항상 열려있습니다.

 

💡 다중정의가 필요하다면?

  • 메서드 이름을 다르게 지어 메서드명을 통해 명시적으로 클라이언트에게 노출하는 방법이 있습니다.
  • ObjectOutputStream 클래스의 writeXXX 메서드를 예로 들 수 있습니다.
  • 모두 같은 매개변수 개수를 가집니다. 하지만 다중정의가 아닌 네이밍을 통해 메서드의 동작을 예상할 수 있게해줍니다.
public class ObjectOutputStream extends OutputStream implements ObjectOutput, ObjectStreamConstants {
    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);
    }
    
    // ... 
}

 

💡 안전한 다중정의 

  • 매개변수 수가 같은 다중정의 메서드가 많더라도, 그중 어느것이 주어진 매개변수 집합을 처리할지가 명확하게 구분된다면 헷갈릴 일이 없을 것입니다. 즉, 매개변수 중 하나 이상의 값이 근본적으로 다르다면 헷갈릴 일이 없습니다. 근본적으로 다르다는 건 두 타입의 값을 서로 어느쪽으로든 형변환할 수 없다는 의미입니다.
  • 이 조건만 충족하면 어느 다중정의 메서드를 호출할지가 매개변수둘의 런타임 타입만으로 결정됩니다. 따라서 컴파일시점에 영향을 받지 않게되고 혼란을 줄일 수 있습니다.
  • 예를들어 ArrayList에는 int를 받는 생성자와 Collection을 받는 생성자가 있는데, 어떤 상황에서든 두 생성자 중 어느것이 호출될지 헷갈릴 일은 없습니다.
List<String> strings1 = new ArrayList<>(20);

Set<String> set = new HashSet<>();
set.add("Hello");

List<String> strings2 = new ArrayList<>(set);

 

  • ArrayList 클래스의 생성자
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
    public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            elementData = EMPTY_ELEMENTDATA;
        }
    }
}

 

💡 Autoboxing의 등장으로 인한 주의사항

  • 자바 5에 등장한 오토박싱으로 인한 문제가 발생할 여지가 생겼습니다.
  • 다음은 List 인터페이스의 remove API입니다.
public interface List<E> extends Collection<E> {

    boolean remove(Object o);

    E remove(int index);
}

 

  • List 인터페이스에 remove 메서드가 다중정의되어 있는데 아래 예제 코드에서 remove 메서드를 호출하면 어떻게 될까요?
  • 제가 지우고자하는 값은 1입니다. 하지만 반환값은 [1, 3, 4, 5]를 반환하고 있습니다.
    그 이유는 List 인터페이스 중 E remove(int index) 메서드가 선택되었기 때문입니다. 이 메서드를 사용하면 매개변수를 인덱스로 사용하기 때문에 1이 아닌 2가 삭제된 것입니다.
public class Example {

    public static void main(String[] args) {

        List<Integer> integers = new ArrayList<>(List.of(1, 2, 3, 4, 5));
        integers.remove(1);
        System.out.println(integers); // [1, 3, 4, 5]
    }
}

 

  • 아래처럼 해야 의도하는 값을 얻을 수 있습니다.
public class Example {

    public static void main(String[] args) {

        List<Integer> integers = new ArrayList<>(List.of(1, 2, 3, 4, 5));
        integers.remove((Integer) 1);
        System.out.println(integers); // [2, 3, 4, 5]
    }
}

 

✔️ 정리

  • 다중정의(오버로딩)된 메서드 중 어떤 메서드가 호출될지는 컴파일 시점에 정해집니다.
  • 재정의(오버라이딩)한 메서드는 동적(런타임)으로 선택되고, 다중정의한 메서드는 정적(컴파일)으로 선택됩니다.
  • 안전하고 보수적으로 갈려면 매개변수 수가 같은 다중정의는 만들지 말아야 합니다.
  • 매개변수가 하나 이상 근본적으로 다른 경우라면 혼동되지 않을 수 있습니다. (ArrayList 클래스의 생성자)
  • 매개변수로 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안됩니다.
    • 참조 메서드와 호출 메서드가 모두 다중정의 되어있을 경우 다중정의 해소 알고리즘이 제대로 동작하지 않을 수 있습니다.

 

 

 

728x90
반응형