ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Kotlin in Action: 5.4 자바 함수형 인터페이스 활용
    Book/Kotlin in Action 2022. 11. 18. 09:05

    코틀린 프로그래밍을 하다보면 다루는 API의 대부분이 자바로 작성된 API이다. 그래서 이번 장에서는 자바 API를 사용할 때 람다를 인자로 전달하는 방법에 대해서 자세히 알아본다.

     

    단일 추상 메소드(SAM) 인터페이스(함수형 인터페이스라고도 부른다)는 추상 메소드를 하나만 가지고 있는 인터페이스를 뜻한다. 

     

    자바 API에는 Runnable이나 Callable과 같은 함수형 인터페이스와 그런 함수형 인터페이스를 활용하는 메소드가 많다.

     

    자바8 이전의 자바에서는 메소드에게 인자로 넘기기 위해 무면 클래스의 인스턴스를 만들어야 했다.

     

     코틀린은 함수형 인터페이스를 인자로 취한 자바 메소드를 호출할 때 (무명 내부 클래스 인스턴스 대신) 람다를 넘길 수 있게 해준다. 

     

    더보기

    자바와 달리 코틀린에는 제대로 된 함수 타입이 존재한다. 따라서 코틀린에서 함수를 인자로 받을 필요가 있는 함수는 함수형 인터페이스가 아니라 함수 타입을 인자 타입으로 사용해야 한다. 코틀린 함수를 사용할 때는 코틀린 컴파일러가 코틀린 람다를 함수형 인터페이스로 변환해주지 않는다. 

     

    1. 자바 메소드에 람다를 인자로 전달.

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

    예를 들어 다음 메소드는 Runnable 타입의 파라미터를 받는다. 

    //Java
    void postponeComputation(int delay, Runnable computation);

    코틀린에서 람다를 이 함수에 넘길 수 있다. 컴파일러는 자동으로 람다를 Runnable인스턴스로 변환해준다.

    postponeComputation(1000) {println(42)}

    여기서 'Runnable 인스턴스'라는 말은 실제로는 'Runnable을 구현한 무명 클래스의 인스턴스'라는 뜻이다. 컴파일러는 자동으로 그런 무명 클래스와 인스턴스를 만들어준다. 그 무명 클래스에 있는 유일한 추상 메소드를 구현할 때 람다 본문을 메소드 본문으로 사용한다.

    여기서는 Runnable의 run이 그런 추상 메소드다.

     

    Runnable을 구현하는 무명 객체를 명시적으로 만들어서 사용할 수도 있다.

    postponeComputation(1000, object : Runnable( //객체 식을 함수형 인터페이스 구현으로 넘긴다.
    	override fun run() {
    		println(42)
    	}
    })

     

    람다와 무명 객체의 차이

    객체를 명시적으로 선언하는 경우 메소드를 호출할 때마다 새로운 객체가 생성되는 반면 람다는 정의가 들어있는 함수의 변수에 접근하지 않는 람다에 대응하는 무명 객체를 매소드를 호출할 때 마다 반복 사용한다. 프로그램 전체에서 Runnable의 인스턴스는 단 하나만 만들어진다.

     

    따라서 명시적인 object선언을 사용하면서 람다와 동일한 코드는 다음과 같아.

    이 경우 Runnable 인스턴스를 변수에 저장하고 메소드를 호출 할 때마다 그 인스턴스를 사용한다.

    vla runnable = Runnable{println(42)}//Runnable은 SAM 생성자
    fun handleComputation() {
    	postponeComputation(1000,runnable) //모든 handleComputation 호출에 같은 객체를 사용한다.
        
    }

    람다가 주면 영역의 변수를 포획한다면 매 호출마다 같은 인스턴스를 사용할 수 없다. 그런 경우 컴파일러는 매번 주변 영역의 변수를 포획한 새로운 인스턴스를 생성해준다.

     

    지금까지 살펴본 대로 대부분의 경우 람다와 자바 함수형 인터페이스 사이의 변환은 자동으로 이루어진다. 

    하지만 수동으로 변환해야 하는 경우가 있다. 그런 경우 어떻게 람다를 처리하는지 살펴보자.

     

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

     

    SAM생성자는 람다를 함수형 인터페이스의 인스턴스로 변환할 수 있게 컴파일러가 자동으로 생성한 함수다. 컴파일러가 자동으로 람다를 함수형 인터페이스 무명 클래스로 바꾸지 못하는 경우 SAM 생성자를 사용할 수 있다.

     

    예를 들어 함수형 인터페이스의 인스턴스를 반환하는 메소드가 있다면 람다를 직접 반환할 수 없고, 반환하고픈 람다를 SAM 생성자로 감싸야 한다.

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

     

    SAM 생성자의 이름은 사용하려는 함수형 인터페이스의 이름과 같다. SAM 생성자는 그 함수형 인터페이스의 유일한 추상 메소드 본문에 사용할 람다만을 인자로 반앋서 함수형 인터페이스를 수현하는 클래스의 인스턴스를 반환한다.

     

    람다로 생성한 함수형 인터페이스 인스턴스를 변수에 저장해야 하는 경우에도 SAM 생성자를 사용할 수 있다. 여러 버튼에 같은 리스너를 적용하고 싶다면 다음 리스트처럼 SAM 생성자를 통해 람다를 함수형 인터페이스 인스턴스로 만들어서 변수에 저장해 활용할 수 있다.

    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)

    OnClickListener를 구현하는 객체 선언을 통해 리스너를 만들 수도 있지만, SAM 생성자를 쓰는 쪽이 더 간결하다.

     

    더보기

    람다를 변환한 무명클래스의 인스턴스를 참조할 방법이 없다.

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

    오버로드한 메소드 중에서 어떤 타입의 메소드를 선택해 람다를 변환해 넘겨줘야 할지 모호할 떄가 있다. 그런 경우 명시적으로 SAM 생성자를 적용하면 컴파일 오류를 피할 수 있다. 

Designed by Tistory.