Generics
Why generics?
Imagine writing a “box” that can hold a value. Without generics you’d write one box for Int, another for String, and so on. Generics let you write it once and use it with any type, while keeping full type safety — the compiler still knows exactly what’s inside.
class Box<T>(val value: T)
val intBox = Box(42) // Box<Int>
val strBox = Box("hello") // Box<String>
println(intBox.value + 1) // compiler knows it's an Int
Generic functions
fun <T> firstOrNull(list: List<T>): T? =
if (list.isEmpty()) null else list[0]
val n = firstOrNull(listOf(1, 2, 3)) // Int?
val s = firstOrNull(listOf("a", "b")) // String?
Constraints: limiting the type
Sometimes a type must have certain abilities. A constraint says “T must be a kind of X”:
fun <T : Comparable<T>> maxOf(a: T, b: T): T =
if (a > b) a else b // works because T can be compared
maxOf(3, 9) // 9
maxOf("apple", "kiwi") // kiwi
Generic classes in real life
// a reusable result wrapper used across many apps
sealed class ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>()
data class Error(val message: String) : ApiResult<Nothing>()
}
fun handle(result: ApiResult<User>) = when (result) {
is ApiResult.Success -> show(result.data) // data is a User
is ApiResult.Error -> showError(result.message)
}
in and out (a gentle intro)
You’ll sometimes see out T (the type is only produced/returned, like a read-only list) and in T (the type is only consumed/passed in). These “variance” keywords let generic types be used more flexibly and safely. As a beginner, just know List<out T> is why a List<Dog> can be used where a List<Animal> is expected.
Common mistakes
- Reaching for
Anyand casting everywhere instead of using a generic type. - Forgetting constraints — if you need to compare or add, constrain the type.
- Over-engineering — only make something generic when it’s genuinely reused.
Summary: Generics (<T>) let you write one reusable, type-safe piece of code for any type. Add constraints (T : Comparable<T>) when the type needs certain abilities.