Extension Functions

Kotlin · Lesson 19 of 29 7 min read Updated

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.

Kotlin
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())
}
Output
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.

Kotlin
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())
}
Output
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).

Kotlin
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())
}
Output
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:

  1. 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.
  2. If the class already has a member function with the same name and parameters, the member always wins.
Kotlin
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())
}
Output
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.

Kotlin
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)
}
Output
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 open members 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 String appears for every string in the project. Keep them focused, and make very specific helpers private to their file.

Key points

  • fun Type.name() adds a function to an existing type without changing it; this is the receiver.
  • Extension properties need a getter because they cannot store a value.
  • Extensions on nullable types (String?) can be called on null and must handle it.
  • Extensions are resolved statically by declared type; members win over extensions.
  • infix allows calls like 18 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
Kotlin
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())
}
Output
******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?

2. A class has a member fun info() and there is also an extension fun MyClass.info(). Which runs for obj.info()?

3. val a: Animal = Dog(), with extensions for both Animal and Dog. Which extension does a.sound() call?

4. Why can an extension property not have an initialiser like val String.tag = "x"?

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