본문으로 바로가기
반응형


이 글은 Kotlin In Action을 참고 하였습니다.

더욱 자세한 설명이나 예제는 직접 책을 구매하여 확인 하시기 바랍니다

5.4 자바의 functional interface 호출

자바에는 이미 많은 functional interface들이 존재합니다.
Runnable, Callable, onClickListener등 기존부터 존재하던것 들과 Java8 부터 Predicate, Function, Supply, Consumer들 50개가 넘는 functional interface이 추가되었습니다.
코틀린에서 이런 함수들을 호출할때 바로 람다식을 사용할수 있습니다.
물론 자바에서 처럼 익명 클래스를 사용해도 됩니다. (코틀린에서는 object를 사용하면 되겠죠?)
변환은 컴파일러가 알아서 합니다~

5.4.1 코틀린에서 자바의 functional interface 호출

자바에서 이미 함수의 인자로 functional interface를 사용하고 있다면, 코틀린에서도 해당 함수를 어렵지 않게 호출 할 수 있습니다.

 // 자바
void post(int delay, Runnable run);
void setOnClickListener(OnClickListener listener);

// 코틀린
post(1000) { println("1000 delay") }
setOnClickListener{view -> println(view.id)}

물론 익명 클래스를 이용해서 자바함수를 호출 할 수 있습니다.

post(1000, object : Runnable {
    override fun run() {
        println("wow~")
   }
})

위 경우 post 함수를 호출할때 마다 Runnable 객체를 생성해서 사용합니다.

좀 비효율적이죠?

하지만 lambda를 직접 넣어주는경우 한번만 객체를 생성한 후 재사용합니다. 훨신 효율적입니다.

val runnable = Runnable { println("lambda good!")} // 전역변수로 컴파일되므로 객체 하나만 생성된다.

fun handleComputation() {
    post(1000, runnable) 
}

컴파일하면 위 코드처럼 생성됩니다.

다만 lambda 내부에서 외부 변수를 참조하는 경우 lambda capturing이 진행되면서 해당 변수를 lambda에서 포함해야 합니다.

따라서 이 경우에는 매번 객체가 생성되어 사용됩니다.

fun handleComputation(msg: String) {
    post(1000, println(msg) )
}
위 코드에서는 handleComputation이 호출될때 마다 lambda를 치환하기 위한 익명 클래스가 생성됩니다.

정리하자면!!
  • 코틀린에서 자바의 functional interface를 호출시 람다식으로 바로 표현할 수 있다.
  • 내부적으로 람다식은 익명클래스로 치환된다.
  • lambda capturing이 발생하지 않는다면, 익명클래스는 한번만 생성되어 재사용된다.
  • lambda capturing이 발생하면, 익명클래스는 매번 생성되어 사용된다.

추가적으로 코틀린의 확장함수들은 대부분 lambda expression을 인자로 받습니다.
이 함수들은 대부분 inline function으로 정의되어 있기 때문에, 객체를 생성하여 사용하지 않습니다.
(inline function은 컴파일시 해당 코드가 호출부분으로 그대로 복사되어 들어갑니다.)
inline function은 추후에 설명합니다.


5.4.2 SAM (Single Abstract Method) 생성자: 람다를 함수형 인터페이스로의 명시적변형

SAM 생성자는 컴파일러가 람다식을 자바의 functional interface로 자동으로 변환하는 함수 입니다.
컴파일러가 자동으로 변환을 못하는경우에는 직접 SAM 생성자를 사용하여 코드를 작성 할 수도 있습니다.

fun createAllDoneRunnable(): Runnable {
    return Runnable { println("All done!") }
}

fun main(args: Array) {
    createAllDoneRunnable().run()
}


createAllDonRunnable()은 Runnable 객체 자체를 반환하는 함수 입니다.

따라서 이때는 SAM 생성자를 사용하여 Runnable을 반환할 수 있습니다.

fun createAllDoneRunnable(): Runnable {
    return object : Runnable { 
        override fun run() {
            println("All done!") 
        }
    }
}

위 코트처럼 직접 object를 이용하여 익명함수를 반환하는것 보다는 훨씬 간결합니다.

SAM 생성자의 이름은 사용하려는 함수형 interface와 같습니다.

람다식으로 제공된 부분을 abstract 함수에 넣고 객체를 반환합니다.

또한 함수형 인터페이스의 인스턴스를 변수에 저장하고 써야하는 경우에도 SAM 생성자를 이용할 수 있습니다.


※ 람다의 listener 등록 / 해제

람다 내부에서 this를 사용하면, 해당 this는 람다를 둘러싸고 있는 외부 객체를 가르킵니다.

컴파일러 입장에서 람다는 코드의 조각을분 객체가 아니기 때문입니다.

이와 다르게 익명클래스 내부에서 this는 익명클래스 자체를 가르킵니다.

이는 익명클래스 자체가 인스턴스가 되기 때문입니다.


Event listener가 이벤트를 처리하고 나서 자기 자신을 해제해야 하는 경우 람다식으로 작성 되었다면 this를 사용할 수 없습니다.

따라서 이경우 익명(무명)클래스를 사용해서 구현하고 this를 이용해서 자기 자신을 해제 하도록 해야 합니다.


5.5 Lambda with receiver (수신 객체 지정 람다) - with, apply

with와 apply를 람다를 이용하여 매우 편리하게 쓸수있는 코틀린에서 제공하는 API입니다.
이 두개의 API는 수신객체를 지정하지 않고 특정 객체를 람다 내부에서 쉽게 접근할 수 있도록 합니다.

5.5.1 with

fun alphabet(): String {
    val result = StringBuilder()
    for (letter in 'A'..'Z') {
         result.append(letter)
    }
    result.append("\nNow I know the alphabet!")
    return result.toString()
}

fun main(args: Array) {
    println(alphabet())
}


위 함수는 ABC...XYZ를 출력하는 코드 입니다.

StringBuilder를 이용했고, append를 이용하여 글자를 붙여 나갑니다.

이때 StringBuilder를 with를 이용하여 사용하면, 람다 내부에서 StringBuilder 참조없이 관련 함수를 호출할 수 있습니다.

fun alphabet(): String {
    val stringBuilder = StringBuilder()
    return with(stringBuilder) {
        for (letter in 'A'..'Z') {
            this.append(letter)   //this로 수신객체를 표현
        }
        append("\nNow I know the alphabet!") // this 없이도 호출 가능
        this.toString()
    }
}

fun main(args: Array) {
    println(alphabet())
}


with()의 param으로 넘겨준 객체는 람다 내부에서 this로 접근하여 사용할수 있으며, this를 빼고 호출해도 됩니다.

여기서 중요한 점은 람다의 맨 마지막 값이 return된다는 점입니다.


with는 사실 parameter가 두개인 함수 입니다.

with(stringBuilder, {람다식}) 형태이지만 가독성을 위해 with() {..}로 표현합니다.


만약 람다 내부에서 외부 class의 같은 이름 함수를 호출하려면 this@클래스명.함수() 로 호출할 수 있습니다.

class OuterClass {   
    
    fun alphabet() = with(StringBuilder()) {
        for (letter in 'A'..'Z') {
            append(letter)
        }
        append("\nNow I know the alphabet!")
        println(this@OuterClass.toString())
        toString()
    }
}

fun main(args: Array) {    
    println(OuterClass().alphabet())
}


5.5.2 apply

apply 함수는 with와 거의 동일한 기능을 제공합니다만 아래 정의한 부분에서 차이가 있습니다.

  • 객체의 확장 함수로 동작한다
  • return값은 객체 자신이다 (람다 내부의 마지막 값이 아님)
fun alphabet() = StringBuilder().apply {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
}.toString()

fun main(args: Array) {
    println(alphabet())
}

with와 똑같은 값을 반환하는 코드 입니다.

apply 함수는 인스턴스를 만들면서 특정 property중 일부를 초기화 하는 경우에 사용합니다.

또한 builder pattern도 apply를 이용해서 훨신 간단하게 만들 수 있습니다. (물론 with로 만들수도 있습니다.)
fun makePeople() {
    People p = People().apply {
        name = "임꺽정"
        age = 40
        tall = 190
    }


추가적으로 StringBuilder를 더 축약시켜서 쓸수 있는 buildString이란 api를 제공합니다.

fun alphabet() = buildString {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
}

fun main(args: Array) {
    println(alphabet())
}

buildString은 내부적으로 StringBuilder를 생성하고, toString()을 자동호출 합니다.


반응형