RizTech Academy logo
RizTech Academy
FunctionsLesson 4 of 525 min

Extension functions

Extension functions are one of Kotlin's most distinctive and loved features. They let you add a function to a type you did not write — String, Int, a library class, anything — as if it had always been there. Used well, they make code read beautifully. Used badly, they scatter behaviour where nobody expects it. This lesson is both halves.

The idea

Suppose you often want to check whether a string is a valid Indian pincode (six digits). You could write a normal function:

fun isValidPincode(s: String): Boolean = s.length == 6 && s.all { it.isDigit() }

println(isValidPincode("411038"))     // true

That works, but the call reads isValidPincode(pincode) — the function is separate from the thing it acts on. An extension function flips it around so the function belongs to the string:

fun String.isValidPincode(): Boolean = this.length == 6 && this.all { it.isDigit() }

println("411038".isValidPincode())    // true
println("41103".isValidPincode())     // false

Read the declaration: fun String.isValidPincode() means "add a function isValidPincode to String". Inside the function, this refers to the string it was called on — the receiver. Now you call it with dot syntax, "411038".isValidPincode(), exactly as if isValidPincode were a built-in String method. You did not modify the String class (you cannot); Kotlin resolves the call to your extension function at compile time.

You can usually drop the explicit this:

fun String.isValidPincode(): Boolean = length == 6 && all { it.isDigit() }

length and all refer to the receiver automatically, the same way you use a class's own properties inside its methods.

Why this reads better

The value is at the call site. Compare:

// plain function — reads inside-out
formatRupees(applyDiscount(basePrice, 10))

// extension functions — reads left to right, like a pipeline
basePrice.applyDiscount(10).formatRupees()

The extension version reads in the order things happen: take the base price, apply a discount, format as rupees. This left-to-right, chainable style is why extensions are everywhere in idiomatic Kotlin, and it is exactly how the standard library's collection operations (.filter { }.map { }) are built — they are extension functions on collections.

Extensions on any type

You can extend any type, including ones you do not own:

fun Int.isEven(): Boolean = this % 2 == 0
fun Int.toRupees(): String = "₹$this"
fun List<Int>.secondOrNull(): Int? = if (size >= 2) this[1] else null

println(4.isEven())                    // true
println(250.toRupees())                // ₹250
println(listOf(10, 20, 30).secondOrNull())  // 20

This is how you tailor library types to your domain without wrapping them in your own classes. A great deal of the polish in a Kotlin codebase is small, well-named extensions that make the common operations read naturally.

Extension properties

You can also add a property the same way, as long as it computes its value (it has no backing field of its own):

val String.isBlank2: Boolean get() = this.trim().isEmpty()
val List<*>.lastIndex2: Int get() = size - 1

println("   ".isBlank2)                // true

Use extension properties for a value that is naturally a property of the type ("the last index of this list") rather than an action; use extension functions for things that do something.

The honest limits, and how to use extensions well

Extensions are powerful, and power invites misuse. Three things to know:

Extensions are resolved statically, not by the object's runtime type. An extension is not truly part of the class — it is syntactic sugar for a function call. This means extensions do not override member functions and are not polymorphic. If a class already has a member function with the same name, the member wins. This rarely bites, but it explains why an extension you added "does nothing" — a member of the same name shadowed it.

They cannot access private members. An extension is outside the class, so it can only use the public API — it cannot reach into private fields. This is a feature: extensions cannot break a class's encapsulation.

Do not scatter behaviour. The danger of extensions is that behaviour ends up living far from the type it acts on, in whichever file happened to need it. A reader looking at String has no way to know your project added isValidPincode unless they know where to look. So: keep extensions discoverable — group related ones in a well-named file (StringExtensions.kt), and prefer an extension only when it genuinely reads better than a plain function. An extension that just wraps a one-liner nobody chains is often clearer as an ordinary function. The test is: does dot-calling it make real code read better? If yes, extend; if it is just fashionable, do not.

Check your work

What an extension function does. Adds a function to an existing type, callable with dot syntax as if it were a member.

What this refers to inside an extension. The receiver — the object the extension was called on.

Why extensions read better. They enable a left-to-right, chainable pipeline style instead of nested inside-out calls.

How the standard library uses them. filter, map and the rest are extension functions on collections.

What an extension property is, and its limit. A computed property added to a type — it must have a getter and no backing field.

How extensions are resolved. Statically, at compile time — they do not override members, and a member of the same name wins.

What an extension cannot access. Private members — it uses only the public API.

The main risk, and how to manage it. Behaviour scattered away from its type — keep extensions grouped and discoverable, and use them only when they genuinely read better.

Practice

  1. Write String.isValidPincode() and test it on "411038" and "abc".
  2. Rewrite it dropping the explicit this and confirm it still works.
  3. Write Int.isEven() and Int.toRupees() and chain them into an expression.
  4. Write List<Int>.secondOrNull() and test it on a list of size 3 and a list of size 1.
  5. Write an extension property String.wordCount that returns the number of words.
  6. Create a class with a member function, then write an extension of the same name. Observe that the member wins.
  7. Take a nested call like format(clean(trim(input))) and, with extensions, rewrite it as a left-to-right chain.
  8. Find a plain utility function you have written and decide whether it reads better as an extension. Convert it only if it does.

Official documentation

Next: lambdas and higher-order functions — the heart of Kotlin's expressiveness.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship