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.