Kotlin Generics

Generics, i.e., "parameterized types", parameterize types and can be used on classes, interfaces, and methods.

Like Java, Kotlin also provides generics to ensure type safety and eliminate the hassle of type casting.

Declare a generic class:

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

When creating an instance of the class, we need to specify the type parameter:

val box: Box<Int> = Box<Int>(1)
// 或者
val box = Box(1) // 编译器会进行类型推断,1 类型 Int,所以编译器知道我们说的是 Box<Int>。

The following example passes integer data and a string to the generic class Box:

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

fun main(args: Array<String>) {
    var boxInt = Box<Int>(10)
    var boxString = Box<String>("Example")

    println(boxInt.value)
    println(boxString.value)
}

The output result is:

10
Example

When defining a generic type variable, you can write out the type parameter in full, or omit it if the compiler can automatically infer the type parameter.

The declaration of Kotlin generic functions is the same as Java's; the type parameter should be placed before the function name:

fun <T> boxIn(value: T) = Box(value)

// 以下都是合法语句
val box4 = boxIn<Int>(1)
val box5 = boxIn(1)     // 编译器会进行类型推断

When calling a generic function, if the type parameter can be inferred, the generic parameter can be omitted.

The following example creates a generic function doPrintln, which handles different types passed in accordingly:

fun main(args: Array<String>) {
    val age = 23
    val name = "example"
    val bool = true

    doPrintln(age)    // 整型
    doPrintln(name)   // 字符串
    doPrintln(bool)   // 布尔型
}

fun <T> doPrintln(content: T) {

    when (content) {
        is Int -> println("整型数字为 $content")
        is String -> println("字符串转换为大写:${content.toUpperCase()}")
        else -> println("T 不是整型,也不是字符串")
    }
}

The output result is:

整型数字为 23
字符串转换为大写:EXAMPLE
T 不是整型,也不是字符串

Generic Constraints

We can use generic constraints to set the allowed types for a given parameter.

In Kotlin, : is used to constrain the upper bound of a generic type.

The most common constraint is the upper bound:

fun <T : Comparable<T>> sort(list: List<T>) {
    // ……
}

ComparableSubtypes of the bound type can replace T. For example:

sort(listOf(1, 2, 3)) // OK。Int 是 Comparable<Int> 的子类型
sort(listOf(HashMap<Int, String>())) // 错误:HashMap<Int, String> 不是 Comparable<HashMap<Int, String>> 的子类型

The default upper bound is Any?.

For multiple upper bound constraints, the where clause can be used:

fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
    where T : CharSequence,
          T : Comparable<T> {
    return list.filter { it > threshold }.map { it.toString() }
}

Variance

Kotlin does not have wildcard types; it has two other things: declaration-site variance and type projections.

Declaration-site Variance

Declaration-site variance uses covariant annotation modifiers: in and out. Consumer in, producer out.

Using out makes a type parameter covariant. A covariant type parameter can only be used as output, and can be used as a return value type but not as an input parameter type:

// 定义一个支持协变的类
class Example<out A>(val a: A) {
    fun foo(): A {
        return a
    }
}

fun main(args: Array<String>) {
    var strCo: Example<String> = Example("a")
    var anyCo: Example<Any> = Example<Any>("b")
    anyCo = strCo
    println(anyCo.foo())   // 输出 a
}

Using in makes a type parameter contravariant. A contravariant type parameter can only be used as input, and can be used as an input parameter type but not as a return value type:

// 定义一个支持逆变的类
class Example<in A>(a: A) {
    fun foo(a: A) {
    }
}

fun main(args: Array<String>) {
    var strDCo = Example("a")
    var anyDCo = Example<Any>("b")
    strDCo = anyDCo
}

Star Projection

Sometimes, you may want to indicate that you do not know any information about the type parameter, but still want to use it safely. The so-called "safe use" here means defining a type projection for a generic type, requiring that all concrete instances of this generic type are subtypes of this projection.

For this problem, Kotlin provides a syntax called star-projection:

  • If the type is defined as Foo<out T>, where T is a covariant type parameter with upper bound TUpper, Foo<> is equivalent to Foo<out TUpper>. It means that when T is unknown, you can safely read a value of type TUpper from Foo<>.
  • If the type is defined as Foo<in T>, where T is a contravariant type parameter, Foo<> is equivalent to Foo<in Nothing>. It means that when T is unknown, you cannot safely write to Foo<> anything.
  • If the type is defined as Foo<T>, where T is a covariant type parameter with upper bound TUpper, for reading values, Foo<*> is equivalent to Foo<out TUpper>; for writing values, it is equivalent to Foo<in Nothing>.

If a generic type has multiple type parameters, each type parameter can be projected independently. For example, if the type is defined as interface Function<in T, out U>, the following star projections can appear:

  1. Function<*, String>, representing Function<in Nothing, String>;
  2. Function<Int, *>, representing Function<Int, out Any?>;
  3. Function<, >, representing Function<in Nothing, out Any?>.

Note: Star projection is very similar to Java's raw types, but it can be used safely.

Other Extensions