이전 시간에는 제네릭의 기본적인 unbounded type parameter, unbounded wildcard type, type erasure에 대해 알아보았습니다.
만약 제네릭에 unbounded type만 존재한다면, 다음과 같은 상황이 불편할 수 있습니다.
나는 숫자에 관련된 클래스만 담을 수 있는
Box
generic type을 만들고 싶어!
만약 Number
의 하위 클래스만 Box
에 담을 수 있다면, 해당 클래스 내에서 Number
에 관련된 자유로운 연산이 가능할 것입니다.
따라서 제네릭의 actual type parameter를 한정지을 수 있다면, 여러 상황에서 이점을 가질 수 있습니다.
하지만 unbounded는 모든 parameterized type이 가능하기 때문에, 위와 같은 설계를 만족하기 위해서는 Box
의 생성자에서 Number
의 하위 클래스만 actual type parameter로 가능하게 하는 일련의 과정이 필요할 것입니다. (instance of
로 체크하는 등)
물론 위의 과정을 추가해서 Number
의 하위 클래스임이 보장되더라도 unbounded type은 Object로 취급되기 때문에 Number
로의 형변환 코드를 추가해야합니다.
당연하게도 제네릭은 이와 같은 상황을 해결할 수 있도록 bounded type을 제공합니다.
bounded type parameter의 경우 extends
로 상한 제한을, bounded wildcard type의 경우 extends
와 super
를 통한 상, 하한 제한이 가능합니다.
첫 번째로 bounded type parameter가 무엇인지, type erasure로 어떠한 바이트 코드가 생성되는지 확인해보겠습니다!
bounded type parameter는 List<E extends Number>
로 표현됩니다.
위와 같이 actual type parameter를 Number
의 sub 클래스로 제한하게되면, Number
의 하위 클래스임이 보장되기 때문에 generic type에서 항상 하위 클래스를 Number
로 업캐스팅하여 다룬다고 생각할 수 있습니다.
예를 들어 다음과 같은 Box
클래스가 존재한다고 가정해보겠습니다.
class Box<E extends Number> {
private E number;
public Box(E number) {
this.number = number;
}
public E getNumber() {
return number;
}
public void setNumber(E number) {
this.number = number;
}
public long getAddedNumber(Number number) {
return this.number.longValue() + number.longValue();
}
}
bounded type parameter를 사용했기 때문에, Number
의 하위 클래스가 아니라면 생성 불가능합니다.
// Number의 하위 클래스
Box<Integer> integerBox = new Box<>(10);
Box<Long> longBox = new Box<>(10L);
Box<Double> doubleBox = new Box<>(1.1D);
// 관계 없는 클래스
Box<String> stringBox = new Box<>(); // error!
따라서 해당 클래스는 확실하게 Number
의 하위 클래스임이 보장되기 때문에 항상 Number
로 업캐스팅되었다고 가정할 수 있습니다.
public long getAddedNumber(Number number) {
// field의 T number를 Object가 아닌, Number로 사용
return this.number.longValue() + number.longValue();
}
psvm {
long result = integerBox.getAddedNumber(longBox.getNumber());
System.out.println(result);
}
// 결과
20
getAddedNumber()
메소드에서 필드의 formal type parameter를 Number
라고 가정하여, 해당 클래스의 메소드의 사용을 확인할 수 있습니다.
즉, bounded type parameter를 사용하면 generic type에서 제한에 사용된 클래스로 업캐스팅하여 사용이 가능합니다.
따라서 제한에 사용된 클래스의 하위 클래스만 actual type parameter로 사용할 수 있는 것을 확인했습니다.
그렇다면 type erasure에 의해 바이트 코드에서는 Box<E extends Number>
의 E number
필드의 타입이 Number
로 치환될 것이라고 예상할 수 있습니다.
private Ljava/lang/Number; number
예상처럼 E
가 Number
로 치환된 것을 확인할 수 있습니다.
wildcard type의 경우 상한, 하한 범위로 제한할 수 있습니다.
상한 제한부터 살펴보겠습니다.
public static void main(String[] args) {
List<Integer> integerList = new ArrayList<>();
integerList.add(1);
printList(integerList);
List<String> stringList = new ArrayList<>();
stringList.add("java");
printList(stringList); // error!
}
private static void printList(List<? extends Number> list) {
list.forEach(System.out::println);
}
printList()
의 매개변수를 upper bounded wildcard type으로 제한했기 때문에 Number
의 하위 클래스를 type으로 가지는 List<Integer>
는 가능하지만, List<String>
은 파라미터로 들어갈 수 없습니다.
그렇다면 하한 제한의 경우는 어떨까요?
private static void printList(List<? super Number> list) {
list.forEach(System.out::println);
}
Number
로 하한 제한을 걸었기 때문에, Number
의 하위 클래스인 Integer
또한 불가능하게 됩니다.
List<Number> numberList = new ArrayList<>();
numberList.add(1);
printList(numberList);
List<Object> objectList = new ArrayList<>();
objectList.add(1);
printList(objectList);
// 결과
1
1
반면에 Number
의 상위 클래스인 자기 자신과 Object
는 파라미터로 가능한 것을 확인할 수 있습니다.
bounded wildcard type의 경우에도 바이트 코드에서 어떻게 변환되는지 확인해봅시다.
// 상한 제한
(Ljava/lang/Number;)V
// 하한 제한
(Ljava/lang/Object;)V
상한 제한의 경우 ?
를 Number
로, 하한 제한의 경우 ?
를 Object
로 치환하는 것을 확인할 수 있습니다.
모든 클래스의 최상위 클래스는 Object
이므로, 하한 제한의 경우 위와 같이 매핑되는 것입니다.
다음 시간에는 조금 더 복잡한 제네릭 예시에 대해 살펴보겠습니다!