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

  1. Create extensions for mathematical operations on lists of numbers
  2. Build string formatting extensions for common use cases
  3. Design date/time extensions for business logic calculations
  4. Create validation extensions for common data types (email, phone, etc.)
  5. Implement collection manipulation extensions (chunking, partitioning, etc.)

Quick Quiz

  1. Can extension properties have backing fields?
  2. What happens when an extension function has the same name as a member function?
  3. Are extensions resolved at compile-time or runtime?
  4. Can you create extensions on nullable types?
Show answers
  1. No, extension properties must be computed (no backing fields allowed)
  2. The member function takes precedence over the extension function
  3. Compile-time (they're resolved statically)
  4. Yes, you can create extensions on nullable types like String?