๐ŸŽ“ A1Learner
๐Ÿ”
0 of 37 lessons completed

Kotlin Generic Constraints

A bound limits which types a type parameter can accept. Variance sets how generic types with related arguments fit together.

A Comparable bound

The bound T : Comparable<T> lets biggest compare its three values. Whole numbers and words both meet the bound.

fun <T : Comparable<T>> biggest(first: T, second: T, third: T): T {
    var best = first
    if (second > best) {
        best = second
    }
    if (third > best) {
        best = third
    }
    return best
}

fun main() {
    println(biggest(3, 9, 4))
    println(biggest("ant", "cat", "bee"))
}

A where clause with two bounds

The where clause asks for values that are both Comparable and Number. Whole numbers and decimal numbers both meet the two bounds.

fun <T> largestScore(values: List<T>): T where T : Comparable<T>, T : Number {
    var best = values[0]
    for (value in values) {
        if (value > best) {
            best = value
        }
    }
    return best
}

fun main() {
    println(largestScore(listOf(2, 8, 5)))
    println(largestScore(listOf(1.5, 3.5, 2.5)))
}

Producing values with out

Source only hands values out, so it marks its type with out. A Source of String can stand where a Source of Any is expected.

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

class WordSource(private val word: String) : Source<String> {
    override fun next(): String {
        return word
    }
}

fun main() {
    val words: Source<String> = WordSource("hello")
    val anything: Source<Any> = words
    println(anything.next())
}

Taking values in with in

Sink only takes values in, so it marks its type with in. A Sink of Any can stand where a Sink of String is expected.

interface Sink<in T> {
    fun accept(value: T)
}

class AnySink : Sink<Any> {
    override fun accept(value: Any) {
        println(value)
    }
}

fun main() {
    val anySink: Sink<Any> = AnySink()
    val stringSink: Sink<String> = anySink
    stringSink.accept("note")
}

A type outside the bound fails

Token has no order, so it does not meet the Comparable bound. The compiler cannot infer T and then reports that a Token does not match the expected Comparable type.

class Token(val label: String)

fun <T : Comparable<T>> biggest(first: T, second: T): T {
    return if (first >= second) first else second
}

fun main() {
    val left = Token("a")
    val right = Token("b")
    println(biggest(left, right))
}

Example

fun <T : Comparable<T>> smallest(values: List<T>): T {
    var best = values[0]
    for (value in values) {
        if (value < best) {
            best = value
        }
    }
    return best
}

fun main() {
    println(smallest(listOf(7, 2, 9)))
    println(smallest(listOf("pear", "fig", "plum")))
}

Key points

  • A bound sits after the type parameter with a colon and a type name.
  • A Comparable bound lets the body compare values with < and >.
  • A where clause lists more than one bound for a type parameter.
  • The out keyword marks a type that only produces values of its type.
  • The in keyword marks a type that only takes in values of its type.
  • A call that breaks a bound fails at compile time.

In this example, smallest walks a list and keeps the smallest value it has seen. The bound lets the body compare each value with <. The number list gives 2 and the word list gives fig.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Kotlin Playground

Exercise

What does the bound in fun <T : Comparable<T>> biggest(a: T, b: T) require?