본문 바로가기
프로그래밍/Java

[Java] Java 제네릭의 형 변환(covariant & contravariant)

by 사바라다 2020. 11. 11.

안녕하세요. 저희는 저번 포스팅에서 배열과 컬렉션 제네릭의 차이점에 대해서 알아보는 시간을 가졌습니다. 그리고 배열은 기본적으로 형변환이 자유로우며 컬렉션 제네릭은 무공변으로 형변환이 불가능 하다는 차이를 알게 되었습니다. 그렇다면 컬렉션을 이용해서는 다양한 형태의 데이터 처리가 불가능 할까요? 그렇지 않습니다.

오늘은 제네릭의 형 변환(covariant & contravariant)에 대해서 알아보는 시간을 가져보겠습니다.

공변성(covariant)

공변성이란 자신이 상속받은 부모 객체로 타입을 변화시킬 수 있다라는 것을 뜻합니다. 제네릭의 공변성을 사용하기 위해서는 extends 키워드를 사용해야합니다. 아래 예제는 이전 포스팅에서 사용한 예제입니다. 아래 처럼 사용하면 컴파일에러가 발생하는 건 우리는 이미 확인하였습니다. 아래의 예제가 정상적으로 돌아갈 수 있도록 수정해보도록 하겠습니다.

public Number sum(List<Number> numbers) {
    long sum = 0;
    for (Number number : numbers) {
        sum += number.longValue();
    }
    return sum;
}

List<Integer> myInts = List.of(1,2,3,4,5);
List<Long> myLongs = List.of(1L, 2L, 3L, 4L, 5L);
List<Double> myDoubles = List.of(1.0, 2.0, 3.0, 4.0, 5.0);
System.out.println(sum(myInts)); // 컴파일 에러
System.out.println(sum(myLongs)); // 컴파일 에러
System.out.println(sum(myDoubles)); // 컴파일 에러

파리미터가 변경된 것을 확인할 수 있습니다. 기존의 Number 였지만 공변성을 만족시키기위하여 ? extends Number로 타입 표현식을 변경하였습니다. ? extends T는 T 타입을 포함하여 그 자식 객체들을 받아들일 수 있게 됩니다.

public Number sum(List<? extends Number> numbers) {
    long sum = 0;
    for (Number number : numbers) {
        sum += number.longValue();
    }
    return sum;
}


List<Integer> myInts = List.of(1,2,3,4,5);
List<Long> myLongs = List.of(1L, 2L, 3L, 4L, 5L);
List<Double> myDoubles = List.of(1.0, 2.0, 3.0, 4.0, 5.0);
System.out.println(sum(myInts)); // 정상
System.out.println(sum(myLongs)); // 정상
System.out.println(sum(myDoubles)); // 정상

그런데 이렇게하면 배열과 동일한 문제점이 발생할 수 있지 않을까요? 즉, 런타임에 잘못된 형 변환이 발생하여 런타임예외(RuntimeException)이 발생할 가능성 말입니다. 다행스럽게도 발생할 가능성은 없습니다. 왜냐하면 ? extends Number제네릭으로 업캐스팅하여 받은 데이터는 읽기는 가능하지만 쓰기는 불가능 하기때문입니다. 아래의 코드를 함께 보도록 하겠습니다. 코드를 보시면 컴파일 오류가 나면서 잘못된 타입을 주입하고있다고 나옵니다.

반공변성(Contravariance)

제네릭의 공변성은 읽기는 가능하지만 쓰기는 불가능하게 하여 런타임중 에러가 발생하는 것을 막습니다. 그런데 경우에 따라서 사용자는 읽기가 아니라 쓰기를 원할 수도 있습니다. 이럴 경우 사용할 수 있는 타입 표현식은 ? super T입니다. 아래 예제를 보도록 하겠습니다.

public void addNumber(List<? super Integer> numbers) {
    numbers.add(6);
    // numbers.get(0); 컴파일 에러
}

List<Number> myInts = new ArrayList<>();
addNumber(myInts);

System.out.println(myInts); // 정상

addNumber 메서드는 List<? super Integer> 타입을 파라미터로 받습니다. 이는 즉 Integer 상위의 부모객체를 파라미터로 받을 수 있다는 뜻입니다. 이 경우에 런타임에러가 날 가능성은 없을까요? 그렇습니다. 없습니다. 왜냐하면 자식 객체는 부모 객체의 모든것을 포함하고 +알파의 요소를 가지고있습니다. 따라서 읽기 메서드를 호출할 수 있다면 Number에 Double 형이 있다면 ClassCastException가 발생할 가능성이 있습니다. 하지만 쓰기는 Integer가 Number의 자식객체이기 때문에 전혀 문제가 없는 것입니다.

마무리

오늘은 이렇게 자바 제네릭의 공변성과 반공변성에 대해서 알아보았습니다.

다음 시간에는 또 다른 이야기로 찾아뵙도록 하겠습니다.

감사합니다.

참조

dzone_covariance-and-contravariance

medium_covariance-and-contravariance-in-jav

댓글