Kotlin - Extensions
Kotlin Extensions
Extensions allow you to add new functionality to existing classes without modifying their source code or using inheritance. Learn to create extension functions, extension properties, and understand their scope and limitations.
Extension Functions
Key Concept: Extensions are resolved statically, meaning they don't actually modify the classes they extend. They're syntactic sugar for static function calls.
Basic Extension Functions
// Extending String with a custom function
fun String.isPalindrome(): Boolean {
val cleaned = this.lowercase().replace(Regex("[^a-z0-9]"), "")
return cleaned == cleaned.reversed()
}
// Extending Int with utility functions
fun Int.isEven(): Boolean = this % 2 == 0
fun Int.isOdd(): Boolean = this % 2 != 0
fun Int.squared(): Int = this * this
// Usage
val text = "A man, a plan, a canal: Panama"
println("'$text' is palindrome: ${text.isPalindrome()}") // true
val number = 5
println("$number is even: ${number.isEven()}") // false
println("$number is odd: ${number.isOdd()}") // true
println("$number squared: ${number.squared()}") // 25
Extension Functions with Parameters
// Extend String with formatting capabilities
fun String.truncate(maxLength: Int, suffix: String = "..."): String {
return if (this.length <= maxLength) {
this
} else {
this.take(maxLength - suffix.length) + suffix
}
}
fun String.repeat(times: Int, separator: String = ""): String {
return (1..times).joinToString(separator) { this }
}
// Extend List with utility functions
fun List.second(): T? = if (size >= 2) this[1] else null
fun List.secondOrNull(): T? = if (size >= 2) this[1] else null
fun List.isNotEmpty(): Boolean = !isEmpty()
fun List.middle(): T? = if (isEmpty()) null else this[size / 2]
// Usage
val longText = "This is a very long text that needs to be truncated"
println(longText.truncate(20)) // This is a very l...
val word = "Hello"
println(word.repeat(3, "-")) // Hello-Hello-Hello
val numbers = listOf(1, 2, 3, 4, 5)
println("Second element: ${numbers.second()}") // 2
println("Middle element: ${numbers.middle()}") // 3
Extension Functions on Nullable Types
// Extension on nullable String
fun String?.isNullOrEmpty(): Boolean {
return this == null || this.isEmpty()
}
fun String?.orDefault(default: String): String {
return if (this.isNullOrBlank()) default else this
}
// Extension on nullable collections
fun Collection?.isNotNullOrEmpty(): Boolean {
return this != null && this.isNotEmpty()
}
// Usage
val nullableString: String? = null
val emptyString: String? = ""
val validString: String? = "Hello"
println("nullableString.isNullOrEmpty(): ${nullableString.isNullOrEmpty()}") // true
println("emptyString.isNullOrEmpty(): ${emptyString.isNullOrEmpty()}") // true
println("validString.isNullOrEmpty(): ${validString.isNullOrEmpty()}") // false
println("Default value: ${nullableString.orDefault("Default")}") // Default
println("Valid value: ${validString.orDefault("Default")}") // Hello
Extension Properties
Computed Extension Properties
// Extension properties must be computed (no backing field)
val String.wordCount: Int
get() = this.split(Regex("\\s+")).filter { it.isNotBlank() }.size
val String.firstWord: String
get() = this.split(Regex("\\s+")).firstOrNull() ?: ""
val String.lastWord: String
get() = this.split(Regex("\\s+")).lastOrNull() ?: ""
// Extension properties for collections
val List.penultimate: T?
get() = if (size >= 2) this[size - 2] else null
val IntRange.size: Int
get() = last - first + 1
// Usage
val sentence = "The quick brown fox jumps over the lazy dog"
println("Word count: ${sentence.wordCount}") // 9
println("First word: '${sentence.firstWord}'") // The
println("Last word: '${sentence.lastWord}'") // dog
val numbers = listOf(1, 2, 3, 4, 5)
println("Penultimate: ${numbers.penultimate}") // 4
val range = 1..10
println("Range size: ${range.size}") // 10
Extension Properties with Getters and Setters
// Extension property on mutable collections
var MutableList.secondElement: T?
get() = if (size >= 2) this[1] else null
set(value) {
if (size >= 2 && value != null) {
this[1] = value
}
}
// Extension property for custom classes
class Person(var firstName: String, var lastName: String)
var Person.fullName: String
get() = "$firstName $lastName"
set(value) {
val parts = value.split(" ", limit = 2)
firstName = parts.getOrNull(0) ?: ""
lastName = parts.getOrNull(1) ?: ""
}
// Usage
val mutableNumbers = mutableListOf(1, 2, 3, 4, 5)
println("Second element: ${mutableNumbers.secondElement}") // 2
mutableNumbers.secondElement = 20
println("Modified list: $mutableNumbers") // [1, 20, 3, 4, 5]
val person = Person("John", "Doe")
println("Full name: ${person.fullName}") // John Doe
person.fullName = "Jane Smith"
println("Updated: ${person.firstName} ${person.lastName}") // Jane Smith
Generic Extension Functions
Type-Safe Extensions
// Generic extension functions
fun T.applyIf(condition: Boolean, block: T.() -> T): T {
return if (condition) this.block() else this
}
fun T.also(block: (T) -> Unit): T {
block(this)
return this
}
fun Collection.joinToStringIndexed(
separator: String = ", ",
transform: (index: Int, T) -> String
): String {
return this.mapIndexed(transform).joinToString(separator)
}
// Extension for any comparable type
fun > T.coerceInRange(range: ClosedRange): T {
return when {
this < range.start -> range.start
this > range.endInclusive -> range.endInclusive
else -> this
}
}
// Usage
val text = "hello world"
val result = text
.applyIf(true) { uppercase() }
.applyIf(false) { reversed() }
.also { println("Processing: $it") }
println("Final result: $result") // HELLO WORLD
val fruits = listOf("apple", "banana", "cherry")
val indexed = fruits.joinToStringIndexed(" | ") { index, item ->
"$index: $item"
}
println(indexed) // 0: apple | 1: banana | 2: cherry
val temperature = 105
val safe = temperature.coerceInRange(0..100)
println("Safe temperature: $safe") // 100
Scope of Extensions
Local Extensions
class StringProcessor {
// Extension function available only within this class
private fun String.processInternal(): String {
return this.trim().uppercase().replace(" ", "_")
}
fun processStrings(strings: List): List {
return strings.map { it.processInternal() }
}
}
fun demonstrateLocalExtensions() {
// Local extension function (only available in this function)
fun Int.factorial(): Long {
return if (this <= 1) 1L else this * (this - 1).factorial()
}
println("5! = ${5.factorial()}") // 120
println("7! = ${7.factorial()}") // 5040
}
// Usage
val processor = StringProcessor()
val texts = listOf(" hello world ", " kotlin extensions ")
println(processor.processStrings(texts)) // [HELLO_WORLD, KOTLIN_EXTENSIONS]
demonstrateLocalExtensions()
Extension Visibility and Imports
// In file: StringExtensions.kt
fun String.removeVowels(): String {
return this.filterNot { it.lowercase() in "aeiou" }
}
private fun String.secretFunction(): String {
return "Secret: $this"
}
// In file: NumberExtensions.kt
fun Double.formatCurrency(symbol: String = "$"): String {
return "$symbol%.2f".format(this)
}
// Usage (would need imports in real project)
// import StringExtensions.removeVowels
// import NumberExtensions.formatCurrency
fun useExtensionsExample() {
val text = "Hello World"
println(text.removeVowels()) // Hll Wrld
val price = 19.99
println(price.formatCurrency()) // $19.99
println(price.formatCurrency("€")) // €19.99
// secretFunction() not accessible outside its file
}
Extensions vs Member Functions
Member Function Takes Precedence
class Example {
fun printMessage() {
println("Member function")
}
}
// Extension function with same name
fun Example.printMessage() {
println("Extension function")
}
fun Example.printAnotherMessage() {
println("Extension only function")
}
// Usage
val example = Example()
example.printMessage() // "Member function" (member wins)
example.printAnotherMessage() // "Extension only function"
Extensions with Different Signatures
class Calculator {
fun add(a: Int, b: Int): Int = a + b
}
// Extension with different signature is allowed
fun Calculator.add(a: Double, b: Double): Double = a + b
fun Calculator.add(numbers: List): Int = numbers.sum()
val calc = Calculator()
println(calc.add(5, 3)) // 8 (member function)
println(calc.add(5.5, 3.2)) // 8.7 (extension)
println(calc.add(listOf(1, 2, 3, 4, 5))) // 15 (extension)
Real-World Extensions
Date and Time Extensions
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
// Date formatting extensions
fun LocalDate.toDisplayString(): String =
this.format(DateTimeFormatter.ofPattern("MMM dd, yyyy"))
fun LocalDateTime.toTimestamp(): String =
this.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
// Date arithmetic extensions
fun LocalDate.daysUntil(other: LocalDate): Long =
ChronoUnit.DAYS.between(this, other)
fun LocalDate.isWeekend(): Boolean =
this.dayOfWeek.value in 6..7
fun LocalDate.addBusinessDays(days: Int): LocalDate {
var result = this
var remaining = days
while (remaining > 0) {
result = result.plusDays(1)
if (!result.isWeekend()) {
remaining--
}
}
return result
}
// Usage
val today = LocalDate.now()
val tomorrow = today.plusDays(1)
val nextWeek = today.plusWeeks(1)
println("Today: ${today.toDisplayString()}")
println("Days until next week: ${today.daysUntil(nextWeek)}")
println("Is today weekend? ${today.isWeekend()}")
println("5 business days from today: ${today.addBusinessDays(5).toDisplayString()}")
Collection Processing Extensions
// Statistical extensions
fun Collection.average(): Double =
if (isEmpty()) 0.0 else sum().toDouble() / size
fun Collection.standardDeviation(): Double {
if (isEmpty()) return 0.0
val mean = average()
val variance = map { (it - mean) * (it - mean) }.average()
return kotlin.math.sqrt(variance)
}
// Grouping extensions
fun List.chunked(size: Int): List> {
return windowed(size, size, true)
}
fun List.partition(predicate: (T) -> Boolean): Pair, List> {
val first = mutableListOf()
val second = mutableListOf()
for (element in this) {
if (predicate(element)) {
first.add(element)
} else {
second.add(element)
}
}
return Pair(first, second)
}
// Validation extensions
fun List.isValidIndex(index: Int): Boolean =
index in 0 until size
fun List.getOrDefault(index: Int, default: T): T =
if (isValidIndex(index)) this[index] else default
// Usage
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
println("Average: ${numbers.average()}")
val (evens, odds) = numbers.partition { it % 2 == 0 }
println("Evens: $evens") // [2, 4, 6, 8, 10]
println("Odds: $odds") // [1, 3, 5, 7, 9]
val chunks = numbers.chunked(3)
println("Chunks: $chunks") // [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
println("Safe access: ${numbers.getOrDefault(15, -1)}") // -1
String Processing Extensions
// Text processing extensions
fun String.toTitleCase(): String =
split(" ").joinToString(" ") { word ->
word.lowercase().replaceFirstChar { it.uppercase() }
}
fun String.removeExtraSpaces(): String =
trim().replace(Regex("\\s+"), " ")
fun String.isValidEmail(): Boolean {
val emailRegex = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
return emailRegex.matches(this)
}
fun String.maskEmail(): String {
if (!isValidEmail()) return this
val parts = split("@")
val username = parts[0]
val domain = parts[1]
val maskedUsername = if (username.length <= 2) {
"*".repeat(username.length)
} else {
username.take(2) + "*".repeat(username.length - 2)
}
return "$maskedUsername@$domain"
}
fun String.extractNumbers(): List =
Regex("\\d+").findAll(this).map { it.value.toInt() }.toList()
// Usage
val messyText = " hello WORLD from kotlin "
println("Title case: '${messyText.removeExtraSpaces().toTitleCase()}'")
val email = "[email protected]"
println("Valid email: ${email.isValidEmail()}") // true
println("Masked: ${email.maskEmail()}") // jo****@example.com
val textWithNumbers = "I have 5 apples and 10 oranges, total 15 fruits"
println("Numbers: ${textWithNumbers.extractNumbers()}") // [5, 10, 15]
Performance Considerations
Inline Extension Functions
// Inline extension for performance-critical code
inline fun T.measureTime(block: T.() -> Unit): Long {
val startTime = System.nanoTime()
this.block()
return System.nanoTime() - startTime
}
// Extension that might be called frequently
inline fun String.fastIsEmpty(): Boolean = length == 0
// Non-inline for comparison
fun String.slowIsEmpty(): Boolean = length == 0
// Usage
val text = "Sample text"
val time = text.measureTime {
repeat(1000) {
fastIsEmpty() // Inlined - no function call overhead
}
}
println("Time taken: ${time / 1_000_000} ms")
Best Practices
✅ Good Practices
- Use extensions to add functionality that feels natural to the type
- Prefer extension functions over utility classes with static methods
- Keep extensions simple and focused on a single responsibility
- Use meaningful names that clearly indicate what the extension does
- Consider making frequently-used extensions inline for performance
- Group related extensions in the same file
❌ Avoid
- Creating extensions that expose internal implementation details
- Using extensions to fix poor class design (refactor the class instead)
- Making extensions too complex or doing too many things
- Creating extensions that break the principle of least surprise
- Overusing extensions where a regular function would be clearer
Architecture Note: Extensions are resolved statically, not polymorphically. They're a compile-time feature that provides a clean syntax for utility functions while maintaining type safety and performance.
Practice Exercises
- Create extensions for mathematical operations on lists of numbers
- Build string formatting extensions for common use cases
- Design date/time extensions for business logic calculations
- Create validation extensions for common data types (email, phone, etc.)
- Implement collection manipulation extensions (chunking, partitioning, etc.)
Quick Quiz
- Can extension properties have backing fields?
- What happens when an extension function has the same name as a member function?
- Are extensions resolved at compile-time or runtime?
- Can you create extensions on nullable types?
Show answers
- No, extension properties must be computed (no backing fields allowed)
- The member function takes precedence over the extension function
- Compile-time (they're resolved statically)
- Yes, you can create extensions on nullable types like String?