Skip to main content

Kotlin Generic Usage Patterns

Generics are a powerful feature in Kotlin that allows you to write flexible and type-safe code. In this guide, we'll explore common patterns for using generics effectively in your Kotlin projects. You'll learn how to leverage generics for better code organization, reusability, and type safety.

Introduction to Generic Patterns

Generics in Kotlin let you create classes, interfaces, and functions that operate on types that are specified later. While the basic syntax may be straightforward, applying generics effectively requires understanding common patterns and best practices.

Let's explore some of the most useful generic patterns in Kotlin!

Pattern 1: Type Parameter Constraints

One of the most useful patterns is constraining type parameters to ensure they have certain capabilities.

Basic Constraints with where and Upper Bounds

kotlin
// Using upper bound
fun <T : Comparable<T>> findMax(list: List<T>): T? {
if (list.isEmpty()) return null
var max = list[0]
for (item in list.drop(1)) {
if (item > max) max = item
}
return max
}

// Example usage
fun main() {
val numbers = listOf(1, 3, 2, 5, 4)
val strings = listOf("apple", "banana", "cherry")

println("Max number: ${findMax(numbers)}") // Output: Max number: 5
println("Max string: ${findMax(strings)}") // Output: Max string: cherry
}

In this example, the T type is constrained to types that implement Comparable<T>, ensuring we can use the > operator.

Multiple Constraints

When a type needs to fulfill multiple interfaces:

kotlin
// Multiple constraints using where clause
fun <T> processEntity(entity: T) where T : Serializable, T : Comparable<T> {
// Now we can use both Serializable and Comparable methods on entity
}

Pattern 2: Type Erasure Workarounds

Kotlin generics use type erasure at runtime. Here's how to work around that:

Reified Type Parameters

kotlin
// Without reification - we can't access T's class information
fun <T> parseNonReified(json: String): T {
// We can't do: if (T::class == String::class)
// Type information about T is erased at runtime
return deserialize(json) // Implementation would require passing Class<T>
}

// With reification - we can access T's class information
inline fun <reified T> parse(json: String): T {
// Now we can do:
when (T::class) {
String::class -> println("Parsing as String")
Int::class -> println("Parsing as Int")
else -> println("Parsing as other type")
}
return deserialize<T>(json) // Implementation can use T::class
}

// Usage example
inline fun <reified T> deserialize(json: String): T {
// In a real implementation, this would use T::class
return when (T::class) {
String::class -> "Parsed string" as T
Int::class -> 42 as T
else -> throw IllegalArgumentException("Unsupported type")
}
}

fun main() {
val str: String = parse("{\"value\":\"test\"}")
val num: Int = parse("{\"value\":42}")

println(str) // Output: Parsed string
println(num) // Output: 42
}

The reified keyword, combined with inline, allows us to access type information at runtime.

Pattern 3: Generic Extension Functions

You can add generic functionality to existing types:

kotlin
// Generic extension function for any List
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null

// Generic extension function with receiver constraint
fun <T : Comparable<T>> List<T>.isSorted(): Boolean {
if (size <= 1) return true
for (i in 1 until size) {
if (this[i] < this[i-1]) return false
}
return true
}

fun main() {
val numbers = listOf(1, 2, 3, 4)
val emptyList = emptyList<String>()

println("Second element: ${numbers.secondOrNull()}") // Output: Second element: 2
println("Second element of empty list: ${emptyList.secondOrNull()}") // Output: Second element of empty list: null

println("Is [1,2,3,4] sorted? ${numbers.isSorted()}") // Output: Is [1,2,3,4] sorted? true
println("Is [3,1,2] sorted? ${listOf(3,1,2).isSorted()}") // Output: Is [3,1,2] sorted? false
}

Pattern 4: Type-Safe Builders with Generics

Generics are excellent for creating type-safe builders:

kotlin
class RequestBuilder<T> {
private var url: String = ""
private var responseHandler: ((T) -> Unit)? = null

fun url(url: String): RequestBuilder<T> {
this.url = url
return this
}

fun onResponse(handler: (T) -> Unit): RequestBuilder<T> {
this.responseHandler = handler
return this
}

fun execute() {
println("Executing request to $url")
// In a real implementation, we would make the network request
// and parse the response as type T
}
}

// Extension function to create a request
inline fun <reified T> request(): RequestBuilder<T> = RequestBuilder()

fun main() {
// Type-safe builder for string response
request<String>()
.url("https://api.example.com/data")
.onResponse { response ->
// Here 'response' is known to be a String
println("Got response: $response")
}
.execute()

// Type-safe builder for Int response
request<Int>()
.url("https://api.example.com/count")
.onResponse { count ->
// Here 'count' is known to be an Int
println("Count is: $count")
}
.execute()
}

This pattern ensures type safety throughout the builder chain.

Pattern 5: Generic Object Containers

Creating containers that can safely store and retrieve typed objects:

kotlin
class Box<T>(var value: T) {
fun replace(newValue: T): T {
val oldValue = value
value = newValue
return oldValue
}

fun <R> map(transformer: (T) -> R): Box<R> {
return Box(transformer(value))
}
}

fun main() {
// Box containing an integer
val intBox = Box(42)
println("Original value: ${intBox.value}") // Output: Original value: 42

val oldValue = intBox.replace(100)
println("Replaced $oldValue with ${intBox.value}") // Output: Replaced 42 with 100

// Transform the Int box into a String box
val stringBox = intBox.map { it.toString() + " transformed" }
println("Transformed box: ${stringBox.value}") // Output: Transformed box: 100 transformed
}

Pattern 6: Producer-Consumer Pattern (Variance)

Understanding in and out type parameters for handling variance:

kotlin
// Producer - using 'out' for covariance
interface Producer<out T> {
fun produce(): T
}

// Consumer - using 'in' for contravariance
interface Consumer<in T> {
fun consume(item: T)
}

// Example implementations
class IntProducer : Producer<Int> {
override fun produce(): Int = 42
}

class NumberConsumer : Consumer<Number> {
override fun consume(item: Number) {
println("Consumed number: $item")
}
}

fun main() {
// Covariance in action - a Producer<Int> can be used as Producer<Number>
val intProducer: Producer<Int> = IntProducer()
val numberProducer: Producer<Number> = intProducer // This is OK because of 'out'
println("Produced number: ${numberProducer.produce()}") // Output: Produced number: 42

// Contravariance in action - a Consumer<Number> can be used as Consumer<Int>
val numberConsumer: Consumer<Number> = NumberConsumer()
val intConsumer: Consumer<Int> = numberConsumer // This is OK because of 'in'
intConsumer.consume(100) // Output: Consumed number: 100
}

Real-World Application: Repository Pattern

Let's see how generics can improve the repository pattern in a real application:

kotlin
// Domain entity
interface Entity {
val id: Long
}

// Example entity
data class User(
override val id: Long,
val name: String,
val email: String
) : Entity

// Generic repository interface
interface Repository<T : Entity> {
suspend fun getById(id: Long): T?
suspend fun getAll(): List<T>
suspend fun save(entity: T): T
suspend fun update(entity: T): Boolean
suspend fun delete(id: Long): Boolean
}

// Implementation for User entity
class UserRepository : Repository<User> {
private val users = mutableMapOf<Long, User>()

override suspend fun getById(id: Long): User? = users[id]

override suspend fun getAll(): List<User> = users.values.toList()

override suspend fun save(entity: User): User {
users[entity.id] = entity
return entity
}

override suspend fun update(entity: User): Boolean {
if (!users.containsKey(entity.id)) return false
users[entity.id] = entity
return true
}

override suspend fun delete(id: Long): Boolean = users.remove(id) != null
}

// Example service using the repository
class UserService(private val repository: Repository<User>) {
suspend fun registerUser(name: String, email: String): User {
val newUser = User(
id = System.currentTimeMillis(), // Simple ID generation
name = name,
email = email
)
return repository.save(newUser)
}

suspend fun findUserById(id: Long): User? = repository.getById(id)
}

// Main function demonstrating usage
suspend fun main() {
val userRepository = UserRepository()
val userService = UserService(userRepository)

val user = userService.registerUser("John Doe", "[email protected]")
println("Created user: $user")

val retrievedUser = userService.findUserById(user.id)
println("Retrieved user: $retrievedUser")
}

This pattern allows you to create type-safe repositories for different entity types without duplicating code.

Summary

We've covered several powerful patterns for using generics in Kotlin:

  1. Type Parameter Constraints - Enforce capabilities on generic types
  2. Type Erasure Workarounds - Use reified type parameters to access type information
  3. Generic Extension Functions - Add type-safe functionality to existing types
  4. Type-Safe Builders - Create fluent APIs with type safety
  5. Generic Object Containers - Safely store and manipulate typed values
  6. Producer-Consumer Pattern - Apply variance with in and out modifiers
  7. Repository Pattern - Create reusable data access layers

By understanding and applying these patterns, you can write more flexible and reusable code while maintaining type safety.

Additional Resources

Exercises

  1. Create a generic Result<T> class that can represent either a success with a value of type T or a failure with an error message.
  2. Implement a generic Cache<K, V> interface with methods to store, retrieve, and remove values.
  3. Create a type-safe event bus using generics that allows subscribers to listen only to specific event types.
  4. Implement a generic sorting algorithm that can sort any list of comparable objects.
  5. Create a generic data pipeline that can transform data through multiple stages with different types.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)