Generic Functions

A generic function is a function written once but usable with many different types, without giving up compile-time type safety or resorting to casts. Instead of writing separate printIntTwice, printStringTwice, and printPersonTwice functions, you write one function parameterized by a type placeholder, and the Kotlin compiler fills in the real type at each call site. Generics power Kotlin’s collections, Comparable, scope functions, and most of the standard library, so understanding generic functions is essential to reading and writing idiomatic Kotlin.

Overview: How Generic Functions Work

In a statically typed language, a function’s parameter and return types are fixed at compile time. Without generics, a function that needs to work with any type either gets duplicated per type, or gets typed as Any and cast constantly — giving up compile-time safety and risking runtime ClassCastExceptions. Kotlin generics solve this: a function declares one or more type parameters in angle brackets before its name, e.g. fun <T> identity(value: T): T. T is not a real type; it’s a placeholder that the compiler substitutes with a concrete type — Int, String, Person, whatever — separately for each call.

Most of the time you never write the type argument explicitly, because Kotlin’s compiler performs type inference: it looks at the arguments you pass and works out what T must be. Calling identity(5) infers T = Int; calling identity("hi") infers T = String. You can always be explicit with identity<Int>(5), and sometimes you must be — for example when the compiler can’t infer T purely from the arguments, such as a generic factory call with no parameters.

An important and often-missed detail: an unbounded type parameter like <T> has an implicit upper bound of Any?, Kotlin’s root nullable type. That means T can be inferred as a nullable type unless you constrain it. If you want to guarantee callers can never plug in a nullable type, you must write <T : Any>, bounding T to the non-null root type Any. This surprises people coming from Java, where raw generics never distinguished nullability at all.

You can also constrain what a type parameter is allowed to do with an upper bound: fun <T : Comparable<T>> largerOf(a: T, b: T): T says “T can be any type, as long as it implements Comparable<T>” — which is what lets the function body legally use the > operator (which desugars to compareTo) on values of type T. Without a bound, the compiler only lets you call the members every possible type has — the ones declared on Any?, namely equals, hashCode, and toString — because that’s all it can prove holds for every substitution of T.

Under the hood, Kotlin compiles to JVM bytecode, and the JVM implements generics through type erasure: at runtime there is no T anywhere in the compiled class file — every occurrence of an unbounded T is erased to Object (or to its upper bound, if it has one). This is why you cannot write value is T or construct a T directly inside an ordinary generic function — the runtime has nothing left to check against. Kotlin gives functions an escape hatch: marking a function inline and its type parameter reified makes the compiler copy the function’s bytecode into every call site, substituting the real type argument as a compile-time constant. That is the only way a generic function can perform an is T check or similar reflective operation.

Generic functions are distinct from generic classes like List<T>: a generic function introduces its own type parameter scoped to just that function, and can appear at the top level, as a class member, or as an extension function — it doesn’t require any enclosing class to be generic itself.

Syntax

The general form of a generic function declaration:

fun <T> functionName(param: T): T {
    return param
}

fun <K, V> makePair(key: K, value: V): Pair<K, V> {
    return Pair(key, value)
}

fun <T : Comparable<T>> largest(a: T, b: T): T {
    return if (a > b) a else b
}

inline fun <reified T> isType(value: Any?): Boolean {
    return value is T
}
Part Meaning
<T> Declares a type parameter named T, placed right after fun and before the function name. Any identifier works, but T, K/V (key/value), R (result), and E (element) are conventional.
<K, V> A function can declare more than one type parameter, comma-separated.
param: T Once declared, T is used exactly like any other type in the parameter list and return type.
<T : Comparable<T>> An upper bound restricts what T can be substituted with, and unlocks the members/operators that bound declares.
reified T Only legal on an inline function; keeps the real type available at runtime for checks like is T.

Multiple bounds are possible with a where clause, e.g. fun <T> process(item: T) where T : Comparable<T>, T : CharSequence { ... }, requiring T to satisfy every listed constraint at once.

Examples

Example 1: A basic generic function

fun <T> printTwice(item: T) {
    println(item)
    println(item)
}

fun main() {
    printTwice("Hello")
    printTwice(42)
}

Output:

Hello
Hello
42
42

The same printTwice function is called with a String and then an Int. The compiler infers T = String for the first call and T = Int for the second, generating type-safe calls without any casting or duplicated code.

Example 2: Returning a value of the generic type

fun <T> headOrNull(list: List<T>): T? {
    return if (list.isEmpty()) null else list[0]
}

fun main() {
    val numbers = listOf(10, 20, 30)
    val empty = emptyList<String>()
    println(headOrNull(numbers))
    println(headOrNull(empty))
}

Output:

10
null

headOrNull returns T? — a nullable version of whatever T is inferred to be — because there might not be a first element. For numbers, T is inferred as Int and the return type becomes Int?; for empty, T is inferred as String from the explicit emptyList<String>() call, and the function correctly returns null instead of crashing on an out-of-bounds access.

Example 3: A bounded type parameter

fun <T : Comparable<T>> largerOf(a: T, b: T): T {
    return if (a > b) a else b
}

fun main() {
    println(largerOf(3, 7))
    println(largerOf("banana", "apple"))
    println(largerOf(3.14, 2.71))
}

Output:

7
banana
3.14

The bound T : Comparable<T> tells the compiler that whatever T ends up being, it must implement Comparable<T> — which Int, String, and Double all do. That is what makes a > b legal inside the function body: the > operator is sugar for a.compareTo(b) > 0, and compareTo only exists because of the bound. Without it, the function would not compile (see Common Mistakes below).

Example 4: Reified type parameters

inline fun <reified T> isType(value: Any?): Boolean {
    return value is T
}

fun main() {
    println(isType<String>("hello"))
    println(isType<String>(42))
    println(isType<Int>(42))
}

Output:

true
false
true

Ordinary generic functions cannot check value is T because the JVM erases T at runtime. Marking the function inline and the type parameter reified tells the compiler to paste the function’s body directly into each call site, where the real type argument (String, then Int) is known and substituted in literally — so the is T check becomes a concrete is String or is Int check by the time it compiles. This trick only works on inline functions, which is why reified can never appear on a non-inline function or on a class’s own type parameters.

How It Works Step by Step

Walking through the call largerOf(3, 7) from Example 3:

  1. The compiler sees the call site largerOf(3, 7) and looks at the argument types: both are Int.
  2. It infers the type parameter T = Int for this particular call.
  3. It checks the bound: does Int implement Comparable<Int>? Yes — so the substitution is legal and compilation proceeds.
  4. Inside the function body, a > b resolves to a.compareTo(b) > 0, using Int‘s own compareTo implementation.
  5. At the bytecode level, the JVM erases the generic signature: the compiled method effectively works with the erasure of the bound (Comparable) rather than Int specifically. Kotlin inserts the necessary boxing so this is invisible and safe from the calling code’s point of view.
  6. The second call, largerOf("banana", "apple"), repeats the same process independently with T = String: the compiler re-checks the bound for String, and the single compiled method is reused — there is only one method in the class file, not one per type, because type erasure means the erased method body is identical regardless of which type T was.

This is the core trade-off of JVM generics: compile-time type safety and zero source duplication, at the cost of the type information not existing at runtime — unless you opt into reified on an inline function, as in Example 4, where the compiler avoids erasure entirely by generating a specialized copy of the code at each call site.

Common Mistakes

Mistake 1: Trying to check the type parameter at runtime

Without reified, T is erased, so an is T check inside an ordinary generic function is illegal:

fun <T> isInstanceOfT(value: Any?): Boolean {
    return value is T
    // Error: Cannot check for instance of erased type: T
}

The fix is to make the function inline and the type parameter reified, as shown in Example 4, so the real type is baked into each call site rather than looked up at runtime.

Mistake 2: Forgetting a bound before using operators or members

fun <T> biggerUnbounded(a: T, b: T): T {
    return if (a > b) a else b
    // Error: operator '>' cannot be applied to 'T' and 'T'
}

An unbounded T only guarantees the members of Any?, and > (i.e. compareTo) is not one of them. The fix is to add the bound that supplies the operator:

fun <T : Comparable<T>> biggerBounded(a: T, b: T): T {
    return if (a > b) a else b
}

Mistake 3: Assuming an unbounded T excludes null

fun <T> identity(value: T): T = value

fun main() {
    val result = identity<String?>(null)
    println(result)
}

Output:

null

This compiles because an unconstrained <T> has an implicit upper bound of Any?, so nothing stops a caller from instantiating T as String? and passing null. If the intent was for identity to only ever accept non-null values, the fix is to bound T to the non-null root type:

fun <T : Any> identity(value: T): T = value

fun main() {
    val result = identity(null)
    // Error: Null can not be a value of a non-null type T
}

With T : Any, the compiler now refuses to instantiate T with a nullable type, catching the mistake before the program ever runs.

Best Practices

  • Use standard single-letter names (T, K/V, R, E) for simple, self-explanatory type parameters; use a short descriptive name only when a function has several type parameters and single letters get confusing.
  • Add the narrowest bound that makes your function body compile — <T : Comparable<T>> instead of leaving T unbounded and casting, and <T : Any> whenever null genuinely should not be a valid argument.
  • Let type inference do the work at call sites; only supply an explicit type argument (function<Int>(...)) when the compiler cannot infer it from the arguments, such as a no-argument generic factory.
  • Reach for inline fun <reified T> only when you actually need runtime type information (is T, T::class); overusing inline bloats bytecode since the function body is copied to every call site.
  • Prefer a generic function over accepting Any and casting internally — casting defers errors to runtime, while generics catch type mismatches at compile time.
  • Keep generic functions small and focused; if you find yourself adding many bounds and where clauses, consider whether a sealed class hierarchy or separate overloads would be clearer.

Practice Exercises

  1. Write a generic function fun <T> swap(pair: Pair<T, T>): Pair<T, T> that returns a new pair with the two elements swapped. Test it with a Pair<Int, Int> and a Pair<String, String>.
  2. Write a bounded generic function fun <T : Comparable<T>> smallestOf(items: List<T>): T? that returns the smallest element in a list, or null if the list is empty. Hint: track a running minimum, comparing with <.
  3. Write inline fun <reified T> countInstances(items: List<Any?>): Int that counts how many elements of a mixed list are of type T. Expected output for countInstances<String>(listOf(1, "a", 2, "b", "c")) is 3.

Summary

  • A generic function declares one or more type parameters, e.g. <T>, right after fun, letting one function body work for many types safely.
  • The compiler infers type arguments from call-site arguments in almost all cases; explicit type arguments are only needed when inference cannot determine them.
  • An unbounded <T> has an implicit upper bound of Any?, meaning T can be nullable unless you write <T : Any>.
  • An upper bound like <T : Comparable<T>> both restricts what types can be substituted and unlocks the members/operators of that bound inside the function.
  • The JVM erases generic type information at runtime; checking value is T only works when the function is inline and the type parameter is reified.
  • Prefer generic functions over Any plus manual casting — generics move type errors from runtime crashes to compile-time errors.