이전의 포스팅에서 자바 제네릭의 기초와 간단한 사용방법에 대해 알아 봤습니다. 이번 포스팅에서는, 자바5에 제네릭과 함께 추가된 와일드 카드에 대해 다뤄 보겠습니다.
제네릭의 등장 이전, 자바는 컬렉션에서 내부적으로 객체들을 Object로 관리하고 있었습니다. Object를 사용함으로써 하나의 컬렉션 안에 여러 객체들을 저장할 수 있는 유연성이 있었지만, 이와 동시에 타입 안정성을 떨어뜨려 런타임 에러가 빈번히 발생하는 문제가 있었습니다.
제네릭이 등장하며 이런 런타임 에러를 미연에 방지할 수 있었지만, 이와 동시에 컬렉션에는 연관된 타입만 삽입할 수 있게 되었습니다.
즉, 제네릭은 자바의 타입 안전성은 높였지만, 유연성은 떨어뜨렸습니다.
문제 제기
Java5 이전의 컬렉션은 모두 동일하게 Object를 내부로 가짐으로 모두 동일한 타입이였습니다. 따라서 Collection Framework의 알고리즘들을 Collection, Map을 구현한 모든 클래스들에 대해 적용할 수 있었습니다.
하지만, 제네릭의 등장으로 인해 모든 제네릭 클래스들은 타입 매개변수에 따라 전부 다른 타입으로 컴파일 타임에 고정되게 되었습니다.
예를 들어 임의의 컬렉션 Set가 존재할때 두가지 Set,
타입 매개변수로 Integer를 설정한 Set
타입 매개변수로 String을 설정한 Set
각각은 자바의 불공변성으로 인해 서로 다른 타입으로 컴파일 타임에 결정됩니다.
이에 따라 모든 콜렉션에 적용되던 Collection 알고리즘 ( 정렬, 이분 탐색, ... ) 등의 API를 각각의 타입에 맞춰 구현해야하는 문제가 생겼습니다. 즉, 재사용성을 높이기 위해 제네릭이 도입되었지만, 제네릭을 도입함으로써 오히려 재사용성이 떨어지는 문제가 발생했습니다.
이를 개선하기 위해 와일드카드가 등장하게 됐습니다.
재사용성(+ 타입 안정성)을 위해 도입된 제네릭이 오히려 재사용성을 저하하는 현상,
이를 보완하기 위해 도입된 와일드 카드.
아름답다.
와일드카드란?
와일드카드란 모든 제네릭 클래스에 지정된 모든 타입을 대신할 수 있는 타입 입니다. 이를통해, 타입에 따라 구별되던 제네릭 클래스들을 타입에 상관없이 유연하게 다룰 수 있으며, 동시에 타입의 안정성 지켜낼 수 있습니다.
와일드 카드를 사용하는 예시를 보여드리겠습니다.
void printCollection(Collection<?> c) {
for (Object e : c) {
// 와일드 카드를 사용할때 제네릭 클래스의 타입은 Object로 캐스팅 됩니다.
System.out.println(e);
}
}
1. 와일드 카드를 통해 파라미터로 받은 제네릭 클래스의 구체적인 타입에 대해 알지 못합니다.
따라서 와일드 카드는 모든 타입을 Object 타입으로 자동으로 캐스팅 합니다.
2. 이로인해 구체적인 타입을 모름으로, 해당 제네릭 클래스에 대해 추가, 수정, 제거 작업은 수행할 수 없습니다.
이를 보완하기 위해 Bounded WildCard가 등장합니다..
Bounded WildCard
와일드 카드가 모든 객체들을 Object로 캐스팅하는 한계를 극복하기 위해 파라미터로 받을 제네릭 클래스의 타입에에 제한을 둡니다. 이 제한을 통해 객체를 Object보다 하위의 클래스로 안정적으로 캐스팅할 수 있음으로, 더 의미있게 제네릭 클래스를 사용할 수 있습니다.
void printCollection(Collection<? extends Number> c) {
for (Number number : c) {
// 와일드 카드를 사용할때 제네릭 클래스의 타입은 Object로 캐스팅 됩니다.
// System.out.println(number);
// 하지만 이제, 상한 경계를 통해 안정적으로 Number 클래스로 캐스팅할 수 있습니다.
System.out.println(number.byteValue());
}
}
위와 같이 상한경계를 통해 파라미터를 Collection 타입이 Number의 하위클래스인 객체들로 제한하여 더 의미있는 작업을 수행할 수 있습니다.
이제 파라미터로 받는 제네릭 클래스의 최상위 타입을 알고 있으니, 제네릭 클래스에 대해 Number를 삽입할 수 있을까요?
우린 printCollection() 메소드의 파라미터로 들어오는 제네릭 클래스의 타입 범위를 정한 것 뿐 입니다.
이 제네릭 클래스는 위 함수뿐만 아니라 구체적인 타입을 지정하는 메소드에 대해서 실행될 수 있으므로, 함부로 변경해선 안됩니다.
아래 예시를 통해 자세히 설명 드리겠습니다. 아래 코드는 제네릭 클래스가 다른 함수에서 실행될때 발생할 수 있는 문제를 보여줍니다.
만약, 최상위 값에 만족하는 인스턴스를 삽입할 수 있다면, 아래는 ClassCastException을 일으키게 될 것입니다.
public class Main {
public static void main(String[] args) {
List<Integer> numbers = IntStream.rangeClosed(1, 100)
.boxed()
.collect(Collectors.toList());
printCollection(numbers);
}
static void printIntegers(Collection<Integer> collection) {
collection.stream().forEach(System.out::println);
}
static void printCollection(Collection<? extends Number> c) {
for (Number number : c) {
System.out.println(number.byteValue());
}
c.add(new Number() {
@Override
public int intValue() {
return 10;
}
@Override
public long longValue() {
return 10;
}
@Override
public float floatValue() {
return (float) 10.000;
}
@Override
public double doubleValue() {
return 10.000;
}
});
}
}
이렇게 제네릭에 와일드카드가 왜 필요한지, 와일드 카드를 더 의미있게 사용하기 위한 Upper Bound, Lower Bound가 왜 존재하는지에 대해 살펴봤습니다. 공부중에 필자가 가졌던 의문점들을 기반으로 설명드렸으며, 혹여나 추가적인 궁금증이 있으시다면 댓글에 남겨주시기 바랍니다.
와일드 카드의 구체적인 사용방법은 여타 다른 블로그를 참조하시기 바랍니다.
'Java' 카테고리의 다른 글
Java IO / NIO (0) | 2024.12.16 |
---|---|
[Java] Optional 클래스 살펴보기 (2) (0) | 2024.01.24 |
[Java] Optional 클래스의 등장 (1) (2) | 2024.01.23 |
[Java] Collection Framework (1) | 2024.01.10 |
[Java] 제네릭의 기초 (1) | 2024.01.04 |