Kotlin Generics

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 class Box with a type parameter T.
  • val intBox = Box(10): Creates an instance of Box with Int type.
  • val stringBox = Box("Hello"): Creates an instance of Box with String 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 function identity with a type parameter T.
  • identity(10): Calls the generic function with an Int value.
  • identity("Hello"): Calls the generic function with a String 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 function sum with a type parameter T that is constrained to be a subtype of Number.
  • sum(10, 20): Calls the generic function with Int values.
  • sum("Hello", "World"): Causes a compile-time error because String is not a subtype of Number.

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 parameter T.
  • val anyProducer: Producer<Any> = stringProducer: Covariance allows Producer<String> to be assigned to Producer<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 parameter T.
  • val anyConsumer: Consumer<Any> = stringConsumer: Contravariance allows Consumer<Any> to be assigned to Consumer<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 parameter T.
  • isOfType<String>("Hello"): Checks if the value is of type String.

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.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top