확장은 상속이나 데코레이터 패턴 없이 기존 클래스나 인터페이스에 새로운 기능을 추가하는 Kotlin 기능이다.
즉, 실제로 클래스를 수정하지 않고도 마치 클래스 내부에 정의된 멤버 함수처럼 사용할 수 있는 함수를 만들 수 있다.
예를 들어 String 클래스에 새로운 기능을 추가할 수 있다.
fun String.lastChar(): Char {
return this[this.length - 1]
}
fun main() {
println("Kotlin".lastChar())
}
n
이 함수는 String 클래스에 정의되어 있지 않지만 마치 멤버 함수처럼 호출할 수 있다.
확장 함수는 다음과 같은 형태로 정의한다.
fun 수신자타입.함수명(...) { ... }
Ex.
fun String.lastChar(): Char {
return this[this.length - 1]
}
여기서 String을 수신자 타입(receiver type)이라고 한다.
확장 함수 내부에서 this는 확장을 호출한 객체(수신자 객체)를 가리킨다.
fun String.lastChar(): Char {
return this[this.length - 1]
}
위 코드에서 this는 "Kotlin" 같은 실제 문자열 객체를 의미한다.
확장 함수는 제네릭 타입도 지원한다.
fun <T> List<T>.second(): T {
return this[1]
}
fun main() {
val numbers = listOf(1, 2, 3)
println(numbers.second())
}
2
확장 함수는 다음과 같은 상황에서 유용하다.
라이브러리 코드를 수정할 수 없을 때 기능을 추가할 수 있다.
fun String.isEmail(): Boolean {
return contains("@")
}
기존 Java에서는 보통 다음과 같은 util 클래스를 만들었다.
StringUtils.isBlank(str)
하지만 Kotlin에서는
str.isBlank()
처럼 객체 중심 API 스타일로 사용할 수 있다.
확장 함수를 사용하면 도메인 중심 코드를 만들 수 있다.
fun LocalDate.isWeekend(): Boolean {
return this.dayOfWeek == DayOfWeek.SATURDAY || this.dayOfWeek == DayOfWeek.SUNDAY
}
사용
if (date.isWeekend()) { ... }
확장 함수는 런타임이 아닌 컴파일 시점에 호출이 결정된다.
즉, 변수의 실제 타입이 아니라 선언된 타입에 따라 결정된다.
open class Shape
class Rectangle: Shape()
fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"
fun printClassName(s: Shape) {
println(s.getName())
}
fun main() {
printClassName(Rectangle())
}
Shape
이유는 s의 타입이 Shape로 선언되어 있기 때문이다.
즉, 확장 함수는 다형성(polymorphism)을 지원하지 않는다.
클래스 내부에 동일한 이름의 멤버 함수가 존재한다면 항상 멤버 함수가 우선적으로 호출된다.
class Example {
fun printFunctionType() {
println("Class method")
}
}
fun Example.printFunctionType() {
println("Extension function")
}
fun main() {
Example().printFunctionType()
}
Class method
즉, 확장 함수는 기존 멤버 함수를 override할 수 없다.
확장 함수는 컴파일 시 static 함수로 변환된다.
Ex.
fun String.lastChar(): Char
는 실제로 다음과 유사한 코드로 컴파일된다.
public static char lastChar(String receiver)
즉, 확장 함수는...
확장 함수는 Nullable 타입에도 정의할 수 있다.
fun Any?.toStringSafe(): String {
if (this == null) return "데이터가 없습니다."
return this.toString
}
fun main() {
val name: String? = null
println(name.toStringSafe())
}
데이터가 없습니다.
이처럼 null-safe 로직을 공통화할 때 유용하다.
확장 프로퍼티도 정의할 수 있다.
val <T> List<T>.lastIndex: Int
get() = size - 1
사용
println(listOf(1, 2, 3).lastIndex)
확장 프로퍼티는 실제 필드(Backing Field)를 가질 수 없다.
다음과 같은 것은 불가능하다
val String.count = 0
이유는 확장이 기존 클래스 외부에서 정의되기 때문이다.
그래서 확장 프로퍼티는 getter / setter 만 정의할 수 있다.
동반 객체에도 확장을 정의할 수 있다.
class MyClass {
companion object
}
fun MyClass.Companion.printHello() {
println("Hello from Companion Extension!")
}
fun main() {
MyClass.printHello()
}
| 특징 | 설명 |
|---|---|
| 기존 클래스 수정 불필요 | 외부 클래스에도 기능 추가 가능 |
| 실제 멤버 함수 아님 | 컴파일 시 static 함수로 변환 |
| 정적 디스패치 | 변수 타입 기준으로 결정 |
| 멤버 함수 우선 | override 불가능 |
| nullable 확장 가능 | null-safe 로직 구현 가능 |
Kotlin의 확장 함수는 기존 클래스 구조를 변경하지 않고 기능을 확장할 수 있는 기능이다.
특히
같은 상황에서 매우 유용하게 사용할 수 있다.
다만 정적 디스패치(static dispatch)라는 특징 때문에 다형성을 기대하고 사용하면 다른 결과가 나올 수 있으므로 주의해야 한다.