코틀린에서는 무명 클래스 인스턴스 대신 람다를 넘길 수 있다.
button.setOnClickListener { view -> ... }
OnClickListener를 구현하기 위해 사용한 람다에는 view라는 파라미터가 있다. view의 타입은 View다. 이는 onClick 메소드의 인자 타입과 같다.
public interface OnClickListener {
void onClick(View v); // -> { view -> ... }
}
이런 코드가 동작하는 이유는 OnClickListener에 추상 메소드가 단 하나만 있기 때문이다. 이렇게 추상 메소드가 단 하나만 있는 인터페이스를 함수형 인터페이스(functional interface) 또는 SAM 인터페이스(Single Abstract Method interface)라고 한다.
자바 API에는 Runnable이나 Callable과 같은 함수형 인터페이스와 그런 함수형 인터페이스를 활용하는 메소드가 많다. 코틀린은 함수형 인터페이스를 인자로 취하는 자바 메소드를 호출할 때 람다로 넘길 수 있게 해준다. 따라서 코틀린 코드는 무명 클래스 인스턴스를 정의하고 활용할 필요가 없어서 여전히 깔끔하며 코틀린다운 코드로 남아있을 수 있다.
자바와 달리 코틀린에는 제대로 된 함수 타입이 존재한다(코틀린은 함수를 일급 시민(first-class citizen)으로 취급).
자바는 함수형 프로그래밍을 위해서 어쩔 수 없이 우회적인 방식(예, 람다를 사용하더라도 인터페이스를 활용)을 사용해야 하지만, 코틀린은 함수 자체가 완전한 타입으로 존재하여 더 강력하고 유연한 함수형 프로그래밍을 지원한다.
5.4.1 자바 메소드에 람다를 인자로 전달
함수형 인터페이스를 인자로 원하는 자바 메소드에 코틀린 람다를 전달할 수 있다. 예를 들어, 다음 메소드는 Runnable 타입의 파라미터를 받는다.
void postponeComputation(int delay, Runnable computation);
코틀린에서 람다를 이 함수에 넘길 수 있다. 컴파일러는 자동으로 람다를 Runnable 인스턴스로 변환해준다.
postponeComputation(1000) { println(42) }
Runnable을 구현하는 무명 객체를 명시적으로 만들어서 사용할 수도 있다.
postponeComputation(1000, object : Runnable { // 객체 식을 함수형 인터페이스 구현으로 넘긴다.
override fun run() {
println(42)
}
})
하지만 람다와 무명 객체 사이에는 차이가 있다. 객체를 명시적으로 선언(=무명 객체)하는 경우 메소드를 호출할 때마다 새로운 객체가 생성된다. 람다는 다르다. 정의가 들어있는 함수의 변수에 접근하지 않는 람다에 대응하는 무명 객체를 메소드를 호출할 때마다 반복 사용한다(외부 변수 미참조 시 최적화 가능).
postponeComputation(1000) { println(42) } // 프로그램 전체에서 Runnable의 인스턴스는 단 하나만 만들어진다.
따라서 명시적인 object 선언을 사용하면서 람다와 동일한 코드는 다음과 같다. 이 경우 Runnable 인스턴스를 변수에 저장하고 메소드를 호출할 때마다 그 인스턴스를 사용한다.
val runnable = Runnable { println(42) }
fun handleComputiation() {
postponeComputation(1000, runnable) // 모든 handleComputation 호출에 같은 객체를 사용한다.
}
람다가 주변 영역의 변수를 포획한다면 매 호출마다 같은 인스턴스를 사용할 수 없다. 그런 경우 컴파일러는 매번 주변 영역의 변수를 포획한 새로운 인스턴스를 생성해준다. 예를 들어, 다음 함수에서는 id를 필드로 저장하는 새로운 Runnable 인스턴스를 매번 새로 만들어 사용한다.
fun handleComputation(id: String) { // 람다 안에서 "id" 변수 포획(참조)
postponeComputation(1000) { println(id) } // handleComputation을 호출할 때마다 새로운 Runnable 인스턴스 생성
}
5.4.2 SAM 생성자: 람다를 함수형 인터페이스로 명시적으로 변경
SAM 생성자는 람다를 함수형 인터페이스의 인스턴스로 변환할 수 있게 컴파일러가 자동으로 생성한 함수다. 컴파일러가 자동으로 람다를 함수형 인터페이스 무명 클래스로 바꾸지 못하는 경우 SAM 생성자를 사용할 수 있다. 예를 들어 함수형 인터페이스의 인스턴스를 반환하는 메소드가 있다면 람다를 직접 반환할 수 없고, 반환하고픈 람다를 SAM 생성자로 감싸야 한다.
// SAM 생성자를 사용해 값 반환하기
fun createAllDoneRunnable(): Runnable {
return Runnable { println("All done!") }
}
>>> createAllDoneRunnable().run()
All done!
SAM 생성자의 이름은 사용하려는 함수형 인터페이스의 이름과 같다. SAM 생성자는 그 함수형 인터페이스의 유일한 추상 메소드의 본문에 사용할 람다만을 인자로 받아서 함수형 인터페이스를 구현하는 클래스의 인스턴스를 반환한ㄷ.
람다로 생성한 함수형 인터페이스 인스턴스를 변수에 저장해야 하는 경우에도 SAM 생성자를 사용할 수 있다. 여러 버튼에 같은 리스너를 적용하고 싶다면 다음 리스트처럼 SAM 생성자를 통해 람다를 함수형 인터페이스 인스턴스로 만들어서 변수에 저장해 활용할 수 있다.
// SAM 생성자를 사용해 listener 인스턴스 재사용하기
val listener = OnClickListener { view ->
val text = when (view.id) {
R.id.button1 -> "First button"
R.id.button2 -> "Second button"
else -> "Unknown button"
}
toast(text)
}
button1.setOnClickListener(listener)
button2.setOnClickListener(listener)
listener는 어떤 버튼이 클릭됐는지에 따라 적절한 동작을 수행한다. OnClickListener를 구현하는 객체 선언을 통해 리스너를 만들 수도 있지만 SAM 생성자를 쓰는 쪽이 더 간결하다.
람다와 리스너 등록/해제하기
람다에는 무명 객체와 달리 인스턴스 자신을 가리키는 this가 없다. 다라서 람다를 변환한 무명 클래스의 인스턴스를 참조할 방법이 없다. 컴파일러 입장에서 보면 람다는 코드 블록일 뿐이고, 객체가 아니므로 객체처럼 람다를 참조할 수는 없다. 람다 안에서 this는 그 람다를 둘러싼 클래스의 인스턴스를 가리킨다.
이벤트 리스너가 이벤트를 처리하다가 자기 자신의 리스너 등록을 해제해야 한다면 람다를 사용할 수 없다. 그런 경우 람다 대신 무명 객체를 사용해 리스너를 구현하라. 무명 객체 안에서는 this가 그 무명 객체 인스턴스 자신을 가리킨다. 따라서 리스너를 해제하는 API 함수에게 this를 넘길 수 있다.
'Kotlin' 카테고리의 다른 글
[Kotlin] @JvmOverloads (0) | 2025.03.20 |
---|---|
[Kotlin] @JvmName (0) | 2025.03.20 |
[Kotlin] 무명 객체와 객체 선언 비교 & 람다와 무명 객체 차이 (0) | 2025.03.19 |
[Kotlin in Action] 5.3. 지연 계산(lazy) 컬렉션 연산 (0) | 2025.03.19 |
[Kotlin in Action] 5.2 컬렉션 함수형 API (0) | 2025.03.18 |
댓글