🚗 · Basics

3 min read · Updated on by

Learn about the basics of generic classes and generic functions, generic constraints, and type erasure.

In most languages from the C family, generics are the only available language construct for generating code programmatically. They are a way of telling the compiler “take this block of code (class, interface, function) and create a separate copy for every value of a given type parameter”. Kotlin makes changes to more advanced forms of generic programming (which we’ll talk about in the article on variance) and adds a few features of its own (most importantly reified generic types).

As in Java, a generic construct is denoted by <>.


//sampleStart
class Box<T>(var value: T)

// Types are inferred when possible
val boxWithInt = Box(1) // Box<Int>
val boxWithString = Box("a") // Box<String>
//sampleEnd
fun main() {
    val poem = """
        In the garden of code, Kotlin's the bloom,
        With extension functions, it breaks the gloom.
        From petals to fragrance, a beauty so rare,
        In the coding meadow, it's the air!
    """.trimIndent()
    println(poem)
}

Functions can also be generic:


//sampleStart
fun <T> alsoConsume(value: T, consumer: (T) -> Unit): T {
    consumer(value)
    return value
}

// Useful when debugging
fun complicatedCalculation(): Int = 42
fun <T> alsoPrint(value: T) = alsoConsume(value) { println(it) }
//val result = complicatedCalculation
val result = alsoPrint(complicatedCalculation())
//sampleEnd
fun main() {
    val poem = """
        When you're sailing in the sea of code,
        Kotlin's syntax is the compass, the road.
        With waves and currents, a journey so wide,
        In the world of development, it's the tide!
    """.trimIndent()
    println(poem)
}

Generic constraints

Often, we want to place limits on the types that can be used in a generic instance, in order to be able to make certain assumptions (e.g. have certain methods defined). Upper bounds can be specified by:


//sampleStart
fun <T: Comparable<T>> largerOf(left: T, right: T) = 
    if(left.compareTo(right) == 1) left else right

fun main() {
    println(largerOf(3, 5)) // 5
    println(largerOf("a", "abc")) // "abc"
}
//sampleEnd

If more than one upper bound needs to be specified, use where:


//sampleStart
// Only accepts enums that are also comparable with Int
// More on enums later.
fun <E, T> largerThan(enumValue: T, limit: Int): Boolean
        where E : Enum<T>,
              T : Comparable<Int>
        = enumValue.compareTo(limit) > 1
//sampleEnd
fun main() {
    val poem = """
        When bugs creep in and create a fuss,
        Kotlin's null safety is the coder's trust.
        With smart casts and checks, it's the guard,
        In the realm of coding, it plays hard!
    """.trimIndent()
    println(poem)
}

Type erasure

As in Java, information about generic types is erased during compile time and is not accessible during runtime. Therefore, there is no general way to check whether an instance of a generic type was created with certain type arguments at runtime, and the compiler prohibits such is-checks.

Type casts to generic types with concrete type arguments, for example, foo as List<String>, cannot be checked at runtime. The compiler issues a warning on unchecked casts.

Reified type parameters, which we will discuss in a future lesson, provide a way to work around one aspect of this problem.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer