Extension Functions
## Learning Objectives
- Understand extension functions
- Learn extension properties
- Work with companion object extensions
- Master generics basics
- Use generic constraints
## Extension Function Basics
Extension functions add functions to existing classes without modifying them:
```kotlin
fun String.addExclamation(): String = this + "!"
fun main() {
val greeting = "Hello".addExclamation()
println(greeting) // Hello!
}
```
### Why Extension Functions?
```kotlin
// Without extension
fun lastChar(str: String): Char = str[str.length - 1]
// With extension
fun String.lastChar(): Char = this[length - 1]
fun main() {
println(lastChar("Kotlin")) // n
println("Kotlin".lastChar()) // n
}
```
## Extension Syntax
```text
fun ClassName.functionName(params): ReturnType {
// this refers to ClassName instance
}
```
```kotlin
fun Int.isEven(): Boolean = this % 2 == 0
fun String.addNumbers(num: Int): String = this + num
fun main() {
println(4.isEven()) // true
println(5.isEven()) // false
println("Answer: ".addNumbers(42)) // Answer: 42
}
```
## Nullable Receiver
Extensions can be called on nullable types:
```kotlin
fun String?.orEmpty(): String = this ?: ""
fun main() {
val nullStr: String? = null
println(nullStr.orEmpty()) // ""
val normalStr: String? = "Hello"
println(normalStr.orEmpty()) // Hello
}
```
## Extension Properties
Add properties to existing classes:
```kotlin
val String.uppercaseFirst: String
get() = if (isEmpty()) "" else this[0].uppercase() + substring(1)
var MutableList.lastIndex: Int
get() = size - 1
set(value) { /* Not common for immutable */ }
fun main() {
println("hello".uppercaseFirst) // Hello
val list = mutableListOf(1, 2, 3)
println(list.lastIndex) // 2
}
```
## Scope of Extensions
### Regular Extensions
```kotlin
package mypackage
fun String.addExclamation() = this + "!"
// Can only be used when imported
```
### member Extensions
```kotlin
class MyClass {
fun String.extended() = "Extended: $this"
}
```
## Companion Object Extensions
```kotlin
class MyClass {
companion object {
// Fields only
}
// Extension on companion object
fun MyClass.Companion.create(): MyClass = MyClass()
}
// Usage
fun main() {
val obj = MyClass.create()
}
```
### More Common Example
```kotlin
class Person private constructor(val name: String) {
companion object {
fun fromName(name: String): Person = Person(name)
}
}
fun main() {
val person = Person.fromName("Alice")
println(person.name) // Alice
}
```
## Generics
### Generic Functions
```kotlin
fun List.firstOrNull(): T? = if (isEmpty()) null else this[0]
fun printItem(item: T) {
println(item)
}
fun main() {
println(listOf(1, 2, 3).firstOrNull()) // 1
println(emptyList().firstOrNull()) // null
printItem("Hello") // Hello
printItem(42) // 42
}
```
### Generic Classes
```kotlin
class Box(val value: T) {
fun getValue(): T = value
}
class Pair(val first: A, val second: B)
fun main() {
val intBox = Box(42)
val stringBox = Box("Hello")
println(intBox.getValue()) // 42
println(stringBox.getValue()) // Hello
val pair = Pair(1, "one")
println("${pair.first}, ${pair.second}") // 1, one
}
```
### Generic Constraints
```kotlin
// Upper bound - T must be Comparable
fun > List.maxOrNull(): T? {
if (isEmpty()) return null
return sortedDescending().first()
}
// Multiple bounds
fun List.joinToString(
separator: String = ", "
): String where T : CharSequence, T : Comparable {
return this.joinToString(separator)
}
fun main() {
println(listOf(1, 5, 3, 2, 4).maxOrNull()) // 5
val strings = listOf("apple", "cherry", "banana")
println(strings.maxOrNull()) // cherry
}
```
## Variance
### Invariance
```kotlin
// List is invariant
class Container
// Container is NOT a subtype of Container
```
### Covariance (out)
Producer only - can only produce, not consume:
```kotlin
class Producer {
fun produce(): T = TODO()
}
// Producer IS a subtype of Producer
val intProducer: Producer = TODO()
val numProducer: Producer = intProducer // OK
```
### Contravariance (in)
Consumer only - can only consume, not produce:
```kotlin
class Consumer {
fun consume(item: T) {}
}
// Consumer IS a subtype of Consumer
val numConsumer: Consumer = TODO()
val intConsumer: Consumer = numConsumer // OK
```
## Type Erasure
Generic type information is erased at runtime:
```kotlin
fun main() {
val list1: List = listOf("a", "b")
val list2: List = listOf(1, 2)
// At runtime, both are just List
println(list1 is List<*>) // true
println(list2 is List<*>) // true
// Cannot do this:
// if (list is List) // Error: Cannot check specific type
}
```
### Reified Types
Use `reified` to make type available at runtime (inline functions only):
```kotlin
import kotlin.reflect.typeOf
inline fun printType() {
println(typeOf())
}
fun main() {
printType() // kotlin.String
printType() // kotlin.Int
printType
- >() // kotlin.collections.List
Comments
Comments powered by Giscus
To enable comments, add your Giscus embed code here.
Learn more about Giscus →