Kotlin Generics — Type Parameters, Variance & Reified Types
Kotlin's generic type system provides powerful abstractions for writing reusable, type-safe code. This guide covers the essential concepts.
Generic Basics
Type Parameters and Constraints:
// Basic generic class
class Box<T>(val value: T)
// Type constraints
class Comparable<T : Comparable<T>> {
fun compare(other: T): Int = value.compareTo(other)
}
// Multiple constraints
interface Processor<T> where T : Comparable<T>, T : CharSequence
Variance: Covariance and Contravariance
Covariance (out): Permits read-only access
interface Producer<out T> {
fun produce(): T
}
val stringProducer: Producer<String> = object : Producer<String> {
override fun produce() = "Hello"
}
// Safe assignment due to out variance
val anyProducer: Producer<Any> = stringProducer
Contravariance (in): Permits write-only access
interface Consumer<in T> {
fun consume(item: T)
}
val anyConsumer: Consumer<Any> = object : Consumer<Any> {
override fun consume(item: Any) { }
}
// Safe assignment due to in variance
val stringConsumer: Consumer<String> = anyConsumer
Star Projection
Use * when the type doesn't matter:
fun printList(list: List<*>) {
for (item in list) {
println(item)
}
}
val stringList: List<String> = listOf("a", "b")
val intList: List<Int> = listOf(1, 2)
printList(stringList)
printList(intList)
Reified Type Parameters
Access type information at runtime with reified:
inline fun <reified T> parseJson(json: String): T {
val gson = Gson()
return gson.fromJson(json, T::class.java)
}
// Runtime type check with inline + reified
inline fun <reified T> isInstance(value: Any): Boolean {
return value is T
}
// Usage
data class User(val name: String, val age: Int)
val user = parseJson<User>(jsonString)
val isString = isInstance<String>("hello")
Intent with Reified:
inline fun <reified T : Activity> Context.startActivity() {
startActivity(Intent(this, T::class.java))
}
// Usage
startActivity<MainActivity>()
Result Sealed Class Pattern
Type-safe error handling:
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
object Loading : Result<Nothing>()
}
fun <T> fetchData(): Result<T> {
return try {
Result.Success(data)
} catch (e: Exception) {
Result.Error(e)
}
}
// Pattern matching
when (val result = fetchData<User>()) {
is Result.Success -> println(result.data)
is Result.Error -> println(result.exception)
is Result.Loading -> println("Loading")
}
Type-Safe Builder Pattern
class HtmlBuilder {
private val elements = mutableListOf<String>()
fun div(init: HtmlBuilder.() -> Unit) {
elements.add("<div>")
this.init()
elements.add("</div>")
}
fun p(content: String) {
elements.add("<p>$content</p>")
}
fun build() = elements.joinToString("\n")
}
fun html(init: HtmlBuilder.() -> Unit): String {
val builder = HtmlBuilder()
builder.init()
return builder.build()
}
// Usage
val page = html {
div {
p("Welcome")
}
}
Key Takeaways
- Use type parameters for generic reusability
- Apply constraints to limit permissible types
- Leverage variance (out/in) for flexible APIs
- Use reified in inline functions for runtime type access
- Employ sealed classes for type-safe error handling
- Build type-safe DSLs with builder patterns
8 Android app templates: Gumroad
Top comments (0)