[Kotlin] Generic And Variance

evergreen_tree·2022년 6월 10일
2

Kotlin

목록 보기
2/6

자바에서도 Generic에 대해 얕게 이해하고 있었는데, Kotlin에서 in, out를 만나니까 제대로 이해하지 못하고 있다는걸 깨닫게 되었습니다. Kotlin 공식 문서를 참고하여 포스팅하였습니다.


🖐Generic


  • Data Type Generalize
  • 클래스 내부에서 사용할 Data Type을 컴파일 시 미리 지정하는 것을 의미합니다.

Class 혹은 Interface 선언

class State<T>(t: T) {
    var value = t
}

자바와 마찬가지로, 클래스에 <type argument>를 붙여, 선언합니다.


인스턴스 생성

val state: State<String> = State<String>("ㅎㅇ")

클래스의 인스턴스를 생성할 때, <> 안에 데이터 타입을 명시하고 인스턴스를 생성할 수 있습니다.

val state = State("ㅎㅇ")

혹은, 따로 데이터 타입을 명시하지 않고 생성자를 통해 유추할 수 있다면 컴파일러가 자료형을 자동으로 추론합니다.


🤷‍♂️Variance


제네릭을 설명할때 이 Variance(가변성) 이라는 개념을 빼놓을 수 없습니다.

Variance는 제네릭 타입의 계층 관계가 어떤지를 나타내는 개념입니다. Variance에는 세 가지 종류가 있습니다.

Type A가 Type B의 하위 타입일 때

  • invariance(무공변) : Class<A>Class <B>의 상속 관계가 없음
  • covariance(공변) : Class<A>Class<B>의 하위 타입
  • contravariance(반공변) : Class<A>Class<B>의 상위 타입

코틀린에서는 자바와 마찬가지로 기본적으로 Generic Class 간 invariance(무공변) 상태입니다. 이는 밑에서 공변성을 이해할 때 더 쉽게 이해할 수 있을 것입니다.

객체지향 제 5원칙 중, 리스코프 치환 원칙을 지키기 위해서는, 상위 타입이 사용되는 경우 하위 타입의 인스턴스를 통해 동작할 수 있어야 합니다.
Generic에서도 이를 지키기 위해서 Variance라는 개념을 사용하게 됩니다.



👨‍🦳Covariance


먼저 자바에서 예를 들어보겠습니다.

Java에서의 covariance(공변)


interface Collection<E> ... {
    void addAll(Collection<E> items);
}

임의로 addAll이라는 메서드를 작성합니다.

매개변수로 받은 items에게 E 타입의 모든 item을 넘겨주는 메서드입니다.

void copyAll(Collection<Object> to, Collection<String> from) {
    to.addAll(from);
}

copyAll을 통해 to로 받은 인자가 addAll을 호출하여 from에게 데이터를 넘겨줘봅시다. 위의 코드는 오류가 날까요?

네. 왜냐하면 String는 Object의 하위 타입이지만, List<String>List<Object>의 하위 타입이 아니기 때문입니다. 이를 무공변(invariance) 관계라고 합니다
위와 같은 경우 런타임 안정성을 보장하기 위해 컴파일 타임에 오류를 발생 시킵니다.


자바에서는 이를 해결하기 위해 와일드 카드라는 개념을 이용합니다.

interface Collection<E> ... {
    void addAll(Collection<? extends E> items);
}

익숙하지 않은 <? extends E> 가 보이는군요. 자바에서는 <? extends E> 를 통해서 E 타입 또는 E의 하위 유형을 허용할 수 있게 하여 covariance(공변) 관계로 만듭니다.

자 다시 공변성에 대해서 짚고 가보겠습니다.

  • covariance(공변) : Type A가 Type B의 하위 타입일 때 Class<A>Class<B>의 하위 타입

Generic Type 이 상속 관계를 가지고 있더라도 Generic Class는 상속 관계가 아닙니다.
즉, 기본적으로 무공변 상태인 것입니다. 자바에서는 이를 해결하기 위해 wildcard type argument를 제공하고, 공변 관계로 만들 수 있습니다.

이제 우리는 Generic의 covariance에 대해 이해할 수 있게 되었습니다.🎈

Kotlin에서의 covariance(공변)

interface List<out E> ... {
   addAll(items: List<E>)
}
fun copyAll(to: List<Object>, from:List<String>) {
    to.addAll(from);
}

Kotlin에서는 클래스에 out을 붙여 컴파일러에게 공변 상태임을 알려주게 됩니다.

보통 제네릭 인자가 생산자가 되는 경우, 즉 제네릭 타입에서 들어온 인수가 하위 타입이어서 상위 타입에 안전하게 할당하는 경우 out이 사용됩니다.
코틀린에서는 이 out 이라는 수식어를 Variance Annotation이라고 부릅니다.



👩‍🦰contravariance(반공변)


공변의 반대 개념으로, 공변을 보완하기 위해 나온 개념입니다. 공변과는 대조적으로 제네릭 인자가 소비자가 되는 경우에 반공변성을 부여합니다.

Java에서는 <? super E> , 코틀린에서는 in 을 통해서 반공변성을 부여합니다.

Kotlin의 예시를 하나 들어보겠습니다.

interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) 
    val y: Comparable<Double> = x
}

Type Number의 하위 Type은 Double입니다. 하지만 T가 in 이기 때문에 인수로 들어오는 Comparable<Number> 는 하위 타입이 됩니다. 따라서 x의 데이터를 Double로 형변환 한 후, y에게 x를 대입하여도 컴파일 에러가 발견되지 않게 됩니다.

The Existential Transformation: Consumer in, Producer out!
:-)
→ T가 소비자면 in, T가 생산자면 out으로 이해하면 될 것입니다.



🏓Kotlin VS JAVA


근데 여기서 Kotlin과 Java와 차이점은 무엇일까요?

Kotlin에서는 interface(혹은 class)에 out 키워드를 붙여 공변성을 제공하고, 자바void addAll(Collection<? extends E> items); E를 사용하는 메서드에 공변성을 제공합니다.

  • Kotlin에서는 이 방식을 Declaration-Site Variance(선언 위치 변환)

  • 자바에서는 이 방식을 Use-Site Variance(사용 위치 변환) 이라고 부릅니다.

뭐가 더 좋을까요? 라고 부른다면, 뻔하지만 Java의 상위 호환인 Kotlin입니다.

Kotlin에서는 클래스에서 out을 한번 선언하면 되지만, Java에서는 매번 붙여줘야 하기 때문에 불필요한 코드가 발생합니다. 물론 코틀린에서도 Use-Site Variance를 제공합니다.


참고 문서
https://kotlinlang.org/docs/generics.html#type-erasure

✅ 코틀린의 다른것이 궁금할땐 아래 링크 확인
https://velog.io/@ham2174

profile
android_developer

6개의 댓글

comment-user-thumbnail
2022년 6월 10일

학교에서 코틀린을 배우게 해주세요 ;ㅁ;

1개의 답글
comment-user-thumbnail
2022년 6월 11일

다음주는 설마 Inline Class에 대해서 나오나요??

1개의 답글
comment-user-thumbnail
2022년 6월 11일

코틀린말고 코맞은은 없나요?

1개의 답글