KotlinInAction/람다로 프로그래밍

자바 함수형 인터페이스 활용

webmaster 2022. 8. 3. 01:35
728x90

코틀린 람다를 자바  API에서 활용할 수 있는지 살펴보자

자바 8 이전의 자바에서는 무명 클래스의 인스턴스를 만들어 메서드에 전달해 주어야 했다

추상 메서드가 단 하나만 존재하는 인터페이스를 함수형 인터페이스, SAM 인터페이스(단일 추상 메서드)라고 부른다.

코틀린은 함수형 인터페이스를 인자로 취하는 자바 메서드를 호출할 때 람다를 넘길 수 있게 해 준다(무명 클래스 인스턴스를 정의하고 활용할 필요가 없어졌다)

자바 메서드에 람다를 인자로 전달

함수형 인터페이스를 인자로 원하는 자바 메서드에 코틀린 람다를 전달할 수 있다.

fun postponeComputation(delay: Int, computation: Runnable){
    computation.run()
}

fun main(args: Array<String>) {
    postponeComputation(1000, {println(42)}) //프로그램 전체에서 Runaable 인스턴스는 단 하나만 만들어진다
    postponeComputation(1000, object: Runnable{ //객체식을 함수형 인터페이스 구현으로 넘긴다.
        override fun run() {
            println(42)
        }
    })
}
  • 컴파일러가 자동으로 람다를 Runnable 인스턴스로 변환해 준다.
    • Runnable 인스턴스는 실제 Runnable 인터페이스를 구현한 무명클래스의 인스턴스이다
  • 무명 클래스에 있는 유일한 추상메서드를 구현할 때 람다 본문을 메서드 본문으로 사용한다
  • 람다와 무명 객체 사이에는 차이가 있는데 객체를 명시적으로 선언하는 경우 메서드를 호출할 때마다 새로운 객체가 생성되지만, 람다는 정의가 들어있는 함수의 변수에 접근하지 않는 람다에 대응하는 무명 객체를 메서드를 호출할 때마다 반복 사용한다.
fun main(args: Array<String>) {
    handleComputation()
}
val runnable = Runnable { println(42) } //전역변수로 컴파일되므로 프로그램 안에 하나의 인스턴스만 존재

fun handleComputation() {
    postponeComputation(1000, runnable)//모든 handleComputation 호출에 같은 객체를 사용
}
  • Runnable 인스턴스를 변수에 저장하고, 메서드를 호출할 때마다 그 인스턴스를 사용한다.
fun handleComputation(id: String) { //람다 안에서 id 변수를 포획
    postponeComputation(1000) { println(id) } //handleComputation 을 호출할 때마다 새로 Runnable 인스턴스를 만든다
}
  • 람다가 주변 영역의 변수를 포획한다면 매 호출마다 같은 인스턴스를 사용할 수 없다.
  • 그럴 경우 컴파일러는 매번 주변 영역의 변수를 포획한 새로운 인스턴스를 생성해준다.
  • 해당 예제에서는 id를 필드로 저장하는 새로운 Runnable 인스턴스를 매번 새로 만들어 사용한다.
  • 컬렉션을 확장한 메서드에 람다를 넘기는 경우 무명 클래스를 인스턴스를 생성해서 넘기는 방식을 사용하지 않는다(8장에서 배운다)
  • inline으로 표시된 코틀린 함수에게 람다를 넘기면 아무런 무명 클래스도 만들어지지 않는다(코틀린 대부분을 확장 함수는 inline표시가 붙어있다), 8장에서 배운다.

TIP : 람다의 자세한 구현

코틀린 1.0에서 인라인 되지 않은 모든 람다 식은 무명클래스로 컴파일된다.

람다가 변수를 포획하면 무명클래스안에 포획한 변수를 저장하는 필드가 생기며, 매 호출마다 그 무명 클래스의 인스턴스를 새로 만들지만 포획한 변수가 없는 람다에서는 인스턴스가 하나만 생긴다. 람다가 선언된 함수 이름을 접두사로 하는 이름이 람다를 컴파일한 클래스에 붙게 된다. 코틀린 1.5부터는 코틀린에서 정의한 함수형 인터페이스를 JVM8 이상의 백엔드 타깃으로 빌드하는 경우 invokeddynamic을 사용한 람다로 변환되며, LambdaMetafactory.metafactory()를 사용한다.

람다를 함수형 인터페이스로 명시적으로 변경(SAM 생성자)

SAM 생성자는 람다를 함수형 인터페이스의 인스턴스로 변환할 수 있게 컴파일러가 자동으로 생성한 함수다.

컴파일러가 자동으로 람다를 함수형 인터페이스 무명 클래스로 바꾸지 못하는 경우 SAM 생성자를 사용할 수 있다. 예를 들어 함수형 인터페이스의 인스턴스를 반환하는 메서드가 있다면 람다를 직접 반환할 수 없고, 반환하고자 하는 람다를 SAM 생성자로 감싸야한다.

 

fun main(args: Array<String>) {
    createAllDoneRunnable().run()
}
fun createAllDoneRunnable(): Runnable {
    return Runnable { println("All done!") }
}
  • SAM 생성자의 이름은 사용하려는 함수형 인터페이스의 이름과 같다.
  • SAM 생성자는 그 함수형 인터페이스의 유일한 추상 메서드의 본문에 사용할 람다만을 인자로 받아 함수형 인터페이스를 구현하는 클래스의 인스턴스를 반환한다.
val listener = OnClickListener{view -> 
	val text = when(view.id){//view.id를 사용해 어떤 버튼이 클릭됐는지 판단
    	R.id.button1 -> "First Button"
        R.id.button2 -> "Second Button"
        else -> "Unknown button"
    }
    toast(text)//text의 값을 사용자에게 보여줌
}

fun main(args: Array<String>) {
	button1.setOnClickListener(listener)
    button2.setOnClickListener(listener)
}
  • 람다로 생성한 함수형 인터페이스 인스턴스를 변수에 저장해야 하는 경우에도 SAM 생성자를 사용할 수 있다.
  • 여러 버튼에 같은 리스너를 적용하고 싶다면 SAM 생성자를 통해 람다를 함수형 인터페이스 인스턴스로 만들어서 변수에 저장해 활용할 수 있다.
    • listener는 어떤 버튼이 클릭됬는지에 따라 적절한 동작을 수행
  • 함수형 인터페이스를 요구하는 메서드를 호출할때  가끔 오버 로드한 메서드 중에서 어떤 타입의 메서드를 선택해 람다를 변환해줘야 할지 모호한 경우가 있는데 그럴 경우 명시적으로 SAM 생성자를 적용하여 컴파일 오류를 피할 수 있다.

TIP : 람다와 리스너 등록/해제

람다에는 무명 객체와 달리 인스턴스 자신을 가리키는 this가 없다는 사실에 유의하자 (람다를 변환한 무명 클래스의 인스턴스를 참조할 방법이 없다) 람다 안에서 this는 그 람다를 둘러싼 클래스의 인스턴스를 가리킨다.

이벤트 리스너가 이벤트를 처리하다가 자기 자신의 리스너 등록을 해제해야 한다면 람다를 사용할 수 없다. 그런 경우 람다 대신 무명 객체를 사용해 리스너를 구현하자. 무명 객안에서는 this가 그 무명 객체 인스턴스 자신을 가리킨다. 따라서 리스너를 해제하는 API함수에게 this를 넘길 수 있다.

728x90

'KotlinInAction > 람다로 프로그래밍' 카테고리의 다른 글

수신 객체 지정 람다 (with와 apply)  (0) 2022.08.03
지연 계산 컬렉션 연산  (0) 2022.08.02
컬렉션 함수형 API  (0) 2022.08.01
람다 식과 멤버 참조  (0) 2022.07.31