자바에서 배열은 공변적입니다. 즉, 서브타입(Subtype)은 슈퍼타입(Supertype) 배열로 대체될 수 있습니다. 하지만 이 공변성은 자주 런타임 오류를 유발할 수 있습니다.
Long[] a = new int[3]; // 컴파일 오류
위 코드에서 Long[]은 int[]과 공변적이지 않기 때문에 컴파일 오류가 발생합니다. 자바에서 기본형 배열은 공변성을 지원하지 않으며, 참조 타입에 대해서만 배열 공변성이 적용됩니다. 따라서 기본형 배열은 공변적이지 않다는 것을 기억해야 합니다.
반면, 제네릭에서는 타입 파라미터가 반공변적입니다. 제네릭 타입 파라미터는 상하위 클래스 관계가 성립하지 않기 때문에, 제네릭 타입은 반공변적입니다. 다음 예시를 통해 살펴볼 수 있습니다:
List<Integer> list = new ArrayList<>();
// List<Number> numbers = list; // 오류: Integer는 Number의 하위 클래스이지만 제네릭은 반공변적이기 때문에 불가능
이처럼, List는 List로 캐스팅할 수 없습니다. 이는 자바의 제네릭은 반공변적이라는 특징을 보여줍니다.
자바에서 다형성은 코드의 유연성과 재사용성을 높여주는 중요한 개념입니다. 다형성은 크게 세 가지로 나눌 수 있습니다:
Ad-hoc 다형성은 오버로딩(Overloading)과 오버라이딩(Overriding)을 통해 구현됩니다. 동일한 이름의 메서드가 매개변수의 타입이나 개수에 따라 다르게 동작할 수 있는 것을 의미합니다.
오버로딩 예시
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
}
오버라이딩 예시
class Animal {
public void sound() {
System.out.println("동물이 소리를 냅니다");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("멍멍");
}
}
서브타입 다형성은 상위 클래스 또는 인터페이스 타입의 변수가 하위 클래스의 인스턴스를 참조할 수 있는 기능입니다. 이는 상속과 인터페이스를 이용하여 구현됩니다.
Animal animal = new Dog(); // Animal 타입이지만 실제 인스턴스는 Dog
animal.sound(); // 출력: 멍멍
파라메트릭 다형성은 제네릭스(Generics)를 통해 구현됩니다. 제네릭을 사용하면, 특정 타입에 의존하지 않고 다양한 타입에 대해 동일한 코드를 사용할 수 있습니다.
List<Integer> intList = new ArrayList<>();
List<String> stringList = new ArrayList<>();
이처럼 제네릭을 사용하면 여러 타입에 대해 동일한 자료 구조와 메서드를 사용할 수 있어 코드의 유연성이 극대화됩니다.
업캐스팅(Upcasting): 하위 클래스의 객체를 상위 클래스 타입으로 캐스팅하는 것은 성능에 영향을 거의 미치지 않습니다. 예를 들어, Dog를 Animal로 캐스팅하는 경우가 업캐스팅입니다.
다운캐스팅(Downcasting): 상위 클래스의 객체를 하위 클래스 타입으로 캐스팅하는 것은 비용이 많이 듭니다. 런타임 시점에 객체가 실제로 하위 클래스의 인스턴스인지 확인하는 과정이 필요하기 때문입니다. 특히, 큰 컬렉션에서 반복적으로 다운캐스팅을 하면 성능 저하가 발생할 수 있습니다.
Animal animal = new Dog();
Dog dog = (Dog) animal; // 다운캐스팅
잘못된 타입으로 캐스팅을 시도하면 ClassCastException이 발생합니다. 이 오류는 런타임에 발생하며, 컴파일 타임에는 발견되지 않습니다.
Object obj = "Hello";
Integer num = (Integer) obj; // ClassCastException 발생
타입 안정성(Type Safety): 제네릭 타입을 사용하면서 원시 타입(raw type)을 피하면, 타입 안정성을 유지할 수 있습니다. 제네릭을 사용할 때는 가능한 한 제네릭 타입을 명시하여 안전하게 코드를 작성하는 것이 좋습니다.
유지 보수성(Maintainability): 다운캐스팅이 지나치게 많이 사용되면 코드가 복잡해지고, 유지 보수가 어려워질 수 있습니다. 따라서 가능한 인터페이스나 제네릭을 활용해 다운캐스팅을 줄이는 것이 좋습니다.
Q1: 제네릭에서 공변성을 적용할 수 있는 경우가 있을까요?
Q2: 다운캐스팅을 최소화하기 위한 자바 설계 패턴에는 어떤 것이 있을까요?
Q3: Ad-hoc 다형성과 서브타입 다형성의 차이를 구체적으로 어떤 상황에서 느낄 수 있을까요?
Q1:
제네릭에서는 직접적으로 공변성을 허용하지 않지만, 와일드카드를 통해 제한된 공변성을 표현할 수 있습니다. 예를 들어, List<? extends Number>를 사용하면 List나 List과 같은 서브타입 리스트를 처리할 수 있습니다.
Q2:
다운캐스팅을 최소화하기 위해 인터페이스를 적극적으로 사용하거나, 제네릭을 활용하는 설계 패턴을 적용할 수 있습니다. 예를 들어, 전략 패턴(Strategy Pattern)이나 템플릿 메서드 패턴(Template Method Pattern)은 다운캐스팅을 줄이고 상속을 보다 효과적으로 사용할 수 있도록 돕습니다.
Q3:
Ad-hoc 다형성은 컴파일 타임에 메서드 오버로딩을 통해 구현되며, 서브타입 다형성은 런타임 시점에 다형성을 제공합니다. 예를 들어, 같은 이름의 메서드를 다양한 타입으로 오버로딩하는 것은 Ad-hoc 다형성이고, 상위 클래스 변수로 다양한 하위 클래스 객체를 참조할 수 있는 것은 서브타입 다형성입니다.