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
// 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:
// 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
// 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:
// 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:
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:
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:
// 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:
// 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:
- Type Parameter Constraints - Enforce capabilities on generic types
- Type Erasure Workarounds - Use
reified
type parameters to access type information - Generic Extension Functions - Add type-safe functionality to existing types
- Type-Safe Builders - Create fluent APIs with type safety
- Generic Object Containers - Safely store and manipulate typed values
- Producer-Consumer Pattern - Apply variance with
in
andout
modifiers - 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
- Kotlin Official Documentation on Generics
- Effective Kotlin Book by Marcin Moskala
- Kotlin in Action by Dmitry Jemerov and Svetlana Isakova
Exercises
- Create a generic
Result<T>
class that can represent either a success with a value of typeT
or a failure with an error message. - Implement a generic
Cache<K, V>
interface with methods to store, retrieve, and remove values. - Create a type-safe event bus using generics that allows subscribers to listen only to specific event types.
- Implement a generic sorting algorithm that can sort any list of comparable objects.
- 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! :)