Reading — step 1 of 7
Learn
Kotlin's extension functions let you add methods to existing types without modifying them. The standard library uses them everywhere — String.toIntOrNull(), List.sumOf {}, Iterable.groupBy {} — all extensions.
Defining an extension function
fun String.exclaim(): String = "$this!"
fun main() {
println("hello".exclaim()) // "hello!"
}
The String. prefix says "add this function to String." Inside, this refers to the receiver — the String it was called on.
No modification to String. No subclassing. Pure compile-time sugar — at the bytecode level, "hello".exclaim() becomes exclaim("hello").
Generic extensions
fun <T> List<T>.second(): T? {
return if (size >= 2) this[1] else null
}
listOf(1, 2, 3).second() // 2
listOf<String>().second() // null
Works for any List<T>.
Extension properties
val String.lastChar: Char
get() = this[this.length - 1]
"hello".lastChar // 'o'
Extension properties can't have backing fields — they must be computed via getter (and optional setter for var).
Bound vs unbound, dispatch
Extensions are statically dispatched — the type at the CALL site decides which extension runs:
open class Animal
class Dog : Animal()
fun Animal.sound() = "generic noise"
fun Dog.sound() = "woof"
val a: Animal = Dog()
println(a.sound()) // "generic noise" — STATIC type is Animal
Different from member functions which are dynamically dispatched. Often surprises Java refugees.
When to use extensions vs methods
Extensions:
- Adding utility methods to library types you don't own (
String,List, JDK classes) - Pure functions that read state and produce a result
- Domain-specific helpers grouped by receiver type
Methods:
- Behavior central to the class (validation, mutation, lifecycle)
- Anything that needs polymorphism or override
- API contract methods
Scope
Extensions are functions in a package — declared at top level OR inside a class (member extension). Top-level extensions are the most common:
// In file utils.kt:
package com.example.utils
fun String.titleCase(): String = ...
Import with import com.example.utils.titleCase at the call site.
Member extensions — receivers within receivers
class HttpClient {
fun String.encoded(): String = URLEncoder.encode(this, "UTF-8")
fun get(path: String, params: Map<String, String>) {
val query = params.entries.joinToString("&") { (k, v) -> "${k.encoded()}=${v.encoded()}" }
// ...
}
}
The extension String.encoded() is only available inside HttpClient. Useful for DSLs and grouping context-specific helpers.
Standard library examples
// All extensions:
"hello".uppercase()
listOf(1, 2, 3).sumOf { it * 2 }
mapOf(1 to "one").mapValues { it.value.uppercase() }
Iterable<T>::filter, ::map, ::groupBy, ::partition, ::count
String::isNullOrBlank, ::toIntOrNull, ::reversed
Kotlin's stdlib gets its concise feel from extension functions. Java's String has ~70 methods; Kotlin adds another ~150 useful extensions on top.
Common mistakes
- Expecting polymorphic dispatch — extensions are static. Override member methods, not extensions.
- Defining extensions in the wrong file — must import at call site. IDE auto-imports help.
- Naming collisions with member methods — member methods always WIN. The extension is shadowed silently.
- Adding
nullto receiver type unintentionally —fun String?.isEmpty(): Booleanaccepts null receivers. Different fromString.isEmpty(). - Overusing extensions for major logic — extensions are sugar; major behavior should live in classes.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…