Coding Log

019. Class - Generic (1)

2018.03.27 14:35 - NamhoonKim NE_Leader


Kotlin

본 카테고리는 2017년 Android 공식 언어로 채택된 Kotlin에 관하여 다룬다.

Kotlin을 이용해 개발하는 Android는 추후 따로 다루기로 하고 언어 자체에만 집중한다.

참고 kotlin 공식 사이트

Class - Generics (1)

Java와 마찬가지로 Koltin의 클래스는 파라미터를 가질 수 있다.

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

일반적으로 클래스의 객체를 생성하려면 아래처럼 적당한 타입의 파라미터들을 제공해야 한다.

val box: Box<Int> = Box<Int>(1)

하지만 생성자의 파라미터나 다른 방법으로 파라미터를 추론할 수 있다면 생략이 가능하다.

아래의 예제 코드를 보면 1이 숫자이기 대문에 Int임을 추론할 수 있어 생략이 가능함을 알 수 있다.

val box = Box(1) // 1 has type Int, so the compiler figures out that we are talking about Box<Int>

변성(Variance)

Java에서 가장 까다로운 것중 하나는 와일드카드(?) 타입이다.

Kotlin은 이 와일드카드 타입이 없는 대신 선언 위치에 따른 변성(declaration-site variance)  타입 프로젝션(type projections) 을 통해 동작한다.

첫 번째로, Java에는 왜 와일드카드가 존재하는지 고민해보아야 한다.

Java의 Generic은 변할 수 없다 즉, 무공변(invariant) 이라는 타입으로 정의되어 있다.

무공변은 타입 앞에 아무런 키워드가 붙지 않고 자기 자신의 타입만을 허용하는 특성을 말한다.

예를 들어 List<String> List<Object>의 하위 타입이 아니라는 것을 의미하는 것이다.

만약 Kotlin의 List도 무공변이라면 Java의 Array보다 나은 점이 없기 때문이다.

아래 코드를 통해 확인해보자.

// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!! The cause of the upcoming problem sits here. Java prohibits this!
objs.add(1); // Here we put an Integer into a list of Strings
String s = strs.get(0); // !!! ClassCastException: Cannot cast Integer to String

위의 코드를 실행하면 ClassCastException이라는 오류를 발생시킨다.

List<Object> objs = strs 이 부분이 문제의 원인으로, Java는 이러한 초기화를 허용하지 않는다.

즉, Integer를 String으로 변환하는 것을 허용하지 않는 것이다.

컴파일타임도 아닌 런타임에서 오류가 발생하는 것을 막기 위한 Java의 정책이지만 일부에 영향을 준다.

예를 들어 Collection 인터페이스의 addAll() 메소드가 있다.

아래의 코드를 보자.

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

만약 위와 같은 형태로 addAll() 메소드가 작성되어있다면 아래와 같은 문제점이 발생할 것이다.

// Java
void copyAll(Collection<Object> to, Collection<String> from) {
  to.addAll(from); // !!! Would not compile with the naive declaration of addAll:
                   //       Collection<String> is not a subtype of Collection<Object>
}

위에서 언급했듯 String Object의 하위 타입이 아니기 때문에 발생하는 문제로 실제로 addAll() 메소드는 아래와 같이 작성되어 있다.

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

와일드카드 타입 인자로 쓰인 ? extends E addAll 메소드가 E객체의 컬렉션이거나 E의 하위 타입인 객체의 컬렉션도 허용한다는 것을 나타낸다.

이는 파라미터명 items에서 E 객체 혹은 E의 하위 객체를 안전하게 읽을 수 있음(read) 을 뜻한다.

하지만 반대로 E의 어떤 하위 객체인지 알 수가 없기때문에 쓰는 것은 불가능(cannot write) 도 뜻하기도 한다.

이런 제약사항을 가진 대신 Collection<String> Collection<? extends Object>의 하위타입처럼 읽을 수 있게된다.

참고 위처럼 extends-bound(upper bound) 를 가진 와일드카드는 타입을 공변(covariant) 으로 만든다.

다시 한 번 정리하면 아래와 같다.

특정 컬렉션에서 item을 가져올 수 있다면 String 타입의 컬렉션을 사용하고 Object 타입으로 읽을 수 있다.

반대로 특정 컬렉션에서 item을 넣을 수만 있다면 Object 컬렉션에 String을 넣을 수 있다.

예를 들어 List<Object>의 상위 타입으로 List<? super String>으로 존재한다.

참고 이를 반공변(contravariance) 라고 한다.

선언 위치에 따른 변성(Declaration-site variance)

제네릭 인터페이스 Source<T> T를 파라미터로 쓰는 메소드가 없고 오직 반환하는 메소드만 있다고 가정하자.

// Java
interface Source<T> {
  T nextT();
}

Source<String> 객체에 대한 참조를 Source<Object> 타입 변수에 저장하는 것은 매우 안전한 방법이다.

하지만 Java에서는 특정 메소드에 대한 호출이 없는 지 인지하지 못하기때문에 오류를 발생시킨다.

// Java
void demo(Source<String> strs) {
  Source<Object> objects = strs; // !!! Not allowed in Java
  // ...
}

이를 해결하기 위해선 Source<? extends Object> 타입의 객체를 선언해야 한다.

하지만 기존에 작업한 메소드를 통해서 충분히 호출할 수 있으므로 타입만 더 복잡해지는 무의미한 작업일 뿐이다.

참고 당연하게도 Java의 컴파일러는 위의 방식도 인지하지 못한다.

Kotlin은 컴파일러에게 직접 알려주는 방식으로 오류를 제거할 수 있으며, 이를 선언 위치에 따른 변성 이라 한다.

예를 들어 Source<T>의 멤버에서 T를 반환만하고 파라미터로 쓰이지않는다고 컴파일러에게 알려주려면 out 키워드를 사용할 수 있다.

interface Source<out T> {
    fun nextT(): T
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // This is OK, since T is an out-parameter
    // ...
}

일반적인 규칙은 아래와 같다.

클래스 C의 파라미터 타입인 T out 으로 선언하면 클래스 C의 멤버에서 out 의 선언 위치에만 T가 위치할 수 있지만, 반환타입은 C<Derived>의 상위타입인 C<Base>으로도 안전하게 처리된다.

이를 클래스 C가 파라미터 타입 T 공변(covariant) 한다고 표현한다.

참고 반대로 파라미터 타입 T 공변(covariant) 타입 파라미터라고도 표현할 수 있다.

 out 키워드를 변성 어노테이션(variance annotation) 이라고 부르며 선언 위치에 따른 공변 에 대해서 사용한다.

out 과 대치되는 키워드로 또 다른 변성 어노테이션인 in 이 존재하며 이는 공변의 반대인 반공변(contravariant) 상태를 표기한다.

아래의 예제를 통해 확인해보자.

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

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 has type Double, which is a subtype of Number
    // Thus, we can assign x to a variable of type Comparable<Double>
    val y: Comparable<Double> = x // OK!
}


DISQUS 로드 중…
댓글 로드 중…

트랙백을 확인할 수 있습니다

URL을 배껴둬서 트랙백을 보낼 수 있습니다