코틀린에서 람다를 활용하면 간결한 코드를 작성할 수 있다. 하지만 우리가 사용하는 많은 API는 자바로 작성되어 있다. 그리고 다행스럽게도 자바 API 호출에 여전히 람다를 사용할 수 있다.
어떻게 그것이 가능할까?
Button 클래스는 setOnClickListener 메서드를 사용해 버튼의 리스너를 설정한다.
이때 인자의 타입은 OnClickListener다.
public class Button {
public void setOnClickListener(OnClickListener l) { ... }
}
OnClickListener 인터페이스는 아래처럼 onClick 메서드 하나만 선언된 인터페이스이다.
public interface OnClickListener {
void onClick(View v);
}
자바에서는 setOnClickListener 메서드에 인자를 넘기기 위해 아래처럼 무명 클래스 객체를 만들어야 했다.
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
...
}
}
하지만 코틀린에서는 무명 클래스 객체 대신 람다를 넘길 수 있다!
button.setOnClickListener { view: View -> ... }
람다는 추상 메서드 onClick과 같은 변수 타입을 갖는다.
이런 코드가 동작하는 이유는 OnClickListener에 추상 메서드가 하나만 있기 때문이다. 이러한 인터페이스를 함수형 인터페이스 or SAM(Single Abstract Method) 인터페이스라고 한다.
자바에는 함수형 인터페이스 타입의 인자를 받는 메서드가 많다.
그리고 코틀린에서는 이러한 메서드에 무명 클래스 객체 대신 람다를 넘길 수 있기 때문에 깔끔하고 코틀린스러운 코드를 작성할 수 있다.
자바 메서드에 람다를 전달했을 때 어떤 일이 벌어지기에 그것이 가능한 걸까?
람다를 전달하면 컴파일러는 자동으로 람다를 함수형 인터페이스를 구현한 무명 클래스 인스턴스로 변환한다.
그리고 람다 본문은 무명 클래스의 추상 메서드 본문으로 활용된다. 예시를 하나 더 살펴보자.
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
Runnable 인터페이스의 run이 그러한 추상 메서드이며,
void postponeComputation(int delay, Runnable computation);
postponeComputation(1000) { println(42) }
여기서는 람다식 { println(42) }이 run의 본문으로 컴파일된다.
하지만 람다와 무명 클래스 인스턴스 사이에는 차이가 있다.
postponeComputation(1000, object : Runnable {
override fun run() {
println(42)
}
}
예시처럼 무명 객체를 명시적으로 선언하여 함수를 호출하면, 호출할 때마다 새로운 Runnable 객체가 생성된다.
하지만 람다는 경우에 따라 차이가 있다.
위 예시처럼 람다가 변수를 포획하지 않으면 람다에 대응하는 무명 클래스의 인스턴스가 단 하나만 생성된다.
postponeComputation(1000) { println(42) }
따라서, 같은 함수를 여러번 호출 하더라도 Runnable 무명 객체는 하나만 생성된다.
fun handleComputation(id: String) {
postponeComputation(1000) { println(id) }
}
반면 람다가 변수를 포획하는 경우, 람다로 호출하더라도 매번 포획한 변수 필드(프로퍼티)를 갖는 인스턴스가 새로 생성된다.
오브코스 그렇지 않다! 코틀린 inline으로 표시된 코틀린 함수에 람다를 넘기면 무명 객체가 생성되지 않는다.
대부분의 코틀린 표준 라이브러리의 확장 함수들은 inline 키워드가 붙어 있다.
아래는 List의 map 함수인데, fun 앞에 inline을 확인할 수 있다.
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}
이에 대한 더 자세한 내용은 Kotlin In Action 8장에서 다루고 있다.
람다를 자동으로 람다를 자동으로 함수형 인터페이스의 무명 인스턴스로 변환하지 못하는 상황이 있을 수 있다. 예시를 보자.
fun createAllDoneRunnable(): Runnable {
return object : Runnable {
override fun run() {
println("All done!")
}
}
}
위 함수는 Runnable 타입을 반환한다. 그리고 본문에서는 람다를 반환하지 않고 있다.
코틀린 컴파일러는 코틀린 함수를 사용할 때는 람다를 SAM 무명 인스턴스로 변환해주지 않는다.
왜냐면 코틀린에는 함수 타입이 별도로 존재하기 때문이다. 따라서, 함수형 인터페이스 타입을 반환해야 한다면, SAM 생성자를 사용한다.
fun createAllDoneRunnable(): Runnable {
return Runnable { println("All done!") }
}
이전 예시를 SAM 생성자를 사용하는 방식으로 변경한 것이다.
SAM 생성자는 람다를 함수형 인터페이스 객체로 변환할 수 있도록 컴파일러가 자동으로 제공하는 함수다.
함수 이름은 인터페이스의 이름과 같으며, 추상 메서드 본문에 사용될 람다만을 인자로 받는다.
만약 람다를 사용할 수 없는 상황이라면 무명 객체를 선언하는 대신 SAM 생성자를 사용하는 것이 더 간결해보인다.
람다는 무명 객체와 달리 람다 내부에서 this로 자신을 참조할 수 없다. 왜냐면 컴파일러 입장에서 람다는 코드 블록에 불과하기 때문이다.
만약 리스너를 등록하고 코드 마지막에 자신을 등록 해제해야 하는 경우, this로 자신을 전달하기 위해 람다 대신 무명 인스턴스를 사용해야 한다.