하나의 참조, 여러 타입
다형성은 상속과 인터페이스가 주는 보상입니다. 부모(또는 인터페이스)로 타입이 지정된 하나의 변수가 어떤 하위 타입의 객체든 담을 수 있고, 그 위에서 메서드를 호출하면 자바는 변수의 선언된 타입이 암시하는 버전이 아니라 객체의 실제 클래스에 속한 버전을 실행한다는 뜻입니다.
상속 페이지에서 그 토대를 이미 보았습니다. 하위 클래스가 부모의 메서드를 오버라이드하는 것이죠. 다형성은 그 오버라이드를 가치 있게 만들어 줍니다. 일반적인 타입을 대상으로 코드를 작성하고, 각각의 구체적인 객체가 저마다의 방식으로 동작하게 해 주기 때문입니다.
a와 b 모두 Animal로 선언되었지만, 각각 자신의 소리를 출력합니다. 그 선택은 실제 객체에 따라 런타임에 이루어집니다.
동적 메서드 디스패치
그 뒤에 있는 메커니즘이 동적 메서드 디스패치(dynamic method dispatch)입니다. 오버라이드된 인스턴스 메서드의 경우, JVM은 어떤 구현을 호출할지 결정하기 위해 객체의 런타임 클래스를 봅니다. 컴파일러는 그 메서드가 선언된 타입에 존재하는지만 확인하고, 실제 선택은 프로그램이 실행될 때까지 미뤄집니다.
이것이 바로 하나의 반복문이 각각이 무엇인지 한 번도 묻지 않고도 온갖 타입의 혼합을 처리하게 해 주는 것입니다.
반복문은 Shape만 압니다. 나중에 Triangle extends Shape를 추가해도 이 코드는 그대로 동작합니다. 바로 그것이 핵심입니다. 코드는 구체적인 타입 목록이 아니라 추상에 의존합니다.
업캐스팅과 다운캐스팅
Dog를 Animal 변수에 저장하는 것은 업캐스팅입니다. 계층을 올라가 더 일반적인 타입으로 이동하는 것이죠. 모든 Dog는 Animal이므로(is a) 항상 안전하고 자바가 암시적으로 처리합니다.
반대 방향으로 가는 것은 다운캐스팅입니다. 부모 참조를 가져와 특정 하위 타입으로 다루는 것이죠. 이것은 객체가 정말로 그 하위 타입일 때만 유효하므로 캐스트를 명시적으로 써야 하고, 틀리면 ClassCastException의 위험을 감수해야 합니다.
마지막 캐스트는 문제없이 컴파일됩니다. 컴파일러는 그것이 틀렸음을 증명할 수 없기 때문이죠. 하지만 Cat은 Dog가 아니므로 실행 시 터집니다. 절대 짐작으로 다운캐스트하지 마세요.
instanceof로 다운캐스트 보호하기
다운캐스팅 전에 instanceof로 실제 타입을 확인하세요. 현대 자바는 그 결과를 같은 식 안에서 바인딩하게 해 주므로(instanceof의 패턴 매칭), 별도의 캐스트를 생략할 수 있습니다.
instanceof는 null에 대해 false를 반환하므로, 이 검사는 NullPointerException으로부터도 여러분을 지켜 줍니다. 다만, 긴 instanceof 사슬을 작성하고 있다면, 그 동작이 오버라이드된 메서드로서 클래스 안에 속해야 한다는 신호인 경우가 많습니다. 분기는 다형성에 맡기세요.
overriding 대 overloading
이 둘은 비슷하게 들리지만 서로 무관하며, 혼동하는 것은 전형적인 혼란의 원인입니다.
오버라이딩(overriding) 은 하위 클래스가 부모의 메서드를 동일한 시그니처로 대체하는 것입니다. 객체의 타입에 따라 런타임에 결정됩니다. 우리가 사용해 온 다형성이 바로 이것입니다.
오버로딩(overloading) 은 한 클래스가 같은 이름이지만 매개변수 목록이 다른 여러 메서드를 갖는 것입니다. 인자의 타입에 따라 컴파일 타임에 결정되며, 런타임 디스패치는 전혀 관여하지 않습니다.
컴파일러는 오직 인자의 정적 타입만으로 일치하는 describe를 고릅니다. 부모/자식 객체가 전혀 관여하지 않으므로, 이것은 런타임 다형성이 아닙니다. 단지 메서드 이름을 재사용할 뿐입니다.
흔한 함정: 필드는 다형적이지 않다
런타임에 디스패치되는 것은 인스턴스 메서드뿐입니다. 필드와 정적 메서드는 선언된 타입에 따라 결정되며, 이는 많은 사람을 헷갈리게 합니다.
p.name()은 Child의 버전을 실행하지만(다형성), p.label은 Parent의 필드를 읽습니다. 필드는 오버라이드되는 것이 아니라 숨겨지기 때문입니다. 해결책은 간단합니다. 필드를 private으로 유지하고 메서드를 통해서만 접근하면, 다형적 호출이 항상 이깁니다.
다음: 접근 제어자
다형성은 하위 클래스가 알맞은 멤버를 보고 오버라이드할 수 있으면서도, 코드의 나머지 부분이 안으로 손을 뻗어 불변식을 깨뜨릴 수 없을 때에만 깔끔하게 동작합니다. 그 균형은 public, protected, private, 그리고 패키지 전용 접근, 즉 접근 제어자로 제어됩니다. 다음에 그것을 다룹니다.
자주 묻는 질문
자바에서 다형성이란 무엇인가요?
다형성이란 하나의 참조 타입이 여러 다른 클래스의 객체를 가리킬 수 있고, 실제로 실행되는 메서드가 변수의 선언된 타입이 아니라 런타임의 객체 실제 타입에 따라 선택되는 것을 뜻합니다. 따라서 Shape shape 변수는 Circle이나 Square를 담을 수 있고, shape.area()를 호출하면 알맞은 버전이 자동으로 실행됩니다.
자바에서 overriding과 overloading의 차이는 무엇인가요?
오버라이딩(overriding) 은 하위 클래스가 상위 클래스의 메서드를 동일한 시그니처로 대체하는 것으로, 런타임 다형성을 이끄는 핵심입니다. 오버로딩(overloading) 은 한 클래스가 같은 이름이지만 매개변수 목록이 다른 여러 메서드를 갖는 것으로, 컴파일러가 인자를 바탕으로 컴파일 타임에 하나를 고릅니다. 오버라이딩은 객체의 타입에 따라 런타임에 결정되고, 오버로딩은 인자의 타입에 따라 컴파일 타임에 결정됩니다.
자바에서 upcasting과 downcasting의 차이는 무엇인가요?
업캐스팅은 자식 객체를 부모 타입으로 다루며(Animal a = new Dog();), 항상 안전하고 보통 암시적으로 이루어집니다. 다운캐스팅은 반대 방향으로(Dog d = (Dog) a;), 객체가 정말로 그 하위 타입일 때만 안전합니다. 그렇지 않으면 ClassCastException을 던집니다. 모든 다운캐스트는 먼저 instanceof로 보호하세요.