Kotlin - Generic

권태용·2020년 9월 27일
0

Kotlin

목록 보기
3/6

Q1. 자바와 코틀린의 제네릭에서 엄청난 차이점이 하나 존재합니다 뭘까요?

무슨 차이가 있을까? 이번에도 공식문서 부터 보자..

https://kotlinlang.org/docs/reference/generics.html#generics

Kotlin Generic 사용법 예시

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

fun main(){
	val box: Box<Int> = Box<Int>(1)
 }
 But if the parameters may be inferred, e.g. from the constructor arguments or by some other means, one is allowed to omit the type arguments:

T라는 제레릭 타입을 선언해주어야 하지만 매개변수에 넣는다면 자동으로 매개변수 타입으로 컴파일을 해준다. 이게 코틀린과 자바의 차이인줄 알았지만 아래의 테스트 코드를 짜봤더니 컴파일이 되었다. 음 그럼 일단 이건 아니다. 문서를 더 읽어보자

class Data<T> {

    T value;

    Data(T input) {
        this.value = input;
    }
}

public class JavaTest {

    public static void main(String[] args) {
        Data t = new Data(1);
        System.out.println(t.value);
    }
}

Variance(변환)

One of the most tricky parts of Java's type system is wildcard types (see Java Generics FAQ). And Kotlin doesn't have any. Instead, it has two other things: declaration-site variance and type projections.

자바에서 가장 교활한(?) 파트는 wildcard type이다. 하지만 코틀린은 이런것이 없다. 대신 다른 두가지가 존재한다. declaration-site variance 와 projections이다.

음 이게 Java와 Kotlin의 차이가 될 수 있을 것 같다. 하지만 Java의 wildcard types도 제대로 알지 못 하고 declaration-site variance 와 projections도 잘 알지 못하기 때문에 하나하나 알아가 보자

Java wildcard

Generic을 사용해서 메소드를 정의할때 wildcard를 사용할 수 있다.
wildcard의 특징은 메소드의 파라미터 클래스 타입을 동적으로 사용할 수 있게 해주는 것이다. 이를 자바에선 use-site variance 이라고 한다.( 사용하는 부분에서 변환이 이루어지기 때문에 )

<?> - 모든 타입 가능
<? extends T> - T를 상속받은 타입
<? super T> - T의 상위 타입

설명만으론 이해가 힘들기에 List.addAll() 메소드에 사용된 예제를 보고 이해해 보자

addAll()에 정의된 파라미터 C는 E를 상속받은 어떤 타입의 리스트만 입력이 가능하다. 여기서 E는 List선언시에 지정되는 타입이다.

이를 확인하기 위한 테스트 코드를 작성해보면 Integer는 Number를 상속받았기 때문에
numberList에 integer리스트를 추가할때는 에러가 발생하지 않지만 integerList에 numberList를 넣을때는 Number로 업캐스팅이 필요하다.

하지만 Generic한 타입에 제한을 풀고 다형성을 고려하기 위해 wildcard가 나왔다. 하지만 공식문서에서 제시하는 아이러니 한점은 따로 있었다.

objs에 strs를 할당은 안되지만 addAll을 하면 (Ojbect는 String의 상위 클래스니깐) 된다. 왜 triky하다 하는지 이해가 된다.

declaration-site variance 와 projections

그렇다면 어떻게 위와 같은 아이러니한 상황을 해결 할 수 있을까?

The key to understanding why this trick works is rather simple: if you can only take items from a collection, then using a collection of Strings and reading Objects from it is fine. Conversely, if you can only put items into the collection, it's OK to take a collection of Objects and put Strings into it: in Java we have List<? super String> a supertype of List<Object>.

trick works 를 이해하는 방법은 간단하다. 만약 너가 item을 Collection에서 가져가기만 한다면 Collection<String>을 사용하라 그리고 Object타입으로 읽으면 괜찮다. 반대로 너가 만약 item을 Collection에 넣기만 한다면 Collection<Object>를 사용하고 String을 넣으면 된다. 

코틀린의 문서는 이러한 아이러니 상황은 put과 read를 동시에 하려할때 문제가 발생한다고 한다 맞는 말이다. Object 클래스를 String으로 읽을 순 없지만 String을 Object로 읽을 수 있다. 반대의 상황도 마찬가지다. 이러한 두가지 상황을 declaration-site variance 와 projections을 사용해 해결 할 것이라고 생각 되었다.

declaration-site variance

이러한 아이러니상황은 Generic Type을 읽는용도(return) 또는 사용하는용도(parameter)로 정확히 나누어 사용한다면 해결 할 수 있다. 때문에 kotlin은 Generic Type 선언부에 out과 in을 사용해서 해당 타입의 사용용도(변환가능성)를 컴파일러에게 정의 해줄 수 있다.

in, out 예제 및 에러케이스

interface Source<out T, in K> {
    fun nextT(): T
    fun putT(item:T) // Error
    fun putK(item:K)
    fun nextK(): K //Error
}

이때 return(out)만 하는 상황을 Produced라 하고, 파라미터로 받아 해당 사용하는 상황을 Consumer라 지칭하고 있다.

declaration-site variance을 통해 java에서 발생하던 아이러니한 상황을 만들지 않게 할 수 있다. 또한 매번 함수선언시에 사용해야 했던 wildcard 방법을 선언부에서 정의하면서 불필요한 반복을 줄일 수 있게 되었다.

type projection

선언부에서 이러한 타입의 변환 가능성을 제한한다고 메소드에서 그 제한이 항상 효율적이라고 볼 수 없다. 그렇기에 java에서 wildcard 방식을 허용 했던것이 아닐까 생각된다.

class Array<T>(val size: Int) {
    fun get(index: Int): T { ... }
    fun set(index: Int, value: T) { ... }
}

예시로 Array에서 T는 리턴 값으로도 쓰이고 인풋 값으로도 쓰인다. 이때는 use-site에서 변환성을 정의해주어야 한다. 그렇기에 코틀린은 메소드에 Generic 타입 파라미터에서 변환성을 정의 해줄 수 있다. 이를 type proejctions 이라고 한다.

star projection

fun demo(t:T, u:U){..}

만약 위 함수에서 T를 정의한다고 했을때 상황상 t에 어떤 타입을 선언시점이 런타임 상황에서 결정된다면 어떻게 해야 할까? Any를 사용 할 수도 있다. 하지만 이는 모든 오브젝트를 받기 때문에 타입을 선언한다기 보다 모든 타입을 오픈하는 것이기에 안전하다고 볼 수는 없다. 이때 '*' 을 사용할 수 있다.

Q1. 자바와 코틀린의 제네릭에서 엄청난 차이점이 하나 존재합니다 뭘까요?

궁극적인 차이는 제네릭 타입을 선언시와 사용시에 변환 시킬 수 있다는 점이 있다. 이는 코드의 효율성과 반복코드를 줄여준다.

이 외에도 코틀린의 경우 제내릭 타입에 대한 extends 함수를 지정해 줄 수 있다. 하지만 extends은 kotlin의 기능이기에 차이라고는 생각되지 않는다.

profile
개발일기장

0개의 댓글