본문 바로가기
Java

오버로딩vs오버라이딩(면접단골질문)

by ybs 2021. 1. 22.
반응형

뻔한 질문이지만 한걸음 더 들어가서 보면 배열과 제네릭까지 묶어서 같이 이야기를 풀어나갈 수 있다. 예제 코드 및 설명은 이펙티브 자바를 많이 참고했다. 먼저 아래의 코드를 살펴보자.


이 코드의 목적은 컬렉션을 종류별로(집합이냐, 리스트냐, 아니면 다른 종류의 컬렉션이냐) 분류하는 것이다.

// 의도한 결과를 내지 않는 분류기
public class CollectionClassifier {
    public static String classify(Set<?> s) {
        return "Set";
    }

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

    public static String classify(Collection<?> lst) {
        return "Unknown 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));
    }
}

이 프로그램은 Set, List, Unkwon Collection 을 순서대로 출력하지 않고 Unknown Collection 만 세 번 출력한다.

 

그 이유는 classify 메서드가 오버로딩 되어 있으며, 오버로딩된 메서드 가운데 어떤 것이 호출될지는 컴파일 타임에 결정되기 때문이다.

 

루프가 세 번 도는 동안, 인자의 컴파일 타임 타입은 전부 Collection<?> (매개변수의 컴파일타임 타입)으로 동일하다.

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

그래서 항상 classify(Collection<?> lst) 메서드만 호출된다(static dispatch 라고도 부른다).

 

다음 아래 코드를 살펴보자.

class Wine {
    String name() { return "wine";}
}

class SparklingWine extends Wine {
    @Override String name() { return "sparklingWine"; }
}

class Champagne extends SparklingWine {
    @Override String name() { return "champagne"; }
}

public class WineTest {
    public static void main(String[] args) {
        Wine[] wines = {
            new Wine(), new SparklingWine(), new Champagne()
        };

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

name 메서드는 Wine 클래스에 선언되어 있고, sparklingWine 클래스와 Champagne 클래스는 name 메서드를 오버라이딩한다.
이번에는 wine, sparklingWine, champagne 이 순서대로 출력된다.

main 메서드에서 순환문의 매 루프마다 객체의 컴파일 타임 타입은 항상 Wine 이었는데도 말이다.

 

그 이유는 name 메서드가 오버라이딩 되어 있으며, 오버라이딩된 메서드 가운데 어떤 것이 호출될지는 런타임에 결정되기 때문이다.

 

오버라이딩 메서드 가운데 하나를 선택 할 때 객체의 컴파일 타임 타입은 영향을 주지 못한다.

런타임에 타입이 매번 달라지므로 순서대로 name 메서드가 호출됐다(dynamic dispatch 라고도 부른다).

 

결국 컴파일 타임과 런타임의 타입이 다른게 핵심이다. 이걸 가지고 배열과 제네릭도 살펴보자.

 

배열은 공변(covariant)이다. Sub 가 Super 의 하위 타입이라면 Sub[]Super[]의 하위 타입이라는 것이다.
반면 제네릭은 불변 타입이다. Type1과 Type2가 있을 때, List<Type1>List<Type2>의 상위 타입이나 하위 타입이 될 수 없다.

 

아래의 코드를 보자.

// 런타임에 ArrayStoreException 예외 발생
Object[] objectArray = new Long[1];
objectArray[0] = "hello";

// 컴파일 되지 않는 코드
List<Object> ol = new ArrayList<Long>(); // 타입 불일치
ol.add("hello");

어느쪽이든 Long용 저장소에 String을 넣을 수 없지만 이 실수를 배열에서는 런타임에 알 수 있고 제네릭은 컴파일 타임에 알 수 있다.

 

배열의 각 원소 타입은 런타임에 결정된다. 그래서 컴파일 타임에 타입 안정성을 보장하지 못하고 런타임에 ArrayStoreException 이 발생한다. 반면 제네릭은 타입 정보가 런타임에는 없어진다(erasure). 즉, 타입과 관계된 조건들은 컴파일 타임에만 검사하고 런타임에는 알 수 없게된다.


왜 제네릭을 이렇게 만들었는지 궁금할 수 있는데 하위호환성 때문이다. 자바5에 새롭게 생긴 제네릭이 기존 코드와 호환성을 맞추기 위해 생긴 메커니즘이다.

// 주의) 새 코드에는 무인자 제네릭 타입을 사용하면 안된다.
List list = new ArrayList<Object>();

 

 

참고 : 이펙티브 자바

반응형

'Java' 카테고리의 다른 글

Singleton(면접단골질문)  (0) 2021.05.09
Optional 형태가 가지는 의미(Monad)  (0) 2021.05.06
reflection 사용해서 api 문서화  (0) 2021.05.02
method return에 대한 고민  (2) 2021.02.07
Finalizer attack  (3) 2021.01.17