Kotlin - Generics 클래스, 함수를 정의하는 방법

제네릭스(Generics)는 클래스나 함수를 정의할 때 타입을 확실히 정하지 않는 것을 말합니다. 그렇기 때문에 다양한 타입으로 클래스를 여러개 정의하지 않아도 됩니다. Generic 함수 및 클래스를 정의하는 방법에 대해서 알아보겠습니다.

또, Invariance(불변성)과 Covariance(공변성)이란 용어도 있습니다. Generics에서 이 용어들이 어떤 것을 의미하는지 알아보고 in/out 키워드에 대해서 알아보겠습니다.

Generic 함수 정의

Generic 함수를 정의할 때, 타입이 정해지지 않은 변수는 함수 이름 앞에 <T>처럼 정의되어야 합니다. 아래 코드는 타입 T 변수 num1과 num2를 더하고 타입 T 변수를 리턴하는 함수입니다.

fun <T> addNumbers(num1: T, num2: T): T {
    return (num1.toDouble() + num2.toDouble()) as T
}

위 함수는 아래 처럼 호출할 수 있습니다.

fun main(args: Array<String>) {
    println(addNumbers(10, 20))      // 결과: 30
    println(addNumbers(10.1, 20.1))  // 결과: 30.200000000000003
}

T는 타입이 정해지지 않았기 때문에 어떤 타입이든 올 수 있습니다.

Generic 클래스 정의

Generic 클래스를 정의할 때 타입이 정해지지 않은 변수는 클래스 이름 다음에 와 같이 정의합니다. Generic 함수와 다른 것은 이름 다음에 <T>가 온다는 것입니다.

아래 코드는 임의의 타입 T를 받는 Rectangle 클래스 입니다. T의 타입은 설정되지 않았기 때문에 어떤 타입이든 올 수 있습니다.

class Rectangle<T>(val width: T, val height: T) {
}

아래 코드는 위에서 정의한 Rectangle 클래스를 생성하는 코드입니다. 객체를 생성할 때 Rectangle<Double>처럼 T의 타입이 무엇인지 써줘야 합니다.

fun main(args: Array<String>) {
    val rec = Rectangle<Double>(10, 20)
    val rec1 = Rectangle<String>("aa", "bb")
}

하지만 코틀린은 전달된 인자로 부터 T의 타입을 추론하기 때문에 아래와 같이 Rectangle만 써줘도 됩니다. 명시적으로 T의 타입을 써주고 싶으시면 위처럼 써주시면 됩니다.

fun main(args: Array<String>) {
    val rec = Rectangle(10, 20)
    val rec1 = Rectangle("aa", "bb")
}

만약 두개 이상의 다른 타입의 변수를 Generic으로 정의하려면 <T, K>처럼 두개의 변수를 써주면 됩니다.

class Rectangle<T, K>(val width: T, val height: T, val name: K) {
}

Constraints(제한, 제약)

위에서 정의한 클래스의 문제점은 Rectangle("aa", "bb")와 같이 숫자가 아닌 인자도 허용이 된다는 것입니다. 우리는 width와 height 변수가 숫자만 허용되도록 만들고 싶습니다. 아래 코드에서 <T: Number>는 super type이 Number인 객체만 T로 받도록 허용합니다.

class Rectangle<T: Number>(val width: T, val height: T) {
    fun getArea(): T {
        return (width.toDouble() * height.toDouble()) as T
    }
}

아래 코드에서 Rectangle을 생성할 때, 인자가 String인 경우 컴파일 에러가 발생하고 Int와 Double 일 때 객체가 생성된 것을 볼 수 있습니다. (Int와 Double는 Number 클래스를 상속받았습니다)

fun main(args: Array<String>) {
    val rec = Rectangle(10, 20)
    val rec1 = Rectangle(10.5, 20.5)
//    val rec2 = Rectangle("aa", "bb") // compile error
}

2개 이상의 Constraints

바로 위에서 T에 Number만 가능하도록 제약을 주었습니다. 추가로 더 많은 제약을 줄 수 있습니다. 아래 코드에서 T는 Number를 상속받고, Comparable을 구현한 객체로 제한하였습니다. 두가지 이상의 제약을 걸려면 아래 처럼 where를 사용해야 합니다.

class Rectangle<T>(val width: T, val height: T)
        where T: Number, T: Comparable<T> {

    fun getArea(): T {
        return (width.toDouble() * height.toDouble()) as T
    }
}

클래스는 1개의 클래스만 상속받을 수 있기 때문에, 2개 이상의 제약은 1개의 클래스와 1개 이상의 인터페이스가 됩니다.

Invariance(불변성)

DoubleNumber를 상속하고, Double의 Super class는 Number입니다.

class Double : Number, Comparable<Double>

하지만 Rectangle<Dobule>의 Super class는 Rectangle<Number>가 아닙니다. 두개의 타입이 서로 상속 관계이지만, Generic 클래스의 상속 관계는 아니라는 것을 Invariance(불변성)라고 합니다.

코틀린에서는 Generic의 모든 타입은 Invariance입니다. Invariance의 반대말은 Covariance인데, in/out 키워드로 Generics를 Covariance로 변경할 수 있습니다. in/out 키워드는 아래에서 설명합니다.

Covariance(공변성, 함께 변하는 속성)

Covariance(공변성)은 Invariance(불변성)의 반대입니다. NumberDouble의 super class일 때 Rectangle<Number>Rectangle<Dobule>의 super class이면, 이것을 Covariance(공변성)라고 합니다.

out 키워드

out 키워드는 두개의 타입이 Invariance일 때, Covariance로 만들어줍니다. 즉, out 키워드는 Rectangle<Number>Rectangle<Dobule>의 super class가 되도록 합니다.

아래 코드는 Rectangle<Double> 객체를 Rectangle<Number>에 대입하는 코드입니다. 이 코드는 빌드에러가 발생합니다.

class Rectangle<T: Number>(val width: T, val height: T) {
}

fun main(args: Array<String>) {
  val derivedClass = Rectangle<Double>(10.5, 20.5)
  val baseClass : Rectangle<Number> = derivedClass
}

하지만, T 앞에 out을 붙여주면 컴파일이 됩니다. 바로 out이 타입의 상속구조가 Generic의 상속구조와 같다는 것을 정의하였기 때문입니다. 컴파일러는 Rectangle<Double>Rectangle<Number>의 하위 클래스라고 인식하고 있습니다.

class Rectangle<out T: Number>(val width: T, val height: T) {
}

fun main(args: Array<String>) {
  val derivedClass = Rectangle<Double>(10.5, 20.5)
  val baseClass : Rectangle<Number> = derivedClass
}

Contravariance(반공변성)

Contravariance는 Covariance의 반대 방향으로 공변성 조건을 만족하는 것을 말합니다. 위의 예제에서, NumberDouble의 super class일 때 Rectangle<Dobule>Rectangle<Number>의 super class라면 Contravariance(반공변성)라고 합니다.

in 키워드

in 키워드는 out 키워드의 반대입니다. in은 타입의 상위/하위 클래스 구조가 Generic에서는 반대 방향의 상위/하위 클래스 구조를 갖는다는 것을 정의합니다. 즉, 두 타입의 관계가 Contravariance임을 나타낼 때 사용합니다.

예제를 보시면, 아래 코드는 T 앞에 in을 정의하였습니다. 그래서 Rectangle<Double>Rectangle<Number>의 상위 클래스가 되었습니다.

class Rectangle<in T: Number>(val width: T, val height: T) {
}

fun main(args: Array<String>) {
    val baseClass = Rectangle<Number>(10.5, 20.5)
    val derivedClass : Rectangle<Double> = baseClass
}

정리

Generics로 함수와 클래스를 정의하는 방법에 대해서 알아보았습니다. Invariant, Invariant, Covariant의 관계를 UML로 표현하면 아래처럼 표현할 수 있습니다. Invariant만 관계가 없고, 나머지는 화살표 방향이 상속 관계입니다.

generics association

참고

Loading script...
codechachaCopyright ©2019 codechacha