오늘도 더 나은 코드를 작성하였습니까?

람다로 프로그래밍 (자바 함수형 인터페이스 사용) 본문

Kotlin in Action/코틀린 기초

람다로 프로그래밍 (자바 함수형 인터페이스 사용)

hik14 2020. 8. 18. 18:29

실제로 다뤄야 되는 API 중에 상당수는 자바로 작성되어 있다.

하지만 코틀린의 람다를 이용하면 아무런 문제 없이 자바 API를 사용할 수 있다.

 

예) 자바에서 버튼에 리스너 객체를 전달하기 위해 무명 클래스의 인스턴스를 생성해서 넘겨준다.

     button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                
            }
        });
        button.setOnClickListener = { view -> }

이런 코드가 작성 가능한 이유는

OnClickListener 인터페이스가 단 하나의 추상 메서드를 가지고 있기 때문이다. 

이런 인터페이스를 SAM(Single Abstract Method) 인터페이스라고 부른다.

 

코틀린은 함수형 인터페이스를 인자로 받는 자바 메서드를 호출할 때 람다를 넘길 수 있게 해 준다. 

따라서 무명 클래스를 정의할 필요가 없으며 코드도 간결하다. 

 

컴파일러가 자동으로 무명 클래스와 객체를 생성해준다. 

 

 

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

void postponeComputation(int delay, Runable computation)
postponeComputation(1000){ println(100) }

delay = 1000, Runable 객체 대신 람다를 넘길 수 있다. 물론 코틀린도 obeject 키워드를 이용하여 무명 클래스를 넘겨줄 수 있다.

 

하지만 이 2개의 차이가 존재한다.

 

무명 클래스의 객체인 경우 메서드를 호출할 때마다 새로운 무명 클래스의 객체를 생성된다.

람다는 정의가 들어가 있는 함수의 변수에 접근하지 않는 람다에 대응하는 무명 객체를 메서드 호출시 반복 사용한다.

 

아래와 같이 작동한다. 

// 아래 처럼 람다를 함수형 인터페이스 객체를 만들어 반복 사용
val runable = Runable{ println(100) }

fun handleComputation(){
	postponeComputation(1000, runable)
}

만약 람다가 함수의 변수를 이용한다면(포획) 매 호출마다 같은 인스턴스를 사용할 수는 없다. 

id를 필드로 저장하는 새로운 Runable 객체를 만들어 전달한다. 

fun handleComputation(id: String){
	postponeComputation(1000){ println(id) }
}

 

* 컬렉션을 확장한 메서드에 람다를 넘기는 경우 코틀린은 무명 클래스를 생성 후 객체를 넘기는 방법을 사용하지 않는다.

코틀린 inline으로 표시된 코틀린 함수에게 람다를 넘기면 아무런 무명 클래스가 생성되지 않는다. 

대부분의 코틀린 확장 함수들은 inline 표기가 되어있다.

 

 

 

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

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

 

지금까지 본 위의 작업은 대부분 컴파일러가 자동으로 해주기 때문에 수동으로 변환할 필요가 없었지만, 어쩔 수 없이 수동 변환이 필요한 경우를 알아보자.

 

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

함수형 인터페이스의 인스턴스를 반환하는 메서드가 있다면 람다를 직접 반환할 수 없고, 반환하고 싶은 람다를 SAM 생성자로 감싸줘야 함.

fun createAllDoneRunnable(): Runnable{

    return Runnable { println("All Done!") } // 람다를 통한 SAM 객체 생성후 반환
   
}

fun main() {
    createAllDoneRunnable().run()
}

 

람다로 생성한 함수형 인터페이스 인스턴스를 변수에 저장하는 경우에도 SAM 생성자를 사용할 수 있다.

        val listener = View.OnClickListener { view ->

            val text = when(view.id) {
                R.id.first_button -> "first button"
                R.id.second_button -> "second button"
                else -> "Unknown button"
            }
            textView.text = text
        }


        first_button.setOnClickListener(listener)
        second_button.setOnClickListener(listener)
        
    }

 

* 람다에는 무명 객체와 달리 자신을 가리키는 this가 없다는 사실을 유의하자.

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

람다 안에서 this는 그 람다를 둘러싼 클래스의 인스턴스이다. 즉, 무명 클래스의 인스턴스와 다르다. 

 

이벤트 리스너가 이벤트를 처리하다가 자신의 리스너 등록을 해제하려면 람다를 사용하면 안 되고 무명 객체를 사용해서 리스너를 리스너를 구현해야 된다. 람다를 변환한 무명 클래스의 객체를 참조 할 수 없기 때문이다.

 

수신 객체 지정 람다

자바의 람다에는 없는 코틀린 람다의 독특한 기능을 이해하자.

수신 객체를 명시하지 않고 람다의 본문 안에서 다른 객체의 메서드를 호출할 수 있게 하는 것이다.

수신 지정 람다(lamda with receiver)라고 부른다.

 

With 함수

 

특정 객체의 이름을 반복하지 않고 그 객체에 대한 연산을 수행할 수 있다.

- 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() {
    println(alphabet())
}

알파벳 출력하기 with를 이용한 구현

fun alphabet(): String{

    val stringBuilder = StringBuilder()

    return with(stringBuilder){ // 메소드를 호출하려는 수신객체 지정(stringBuilder)
        for (letter in 'A'..'z') 
            this.append(letter) // this = stringBuilder

        append("\nnow I know the alphabet") // this 생량
        this.toString() // 블록의 마지막에 값을 반환.
    }
}

 

with는 특별한 구문처럼 보이지만 사실은 파라미터가 2개인 함수이다.

with( 수신 지정 객체 , 람다 식 ) *람다식을 받는 마지막 인자는 밖으로 빼낼 수 있다.

----> with(수신 지정 객치){... }

 

with 함수는 첫 번째로 받은 인자를 두 번째로 인자로 받은 람다의 수신 객체로 만들어 넘긴다.

두 번째 인자인 람다식에 this를 통해 첫 번째 인자에 접근 가능하다.

this. 을 생략하고도 접근 가능하다.

 

fun alphabet(): String{
    
    return with(StringBuilder()){ // 메소드를 호출하려는 수신객체를 바로 생성해서 대입.
        for (letter in 'A'..'z')
            append(letter)
        append("\nnow I know the alphabet") 
        toString() 
    }
}

 

* 메서드 이름 충돌.

 

with에게 넘긴 객체의 클래스와 with를 사용하는 코드가 들어있는 클래스 안에 이름이 같은 메서드가 있으면 무슨 일이 생길까??? 메서드를 찾을 수 없단 에러가 난다.

 

수신 객체의 메서드를 쓰려면  this.methodName()을 호출하고

외부의 메서드를 사용하려면 외부 클래스를 명시하면 된다. 

class Test {

    override fun toString(): String {
        return "this is OuterClass toString"
    }

    fun alphabet(): String {
        return with(StringBuilder()) { // 메소드를 호출하려는 수신객체를 바로 생성해서 대입.
            for (letter in 'A'..'z')
                this.append(letter)
            append("\nnow I know the alphabet")

            val str = this@Test.toString() //클래스 명시
            str
        }
    }
}

fun main() {
    println(Test().alphabet())
}

with를 반환하는 값은 람다를 실행한 결과 값이다.

람다 함수 블록의 마지막 값이 될 것이다.

 

Apply 함수

 

람다의 결과 대신 수신객체가 필요한경우.

with 함수와 매우 유사하지만 유일한 차이점은 항상 자신에게 전달된 객체(수신 객체)를 반환한다

 

apply는 확장 함수로 정의되어있다.

apply 함수는 객체의 인스턴스를 만들면서 즉시 프로퍼티중 일부를 초기화해야 되는 경우 매우 유용하다.

 

fun alphabet() = StringBuilder().apply{
    for (letter in 'A'..'Z')
        append(letter)
    append("\nnow I know the alphabet")
}.toString()

 

*builderString 함수

수신 객체는 항상 StringBuilder이다. 

StringBuilder객체를 만드는 일과 toString호출을 알아서 해준다.

따라서 String을 생성 시 매우 유용하다.

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