Extension Functions
Imagine you often need to check whether a word is a palindrome, or to show an amount as Rs 1299. You cannot add methods to Kotlin's String or Int classes, because you do not own their code. In Java you would write a utility class full of static methods, such as TextUtils.isPalindrome(word).
Kotlin offers something nicer: extension functions. They let you write word.isPalindrome() as if the function were part of String, without changing or inheriting from it. In fact, much of Kotlin's standard library (uppercase, filter, sumOf, let) is written as extensions. In this lesson you will write your own, and learn their rules and limits.
Writing an extension function
Put the type you are extending, called the receiver type, and a dot before the function name. Inside the function, this refers to the object it was called on, and you can usually leave this out.
fun String.isPalindrome(): Boolean {
val clean = this.lowercase().filter { it.isLetter() }
return clean == clean.reversed()
}
fun Int.toRupees(): String = "Rs $this"
fun main() {
println("Malayalam".isPalindrome())
println("Nitin".isPalindrome())
println("Kotlin".isPalindrome())
println(1299.toRupees())
}true true false Rs 1299
"Malayalam".isPalindrome() reads naturally, and your editor's auto-complete will suggest isPalindrome when you type a dot after any string. The extension can use the receiver's public members (like lowercase()), but not its private ones; an extension does not get inside the class.
Extension properties and nullable receivers
You can add properties too. An extension property cannot store a value (there is nowhere to put it), so it must have a getter that computes the result.
An extension can also be declared on a nullable type. Then you may call it on a variable that is null, and the function must handle that case itself.
val String.wordCount: Int
get() = trim().split(Regex("\\s+")).count { it.isNotEmpty() }
fun String?.orDash(): String = if (this == null || this.isBlank()) "-" else this
fun main() {
println(" Good morning Pune ".wordCount)
println("".wordCount)
val middleName: String? = null
println("Aarav ${middleName.orDash()} Mehta")
println("Isha".orDash())
}3 0 Aarav - Mehta Isha
Regex("\\s+") matches one or more spaces, so several spaces in a row count as one gap. Notice that middleName.orDash() needs no ?., because orDash accepts String?. The standard library's isNullOrBlank() and orEmpty() are built the same way.
Extensions on your own and generic types
Extensions are useful on your own classes too, especially on collections of them. They keep small helpers close to where they are used without crowding the class. You can also write generic extensions that work for any element type T (generics get their own lesson next).
data class Order(val item: String, val amount: Int)
fun List<Order>.total(): Int = sumOf { it.amount }
fun List<Order>.biggest(): Order? = maxByOrNull { it.amount }
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null
fun main() {
val orders = listOf(
Order("Thali", 180),
Order("Lassi", 60),
Order("Paneer Tikka", 260),
)
println(orders.total())
println(orders.biggest())
println(orders.secondOrNull())
println(listOf("Pune").secondOrNull())
}500 Order(item=Paneer Tikka, amount=260) Order(item=Lassi, amount=60) null
orders.total() reads like a sentence. Inside total, sumOf is called on this, the list.
Extensions are resolved statically
An extension does not really change the class. The compiler turns "Kotlin".isPalindrome() into an ordinary function call with the string as the first argument. This has two consequences you must know:
- The extension that runs is chosen from the declared type of the variable at compile time, not from the real object at run time. Extensions are not polymorphic.
- If the class already has a member function with the same name and parameters, the member always wins.
open class Animal
class Dog : Animal()
fun Animal.sound() = "some sound"
fun Dog.sound() = "woof"
class Car {
fun honk() = "member honk"
}
fun Car.honk() = "extension honk"
fun main() {
val pet: Animal = Dog()
println(pet.sound())
println(Dog().sound())
println(Car().honk())
}some sound woof member honk
pet holds a Dog, but its declared type is Animal, so Animal.sound() runs. If you need behaviour that depends on the real object, use a member function with open and override instead. The compiler warns you that Car.honk() is shadowed by a member and will never be called.
Infix functions and lambdas with receivers
Mark a one-parameter extension (or member) function with infix to call it without a dot and brackets, like the built-in to in "a" to 1.
A lambda with receiver has a function type such as StringBuilder.() -> Unit. Inside the lambda, this is a StringBuilder, so you can call its methods directly. This is how buildString, apply and Kotlin's type-safe builders (including Gradle's Kotlin DSL and Jetpack Compose) let you write code that looks like a small custom language.
infix fun Int.percentOf(total: Int): Int = total * this / 100
fun buildReceipt(block: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.block()
return sb.toString()
}
fun main() {
println(18 percentOf 2500)
val receipt = buildReceipt {
appendLine("Kochi Spice Store")
appendLine("Pepper 250g Rs 180")
append("Total Rs 180")
}
println(receipt)
}450 Kochi Spice Store Pepper 250g Rs 180 Total Rs 180
Inside the lambda passed to buildReceipt, appendLine(...) means this.appendLine(...) on the StringBuilder. Use infix sparingly; it suits functions that read like an operator, such as percentOf or to.
Where to put extensions
Declare extensions at the top level of a file, usually grouped by topic in files such as StringExtensions.kt. To use them from another package, import them by name: import com.example.util.isPalindrome. You can also declare an extension inside a class; it is then available only inside that class.
Common mistakes
- Expecting extensions to override. They are chosen by the declared type; use
openmembers for polymorphism. - Writing an extension with the same name as a member. The member always wins, and your extension silently does nothing.
- Trying to store state in an extension property. Extension properties have no backing field; they must compute their value.
- Adding extensions to everything. An extension on
Stringappears for every string in the project. Keep them focused, and make very specific helpersprivateto their file.
Key points
fun Type.name()adds a function to an existing type without changing it;thisis the receiver.- Extension properties need a getter because they cannot store a value.
- Extensions on nullable types (
String?) can be called onnulland must handle it. - Extensions are resolved statically by declared type; members win over extensions.
infixallows calls like18 percentOf 2500.- Lambdas with receivers (
StringBuilder.() -> Unit) power builders and DSLs.
Exercise
Write three extensions: String.maskPhone() that shows only the last four digits of a 10-digit number ("9876543210" becomes "******3210"), an extension property Int.isLeapYear, and List<Int>.secondLargest(): Int? that returns null when there are fewer than two distinct values. Test each with two inputs.
Show solution
fun String.maskPhone(): String = "*".repeat(length - 4) + takeLast(4)
val Int.isLeapYear: Boolean
get() = (this % 4 == 0 && this % 100 != 0) || this % 400 == 0
fun List<Int>.secondLargest(): Int? {
val distinctSorted = distinct().sortedDescending()
return distinctSorted.getOrNull(1)
}
fun main() {
println("9876543210".maskPhone())
println("9123401234".maskPhone())
println(2024.isLeapYear)
println(1900.isLeapYear)
println(listOf(70, 95, 88, 95).secondLargest())
println(listOf(50, 50).secondLargest())
}******3210 ******1234 true false 88 null
takeLast(4) keeps the last four characters. distinct() removes the repeated 95 before sorting, so the second largest is 88; with only one distinct value, getOrNull(1) returns null.
Quick Quiz
1. Inside fun String.shout() = uppercase() + "!", what does uppercase() act on?
The receiver (this) is the string before the dot, and this can be left out.
2. A class has a member fun info() and there is also an extension fun MyClass.info(). Which runs for obj.info()?
Members always win over extensions with the same signature.
3. val a: Animal = Dog(), with extensions for both Animal and Dog. Which extension does a.sound() call?
Extensions are resolved by the declared type at compile time.
4. Why can an extension property not have an initialiser like val String.tag = "x"?
There is no backing field for an extension property.
Learn Kotlin with a mentor
Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
