5장 람다로 프로그래밍
5.1 람다 식과 멤버 참조
람다 소개: 코드 블록을 함수 인자로 넘기기
button.setOnClickListener { /* 클릭시 수행할 동작 */ }
람다 식의 문법
val sum = { x: Int, y: Int -> x + y }
run { println(42) }
- 람다 식은 중괄호로 둘러싸여 있다.
- 식이기 때문에 변수에 저장할 수 있다.
run
함수를 통해 인자로 받은 람다를 실행할 수 있다.- 실행 시점에서 람다 호출에는 아무 부가 비용이 들지 않는다.
data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31))
people.maxBy({ p: Person -> p.age }) // (1)
people.maxBy() { p: Person -> p.age } // (2)
people.maxBy { p: Person -> p.age } // (3)
people.maxBy { p -> p.age } // (4)
people.maxBy { it.age } // (5)
people.maxBy(Person::age) // (6)
- 파라미터로 전달하는 람다식은 여러 가지 방법으로 기술 가능하다.
- (1): 인자로 람다를 전달하는 정석적인 방법이다.
- (2): 마지막 인자가 람다인 경우 람다를 괄호 밖으로 이동할 수 있다.
- (3): 괄호안에 명시할 인자가 없는 경우 생략할 수 있다.
- (4): 컴파일러의 타입 추론으로 람다의 파라미터 타임은 생략 가능하다.
- (5): 디폴트 파라미터
it
을 사용하면 더 간단해진다. - (6): 멤버 참조를 이용해도 된다.
현재 영역에 있는 변수에 접근
fun printProblemCounts(responses: Collection<String>) {
var clientErrors = 0
var serverErrors = 0
responses.forEach {
if (it.startsWith("4") {
clientErrors++
} else if (it.startsWith("5")) {
serverErrors++
}
}
println("${clientErrors} client errors, ${serverErrors} server errors")
}
- 람다는 현재 영역에 있는 변수에 접근 가능하다.
- 자바와 달리 람다에서
final
변수도 접근 가능하다. - 람다 안에서 사용하는 외부 변수를 람다가 포획(capture)한 변수라고 부른다.
멤버 참조
val getAge = Person::age
fun salute() = println("Salute!")
run(::salute) // Salute!
- 이미 선언된 함수와 동일한 기능을 하는 람다가 필요한 경우, 멤버 참조를 이용가능하다.
::
를 사용하영 멤버를 참조한다.- 멤버 참조는 프로퍼티나 메소드를 단 하나만 호출하는 함수 값을 만들어준다.
- 멤버 참조를 통해 확장 함수에 대한 참조도 얻을 수 있다.
5.2 컬렉션 함수형 API
아래의 API들은 자바에서 제공하는 API와 사용성이 같다.
- filter, map
- any, all, count, find
- groupBy
- flatMap, flatten
5.3 지연 계산(lazy) 컬렉션 연산
일반적으로 컬렉션 함수는 결과 컬렉션을 즉시(eagerly) 생성한다. 이로 인해 연쇄된 컬렉션 함수 체인에서는 매 단계마다 임시로 계산된 중간 결과 컬렉션을 생성하게 된다. 시퀀스(sequence)를 사용하면 중간 임시 컬렉션 없이 컬렉션 연산을 연쇄할 수 있다.
시퀀스 연산 실행: 중간 연산과 최종 연산
listOf(1, 2, 3, 4)
.map { print("map($it) "); it * it }
.filter { print("filter($it) "); it % 2 == 0 }
// map(1) map(2) map(3) map(4) filter(1) filter(4) filter(9) filter(16)
listOf(1, 2, 3, 4).asSequence()
.map { print("map($it) "); it * it }
.filter { print("filter($it) "); it % 2 == 0 }
.toList()
// map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16)
- 컬렉션에
asSequence()
를 호출해 시퀀스를 만들 수 있다. - 시퀀스는 자바의
stream
처럼 중간 연산과 최종 연산으로 나뉜다 - 중간 연산은 다른 시퀀스를 반환하며 항상 지연 계산된다.
- 최종 연산은 연기됐던 모든 계산을 수행시켜 수행한 결과를 얻는다.
- 최종 연산을 호출하지 않으면 중간 연산도 수행되지 않는다.
위 예제의 로깅을 통해 시퀀스를 사용하지 않았을 때와 시퀀스를 사용했을 때의 연산 순서를 확인할 수 있다.
여러 CPU에서 병렬적으로 실행하는 기능이 필요하다면 자바의 parallelStream
을 사용해야 한다.
시퀀스 만들기
val naturalNumbers = generateSequence(0) { it + 1 }
val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
println(numbersTo100.sum()) // 5050
generateSequence
함수를 통해 시퀀스를 만들 수 있다.- 첫번째 인자로 지정할 값, 두번째 인자로 다음 원소를 계산하는 방법을 명시한다.
5.4 자바 함수형 인터페이스 활용
자바 메서드에 람다를 인자로 전달
/* java */
void postponeComputation(int delay, Runnable computation);
/**
* 코틀린에서 사용
* 마지막 인자가 함수형 인터페이스므로 괄호 바깥에 람다로 전달
* 프로그램 전체에서 Runnable의 인스턴스는 단 하나만 만들어진다.
*/
postponeComputation(1000) { println(42) }
/**
* 무명 객체를 사용하여 명시적으로 객체를 선언
* 메서드가 호출될 때 마다 새로운 객체가 생성된다.
*/
postponeComputation(1000, object : Runnable {
override fun run() {
println(42)
}
}
/**
* 주변 영역의 변수를 포획한다면
* 컴파일러는 매번 해당 변수를 포함한 인스턴스를 매번 새로 생성한다.
*/
fun handleComputation(id: String) {
/**
* 람다 안에서 handleComputation method 의 id 값을 captor 했기에
* 호출 때마다 새로 Runnable 인스턴스를 생성한다.
*/
postponeComputation(1000) { println(id) }
}
- 자바의 함수형 인터페이스는 코틀린 람다 또는 무명 객체로 대체된다.
SAM 생성자: 람다를 함수형 인터페이스로 명시적으로 변경
fun createAllDoneRunnable(): Runnable {
return Runnable { print("All done!") }
}
createAllDoneRunnable().run() // All done!
- SAM 생성자는 람다를 함수형 인터페이스의 인스턴스로 변환할 수 있게 컴파일러가 자동으로 생성한 함수이다.
- 컴파일러가 자동으로 람다를 함수형 인터페이스 무명 클래스로 바꾸지 못하는 경우 SAM 생성자를 사용한다.
- SAM 생성자의 이름은 사용하려는 함수형 인터페이스의 이름과 같다.
5.5 수신 객체 지정 람다: with과 apply
수신 지정 객체 람다(lambda with receiver)
: 수신 객체를 명시하지 않고 람다에서 다른 객체의 메서드를 호출할 수 있게 한 람다.
with 함수
fun alphabet() = with(StringBuilder()) {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
toString()
}
with
함수는 첫번째 인자로 객체(수신 객체)를, 두번째 인자로 람다를 받는다.- 이때
with
함수로 전달한 객체는 람다의 파라미터로 전달된다. - 람다 내에서
this
키워드를 사용해서 인자로 받은 객체의 프로퍼티나 메서드에 접근이 가능하다. 이때this
키워드는 생략 가능하다.- 람다 외부에 동일한 이름의 메서드가 있을 때
this
를 사용해서 구분할 수 있다.
- 람다 외부에 동일한 이름의 메서드가 있을 때
with
의 반환값은 람다의 결과이다.
apply 함수
fun alphabet = StringBuilder().apply {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}.toString()
apply
는with
과 비슷하나 결과로 수신 객체를 반환한다.apply
는 확장 함수로 정의되어 있다.
fun alphabet() = buildString {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}
- 표준 라이브러리인
buildString
을 사용하여 간결화 할 수 있다. - 알아서
StringBuilder
객체 생성과toString
을 호출해준다. buildString
인자는 수신 객체 지정 람다이며, 수신 객체는 항상StringBuilder
이다