728x90
기존 코드와 코틀린 코드를 자연스럽게 통합하는 것은 코틀린의 핵심 목표 중 하나이다
완전히 코틀린만으로 이루어진 프로젝트 조차도 JDK나, 안드로이드 프레임워크 또는 다른 서드파티 프레임워크 등의 자바 라이브러리를 기반으로 만들어진다.
코틀린을 기존 자바 프로젝트에 통합하는 경우에는 직접 변환할 수 없거나 미처 변환하지 않은 기존 자바 코드를 처리할 수 있어야 한다
확장 함수
fun String.lastChar(): Char = this.get(this.length - 1)
//fun String.lastChar(): Char = get(length - 1) //this 생략
fun main(args: Array<String>) {
println("Kotlin".lastChar())
}
- 확장 함수를 만들려면 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이기만 하면 된다.
- 클래스의 이름을 수신 객체 타입, 확장 함수가 호출되는 대상이 되는 값(객체)을 수신 객체라고 부른다.
- 이 함수를 호출하는 구문은 다른 일반 클래스 멤버를 호출하는 구문과 같다
- String이 수신 객체 타입, "Kotlin"이 수신 객체이다
- String 클래스에 새로운 메서드를 추가하는 것 같다
- String 클래스의 소스코드를 소유한 것도 아니지만 내가 원하는 메서드를 String 클래스에 추가할 수 있다.
- 심지어 String이 자바나 코틀린 어떤 언어로 작성됐는지는 중요하지 않다(JVM 언어로 작성된 클래스면 확장 가능)
- 일반 메서드의 본문에서 this를 사용할 때와 마찬가지로 확장 함수 본문에도 this를 사용할 수 있으며 생략도 가능하다
- 확장 함수 내부에서는 일반적인 인스턴스 메서드의 내부에서와 마찬가지로 수신 객체의 메서드나 프로퍼티를 바로 사용할 수 있다.
- 확장 함수의 캡슐화를 깨지 않고, 사용할 수 있다
- 확장 함수 안에서는 클래스의 내부에서만 사용할 수 있는 비공개 멤버나 보호된 멤버를 사용할 수 없다
임포트와 확장 함수
확장 함수를 정의했다고 해도 자동으로 프로젝트 안의 모든 소스코드에서 그 함수를 사용할 수 있지는 않다
확장 함수를 사용하기 위해서는 그 함수를 다른 클래스나 함수와 마찬가지로 임포트 해야 한다.
코틀린에서는 클래스를 임포트 할 때와 동일한 구문을 사용해 개별 함수를 임포트 할 수 있다.
import ch03.strings.lastChar
import ch03.strings.lastChar as last
import ch03.strings.*
fun main(args: Array<String>) {
val c = "Kotlin".lastChar()
val tmpC = "Kotlin".last()
}
- *를 사용하거나, as 키워드를 사용해서 임포트 한 클래스나 함수를 다른 이름으로 부를 수 있다
- 한 파일 안에서 다른 여러 패키지에 속해있는 이름이 같은 함수를 가져와 사용해야 하는 경우 이름을 바꿔서 임포트 하면 이름 충돌을 막을 수 있다
- 코틀린 문법상 확장 함수는 반드시 짧은 이름을 써야 한다
- 따라서 임포트 할 때 이름을 바꾸는 것이 확장 함수 이름 충돌을 해결할 수 있는 유일한 방법이다
자바에서 확장 함수 호출
char c = StringUilKt.lastChar("JAVA")
- 내부적으로 확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메서드이다
- 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점 부가 비용이 들지 않는다
- 자바에서 호출할 때에도 단지 정적 메서드에 첫 번째 인자로 수신 객체만 넘기면 된다
- 다른 최상위 함수와 마찬가지로 확장 함수가 들어있는 자바 클래스 이름도 확장 함수가 들어있는 파일 이름에 따라 결정된다
확장 함수로 유틸리티 함수 정의
fun <T> Collection<T>.joinToString(
separator: String = ", ",
prefix: String = "",
postfix: String = ""
): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator) //첫 원소 구분자 안붙힌다
result.append(element)
}
result.append(postfix)
return result.toString()
}
fun main(args: Array<String>) {
val list = listOf(1, 2, 3)
println(list.joinToString(separator = "; ", prefix = "(", postfix = ")"))
//(1; 2; 3)
println(list.joinToString(" "))
//1 2 3
}
- Collection<T>에 대한 확장 함수를 선언한다.
- 파라미터 디폴트 값을 지정한다.
- this는 수신 객체를 가리킨다.
- 여기서는 T타입의 원소로 이뤄진 컬렉션이다
- joinToString을 마치 클래스의 멤버인 것처럼 호출할 수 있다
- 확장 함수는 단지 정적 메서드 호출에 대한 문법적인 편의일 뿐이다.(클래스가 아닌 더 구체적인 타입을 수신 객체 타입으로 지정할 수도 있다)
fun Collection<String>.join(
separator: String = ", ",
prefix: String = "",
postfix: String = ""
) = joinToString(separator, prefix, postfix)
fun main(args: Array<String>) {
println(listOf("One", "Two", "Three").join(" "))
//println(listOf(1, 2, 3).join()) // 에러
}
- 문자열 컬렉션에 대해서만 호출할 수 있는 join 함수를 정의
- int형 컬렉션에 join을 실행하면 오류가 발생
확장 함수는 오버라이드 할 수 없다
코틀린의 메서드 오버라이드는 일반적인 객체지향의 메서드 오버라이드와 마찬가지이다.
단, 확장 함수는 오버라이드는 불가능하다.
open class View{
open fun click() = println("View Click")
}
class Button : View(){
override fun click() {
println("Button Click")
}
}
fun main(args: Array<String>) {
val view: View = Button()
view.click()
}
- Button이 View의 하위 타입이기 때문에 View타입 변수를 선언해도 Button타입 변수를 그 변수에 대입할 수 있다.
- view타입 변수에 click이벤트를 동작시키게 된다면 "Button Click"인 오버라이드 된 메서드가 호출된다.
- 확장 함수는 클래스의 일부가 아니라 클래스 밖에 선언된다.
- 이름과 파라미터가 같은 확장 함수를 기반 클래스와 하위 클래스에 대해 정의해도 실제로는 확장 함수를 호출할 때 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 함수가 호출될지 결정된다.
- 그 변수에 저장된 객체의 동적인 타입에 의해 결정되지 않는다.
fun main(args: Array<String>) {
fun View.showOff() = println("I'm a View!")
fun Button.showOff() = println("I'm a Button!")
val view: View = Button()
view.showOff()
}
- view가 가리키는 객체는 Button 타입이지만, view의 타입이 View타입이기 때문에 View의 확장 함수가 호출된다.
- 확장 함수는 오버라이드 할 수 없다
ExtensionsKt.showOff(view); //이거와 같다 => static
확장 프로퍼티
확장 프로퍼티를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가할 수 있다.
프로퍼티라는 이름으로 불리기는 하지만 상태를 저장할 적절한 방법이 없기 때문에 실제로 확장 프로퍼티는 아무 상태도 가질 수 없다.
하지만 프로퍼티 문법으로 더 짧게 코드를 작성할 수 있어서 편한 경우가 있다
val String.lastChar: Char
get() = get(length - 1)
- 수신 객체 클래스가 추가되었다
- 뒤받침 하는 필드가 없어서 기본 게터 구현을 제공할 수 없으므로 최소한 게터는 꼭 정의를 해야 한다.
- 초기화 코드에서 계산한 값을 담을 장소가 없으므로 초기화 코드도 쓸 수 없다.
var StringBuilder.lastChar : Char
get() = get(length - 1)
set(value: Char){
this.setCharAt(length - 1, value)
}
fun main(args: Array<String>) {
println("Kotlin".lastChar)
val sb = StringBuilder("Kotlin?")
sb.lastChar = '!'
println(sb)
}
- 자바에서 확장프로퍼티를 사용하고 싶다면 항상 StringUtilsKt.getLastChar("JAVA")처럼 게터나 세터를 명시적으로 호출해야 한다.
728x90
'KotlinInAction > 함수 정의와 호출' 카테고리의 다른 글
| 로컬 함수와 확장 (0) | 2022.07.29 |
|---|---|
| 문자열과 정규식 다루기 (0) | 2022.07.29 |
| 컬렉션 처리 (0) | 2022.07.28 |
| 함수를 호출하기 쉽게 만들기 (0) | 2022.07.26 |
| 코틀린에서 컬랙션 만들기 (0) | 2022.07.26 |