Defining Extension Functions
The Problem: Adding Behavior to Existing Classes
Sometimes you need to add functionality to a class you do not own. Java requires wrapper classes or utility methods. Kotlin solves this with extension functions, which let you add methods to existing classes without modifying their source code.
Syntax
An extension function is declared with a receiver type before the function name:
fun String.removeVowels(): String {
return this.filter { it !in "aeiouAEIOU" }
}
println("Hello World".removeVowels()) // Hll Wrld
The this keyword inside the function refers to the receiver object (the String instance the function is called on). The function is called as if it were a member of the class.
How Extension Functions Are Resolved
Extension functions are resolved statically, not polymorphically. The compiler determines which extension function to call based on the declared type of the variable, not its runtime type:
open class Animal
class Dog : Animal()
fun Animal.speak() = "Generic sound"
fun Dog.speak() = "Woof!"
val animal: Animal = Dog()
println(animal.speak()) // Generic sound (not Woof!)
This is different from virtual method calls. If you need polymorphic behavior, use regular inheritance.
Extension Functions on Nullable Types
You can define extensions on nullable types to handle null cases gracefully:
fun String?.isNullOrEmpty(): Boolean {
return this == null || this.isEmpty()
}
val name: String? = null
println(name.isNullOrEmpty()) // true
println("".isNullOrEmpty()) // true
println("Kotlin".isNullOrEmpty()) // false
This pattern is used extensively in Kotlin's standard library.
Member vs Extension Functions
If a class already has a member function with the same signature as an extension function, the member function always wins:
class Sample {
fun foo() = "member"
}
fun Sample.foo() = "extension"
println(Sample().foo()) // member
This rule prevents ambiguity: existing APIs always take precedence over extensions.
Extension Properties
Computed Extension Properties
Extension properties add computed properties to existing classes. They do not have backing fields, so they must be defined with custom getters:
val String.wordCount: Int
get() = split(" ").size
println("hello world".wordCount) // 2
Extension properties are useful for derived computations that do not need storage.
Practical Examples
val String.isEmail: Boolean
get() = contains("@") && "." in substringAfter("@")
val String.truncated: String
get() = if (length > 20) take(20) + "..." else this
val <T> List<T>.secondOrNull: T?
get() = if (size >= 2) this[1] else null
println("user@example.com".isEmail) // true
println(listOf(1).secondOrNull) // null
println(listOf(1, 2, 3).secondOrNull) // 2
Chaining Extensions
Extension functions and properties can be chained for readable pipelines:
val result = " Hello World "
.trim()
.lowercase()
.replace(" ", "-")
.take(10)
println(result) // hello-world
When to Write Extensions
- When you need a method on a class you do not own (e.g., Android SDK classes)
- When the method is logically related to the class but not core functionality
- When you want to add DSL-like syntax to existing types
- When the method would be useful across multiple call sites
Avoid extensions that change the meaning of existing methods or depend on mutable state.
Practice Problems
Write extension functions on String: isPalindrome() that checks if the string reads the same forwards and backwards (case-insensitive), and truncate(maxLength) that returns the first maxLength characters followed by '...' if longer.
Solution
fun String.isPalindrome(): Boolean {
val cleaned = lowercase().filter { it.isLetterOrDigit() }
return cleaned == cleaned.reversed()
}
fun String.truncate(maxLength: Int): String {
return if (length > maxLength) take(maxLength) + "..." else this
}
fun main() {
println("Racecar".isPalindrome()) // true
println("Hello".isPalindrome()) // false
println("Hello World".truncate(5)) // Hello...
println("Hi".truncate(5)) // Hi
} Write an extension function on List<Int> called runningSum that returns a new list where each element is the sum of all previous elements including itself. Example: [1, 2, 3] becomes [1, 3, 6].
Solution
fun List<Int>.runningSum(): List<Int> {
val result = mutableListOf<Int>()
var sum = 0
for (element in this) {
sum += element
result.add(sum)
}
return result
}
fun main() {
println(listOf(1, 2, 3, 4).runningSum()) // [1, 3, 6, 10]
println(listOf(1, 1, 1, 1).runningSum()) // [1, 2, 3, 4]
} Write an extension function on List<T> that returns the list if non-null or an empty list if null. Then write an extension on Int? that returns a formatted string like 'Value: 42' or 'No value' if null.
Solution
fun <T> List<T>?.orEmptyList(): List<T> {
return this ?: emptyList()
}
fun Int?.formatValue(): String {
return if (this != null) "Value: $this" else "No value"
}
fun main() {
val nullList: List<Int>? = null
println(nullList.orEmptyList()) // []
println(listOf(1, 2).orEmptyList()) // [1, 2]
val nullInt: Int? = null
println(nullInt.formatValue()) // No value
println(42.formatValue()) // Value: 42
} Quiz
1. How are extension functions resolved in Kotlin?
2. What happens if a class has both a member function and an extension function with the same signature?
3. Why can extension properties not have backing fields?
4. What is the benefit of defining an extension on a nullable type like String?
Flashcards
Question
What is an extension function in Kotlin?
Click to reveal answer
Answer
A function declared with a receiver type that adds methods to existing classes without modifying their source. Declared as fun ClassName.methodName(). Called as if it were a member function.
Question
Are extension functions resolved statically or dynamically?
Click to reveal answer
Answer
Statically at compile time. The compiler uses the declared type of the variable, not the runtime type. This differs from virtual method dispatch.
Question
Can extension properties have backing fields?
Click to reveal answer
Answer
No. Extension properties do not modify the class structure. They must be defined with custom getters only. They cannot maintain state.
Question
When should you avoid writing extension functions?
Click to reveal answer
Answer
When they change the meaning of existing methods, depend on mutable state, or could surprise users of the class. Also avoid when a regular utility function would be clearer.
Revision Notes
Key Takeaways
- 1. Extension functions let you add methods to classes you do not own.
- 2. They are resolved statically based on the declared type, not the runtime type.
- 3. Member functions always take precedence over extension functions.
- 4. Extension properties cannot have backing fields and must use custom getters.
- 5. Nullable type extensions handle null checks internally for cleaner caller code.
Interview Tips
- • Explain that extension functions are compile-time constructs, not runtime polymorphism.
- • Know that member functions shadow extension functions with the same signature.
- • Be ready to discuss when to use extensions vs utility functions.
- • Understand how extensions enable DSL-like syntax in Kotlin.
Cheat Sheet
Extension Functions Cheat Sheet
Syntax:
fun ReceiverType.functionName() = ...thisrefers to the receiver object
Resolution:
- Statically resolved at compile time
- Member functions always win over extensions
- Based on declared type, not runtime type
Nullable Extensions:
fun String?.isNullOrEmpty(): Boolean- Handle null internally for cleaner caller code
Extension Properties:
- Computed properties with custom getters
- No backing fields allowed
- Useful for derived values
Best Practices:
- Use for adding utility methods to classes you do not own
- Avoid if it changes existing method semantics
- Keep extensions focused and predictable