Java Generic (2)

wannabeking·2023년 4월 17일
0

Java

목록 보기
10/13
post-thumbnail

이전 시간에는 제네릭의 기본적인 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의 경우 extendssuper를 통한 상, 하한 제한이 가능합니다.

첫 번째로 bounded type parameter가 무엇인지, type erasure로 어떠한 바이트 코드가 생성되는지 확인해보겠습니다!



Bounded Type Parameter

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

예상처럼 ENumber로 치환된 것을 확인할 수 있습니다.



Bounded Wildcard Type

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이므로, 하한 제한의 경우 위와 같이 매핑되는 것입니다.


다음 시간에는 조금 더 복잡한 제네릭 예시에 대해 살펴보겠습니다!



profile
내일은 개발왕 😎

0개의 댓글