Kotlin Generic Functions
Introduction
Generic functions are a powerful feature in Kotlin that allow you to write code that can work with different types while maintaining type safety. Rather than writing separate functions for each data type, you can write a single generic function that can handle multiple types. This promotes code reuse, type safety, and makes your code more flexible.
In this tutorial, you'll learn how to define and use generic functions in Kotlin, understand type parameters, and see practical applications of generic functions in real-world scenarios.
Basic Generic Functions
What Are Generic Functions?
A generic function is a function that is declared with type parameters. These type parameters act as placeholders for specific types that will be provided when the function is called.
Syntax for Generic Functions
Here's the basic syntax for defining a generic function in Kotlin:
fun <T> functionName(parameter: T): T {
// Function body
return parameter
}
The <T>
before the function name declares a type parameter named T
. You can use any name for your type parameter, but conventionally single uppercase letters like T, E, R are used.
Simple Example: A Generic Print Function
Let's create a simple generic function that prints any type of element:
fun <T> printItem(item: T) {
println("Item value: $item")
}
fun main() {
printItem("Hello, Generics!") // Works with String
printItem(42) // Works with Int
printItem(3.14) // Works with Double
printItem(true) // Works with Boolean
}
Output:
Item value: Hello, Generics!
Item value: 42
Item value: 3.14
Item value: true
In this example, printItem
is a generic function that can accept any type of parameter. The compiler infers the type based on what's passed to the function.
Multiple Type Parameters
You can define functions with multiple type parameters when you need to work with multiple generic types.
fun <T, U> printPair(first: T, second: U) {
println("First: $first (${first::class.simpleName}), Second: $second (${second::class.simpleName})")
}
fun main() {
printPair("Hello", 123)
printPair(3.14, listOf(1, 2, 3))
}
Output:
First: Hello (String), Second: 123 (Int)
First: 3.14 (Double), Second: [1, 2, 3] (ArrayList)
Type Constraints
Sometimes you want to restrict the types that can be used with your generic function. Type constraints allow you to specify that a type parameter must be a specific type or inherit from a specific type.
Basic Type Constraints
Here's how to constrain a type parameter:
fun <T : Number> doubleValue(value: T): Double {
return value.toDouble() * 2
}
fun main() {
println(doubleValue(5)) // Int
println(doubleValue(3.14)) // Double
println(doubleValue(10.5f)) // Float
// This would cause a compilation error:
// println(doubleValue("Hello")) // String is not a Number
}
Output:
10.0
6.28
21.0
In this example, the <T : Number>
constraint specifies that T
must be a Number
or a subtype of Number
. This allows us to call toDouble()
on the value.
Multiple Constraints
You can also specify multiple constraints using the where
clause:
interface Printable {
fun print()
}
class PrintableInt(val value: Int) : Printable {
override fun print() {
println("PrintableInt: $value")
}
}
fun <T> printAndProcess(item: T) where T : Printable, T : Comparable<T> {
item.print()
println("This item is also Comparable")
}
fun main() {
val printableInt = PrintableInt(42)
// This would work if PrintableInt also implemented Comparable
// printAndProcess(printableInt)
}
In this example, the function requires that T
implements both the Printable
and Comparable
interfaces.
Type Inference with Generic Functions
In most cases, Kotlin can infer the type parameters from the arguments you pass to a generic function:
fun <T> createList(vararg items: T): List<T> {
return items.toList()
}
fun main() {
// Type inference works here
val stringList = createList("apple", "banana", "cherry")
val intList = createList(1, 2, 3, 4, 5)
println(stringList) // List<String>
println(intList) // List<Int>
// You can also specify the type explicitly
val explicitList = createList<Double>(1.1, 2.2, 3.3)
println(explicitList)
}
Output:
[apple, banana, cherry]
[1, 2, 3, 4, 5]
[1.1, 2.2, 3.3]
Extension Functions with Generics
You can combine the power of extension functions with generics to create very flexible and reusable code:
fun <T> T.printWithType(): T {
println("Value: $this (Type: ${this!!::class.simpleName})")
return this
}
fun main() {
"Hello".printWithType()
42.printWithType()
listOf(1, 2, 3).printWithType()
}
Output:
Value: Hello (Type: String)
Value: 42 (Type: Int)
Value: [1, 2, 3] (Type: ArrayList)
Practical Examples
Example 1: Generic Swap Function
A function that swaps two elements in a mutable list:
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val tmp = this[index1]
this[index1] = this[index2]
this[index2] = tmp
}
fun main() {
val numbers = mutableListOf(1, 2, 3, 4)
println("Before swap: $numbers")
numbers.swap(0, 3)
println("After swap: $numbers")
val fruits = mutableListOf("apple", "banana", "cherry", "date")
println("Before swap: $fruits")
fruits.swap(1, 3)
println("After swap: $fruits")
}
Output:
Before swap: [1, 2, 3, 4]
After swap: [4, 2, 3, 1]
Before swap: [apple, banana, cherry, date]
After swap: [apple, date, cherry, banana]
Example 2: A Generic Repository
This example shows a simple generic repository pattern commonly used in applications with databases:
interface Entity {
val id: Int
}
class User(override val id: Int, val name: String) : Entity
class Product(override val id: Int, val title: String, val price: Double) : Entity
class Repository<T : Entity> {
private val items = mutableMapOf<Int, T>()
fun save(item: T) {
items[item.id] = item
}
fun findById(id: Int): T? {
return items[id]
}
fun findAll(): List<T> {
return items.values.toList()
}
fun delete(id: Int) {
items.remove(id)
}
}
fun main() {
// User repository
val userRepo = Repository<User>()
userRepo.save(User(1, "Alice"))
userRepo.save(User(2, "Bob"))
val user = userRepo.findById(1)
println("Found user: ${user?.name}")
// Product repository
val productRepo = Repository<Product>()
productRepo.save(Product(1, "Laptop", 999.99))
productRepo.save(Product(2, "Smartphone", 599.99))
val products = productRepo.findAll()
println("All products:")
products.forEach { println("${it.title}: $${it.price}") }
}
Output:
Found user: Alice
All products:
Laptop: $999.99
Smartphone: $599.99
Example 3: Generic Operations on Collections
fun <T> Collection<T>.findByPredicate(predicate: (T) -> Boolean): List<T> {
val result = mutableListOf<T>()
for (item in this) {
if (predicate(item)) {
result.add(item)
}
}
return result
}
fun main() {
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val evenNumbers = numbers.findByPredicate { it % 2 == 0 }
println("Even numbers: $evenNumbers")
val words = listOf("apple", "banana", "avocado", "cherry", "apricot")
val aWords = words.findByPredicate { it.startsWith("a") }
println("Words starting with 'a': $aWords")
}
Output:
Even numbers: [2, 4, 6, 8, 10]
Words starting with 'a': [apple, avocado, apricot]
Reified Type Parameters
One limitation of generics in many languages is that type information is erased at runtime. Kotlin has a special feature called reified type parameters that allows you to access the type information at runtime within inline functions.
inline fun <reified T> Any.isA(): Boolean = this is T
inline fun <reified T> List<*>.filterIsInstance(): List<T> {
val result = mutableListOf<T>()
for (element in this) {
if (element is T) {
result.add(element)
}
}
return result
}
fun main() {
val value = "Hello"
println("Is value a String? ${value.isA<String>()}")
println("Is value an Int? ${value.isA<Int>()}")
val mixed = listOf(1, "Hello", 3.14, true, "World")
val onlyStrings = mixed.filterIsInstance<String>()
println("Strings from the list: $onlyStrings")
}
Output:
Is value a String? true
Is value an Int? false
Strings from the list: [Hello, World]
The reified
keyword preserves the type information at runtime, but this only works with inline
functions.
Best Practices for Generic Functions
-
Use meaningful type parameter names:
- Use
T
for a general type - Use
E
for element type - Use
K
andV
for key and value types - Use descriptive names when appropriate (
Element
,Key
, etc.)
- Use
-
Apply constraints only when necessary: Don't over-constrain your type parameters.
-
Leverage type inference: Let Kotlin infer types when possible to make your code cleaner.
-
Document your generic functions: Explain what the type parameters mean, especially when you have multiple type parameters.
Summary
Generic functions in Kotlin provide a powerful way to write flexible, reusable code while maintaining type safety. With generic functions, you can:
- Write a single function that works with multiple types
- Apply constraints to types when necessary
- Create extension functions that work with any type
- Access type information at runtime using reified type parameters
Generics help you write more reusable code, avoid code duplication, and catch type errors at compile-time rather than runtime.
Exercises
-
Write a generic function
findMax
that returns the maximum element from a list of comparable items. -
Create a generic
Stack<T>
class with push, pop, and peek operations. -
Write a generic extension function for any collection that finds and returns the most frequent element.
-
Implement a generic
map
function that transforms a list of one type into a list of another type. -
Create a generic
Result<T>
class that can either hold a successful value of typeT
or an error message.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)