Introduction
Generics in Kotlin allow you to create classes, interfaces, and functions that can operate on any data type while maintaining type safety. Generics enable you to write more flexible and reusable code. This chapter will cover the basics of generics, including how to define generic classes and functions, use generic constraints, and understand variance.
Generic Classes
A generic class is a class that can work with any data type. You define a generic class by placing a type parameter in angle brackets after the class name.
Syntax
class ClassName<T>(val property: T)
Example
fun main() {
val intBox = Box(10)
val stringBox = Box("Hello")
println(intBox.value) // Prints: 10
println(stringBox.value) // Prints: Hello
}
class Box<T>(val value: T)
Explanation:
class Box<T>(val value: T)
: Defines a generic classBox
with a type parameterT
.val intBox = Box(10)
: Creates an instance ofBox
withInt
type.val stringBox = Box("Hello")
: Creates an instance ofBox
withString
type.
Output:
10
Hello
Generic Functions
A generic function is a function that can operate on any data type. You define a generic function by placing a type parameter in angle brackets before the function name.
Syntax
fun <T> functionName(parameter: T): T {
// Function body
}
Example
fun main() {
println(identity(10)) // Prints: 10
println(identity("Hello")) // Prints: Hello
}
fun <T> identity(value: T): T {
return value
}
Explanation:
fun <T> identity(value: T): T
: Defines a generic functionidentity
with a type parameterT
.identity(10)
: Calls the generic function with anInt
value.identity("Hello")
: Calls the generic function with aString
value.
Output:
10
Hello
Generic Constraints
You can impose constraints on a type parameter to restrict the types that can be used as arguments. This is done using the where
keyword or by specifying a type bound.
Syntax
fun <T : Number> functionName(parameter: T): T {
// Function body
}
Example
fun main() {
println(sum(10, 20)) // Prints: 30
// println(sum("Hello", "World")) // Compile-time error
}
fun <T : Number> sum(a: T, b: T): Double {
return a.toDouble() + b.toDouble()
}
Explanation:
fun <T : Number> sum(a: T, b: T): Double
: Defines a generic functionsum
with a type parameterT
that is constrained to be a subtype ofNumber
.sum(10, 20)
: Calls the generic function withInt
values.sum("Hello", "World")
: Causes a compile-time error becauseString
is not a subtype ofNumber
.
Output:
30.0
Variance
Variance in Kotlin generics describes how subtyping between more complex types relates to subtyping between their components. Kotlin provides in
and out
keywords to define variance.
Covariance (out
)
A type parameter is covariant if it is only used as an output (return type). Use the out
keyword to make a type parameter covariant.
Example
fun main() {
val stringProducer: Producer<String> = Producer("Hello")
val anyProducer: Producer<Any> = stringProducer // Covariance
println(anyProducer.produce()) // Prints: Hello
}
class Producer<out T>(private val value: T) {
fun produce(): T {
return value
}
}
Explanation:
class Producer<out T>(private val value: T)
: Defines a covariant type parameterT
.val anyProducer: Producer<Any> = stringProducer
: Covariance allowsProducer<String>
to be assigned toProducer<Any>
.
Output:
Hello
Contravariance (in
)
A type parameter is contravariant if it is only used as an input (parameter type). Use the in
keyword to make a type parameter contravariant.
Example
fun main() {
val stringConsumer: Consumer<String> = Consumer()
val anyConsumer: Consumer<Any> = stringConsumer // Contravariance
anyConsumer.consume("Hello") // Works because Any can accept a String
}
class Consumer<in T> {
fun consume(item: T) {
println("Consumed: $item")
}
}
Explanation:
class Consumer<in T>
: Defines a contravariant type parameterT
.val anyConsumer: Consumer<Any> = stringConsumer
: Contravariance allowsConsumer<Any>
to be assigned toConsumer<String>
.
Output:
Consumed: Hello
Reified Type Parameters
Kotlin supports reified type parameters in inline functions. Reified type parameters allow you to use the type information at runtime, which is normally erased.
Example
fun main() {
println(isOfType<String>("Hello")) // Prints: true
println(isOfType<String>(123)) // Prints: false
}
inline fun <reified T> isOfType(value: Any): Boolean {
return value is T
}
Explanation:
inline fun <reified T> isOfType(value: Any): Boolean
: Defines an inline function with a reified type parameterT
.isOfType<String>("Hello")
: Checks if the value is of typeString
.
Output:
true
false
Example Program with Generics
Here is an example program that demonstrates various aspects of using generics in Kotlin:
fun main() {
// Generic classes
val intBox = Box(10)
val stringBox = Box("Hello")
println(intBox.value)
println(stringBox.value)
// Generic functions
println(identity(10))
println(identity("Hello"))
// Generic constraints
println(sum(10, 20))
// println(sum("Hello", "World")) // Compile-time error
// Variance
val stringProducer: Producer<String> = Producer("Hello")
val anyProducer: Producer<Any> = stringProducer
println(anyProducer.produce())
val stringConsumer: Consumer<String> = Consumer()
val anyConsumer: Consumer<Any> = stringConsumer
anyConsumer.consume("Hello")
// Reified type parameters
println(isOfType<String>("Hello"))
println(isOfType<String>(123))
}
// Generic class
class Box<T>(val value: T)
// Generic function
fun <T> identity(value: T): T {
return value
}
// Generic constraints
fun <T : Number> sum(a: T, b: T): Double {
return a.toDouble() + b.toDouble()
}
// Covariance
class Producer<out T>(private val value: T) {
fun produce(): T {
return value
}
}
// Contravariance
class Consumer<in T> {
fun consume(item: T) {
println("Consumed: $item")
}
}
// Reified type parameters
inline fun <reified T> isOfType(value: Any): Boolean {
return value is T
}
Output:
10
Hello
10
Hello
30.0
Hello
Consumed: Hello
true
false
Conclusion
In this chapter, you learned about generics in Kotlin, including how to define generic classes and functions, use generic constraints, understand covariance and contravariance, and utilize reified type parameters. Generics enable you to write flexible and reusable code while maintaining type safety. Understanding and applying generics is crucial for writing robust and maintainable Kotlin programs.