[Kotlin In Action] 5장. 람다로 프로그래밍
람다 식과 멤버 참조
람다식
함수 타입에 넘길 수 있는 코드 조각.
항상 중괄호로 둘러싸여 있으며, 인자 목록 주변에 괄호가 없다.
람다에서 화살표가 인자 목록과 람다 본문을 구분한다.
{ x: Int, y: Int ‐> x + y }
람다가 마지막 인자라면 괄호 밖에 위치시킬 수도 있다.
또한 인자가 한개이고 타입 추론이 가능하다면 default 명인 it을 사용할 수 있다.
val people = listOf(Person("Alice", 20), Person("Bob", 31))
people.maxBy( { p: Person ‐> p.age } )
people.maxBy { p: Person ‐> p.age }
people.maxBy { p ‐> p.age }
people.maxBy { it.age }
현재 영역에 있는 변수에 접근
자바와 달리 final 변수가 아닌 변수에도 접근이 가능하며, 수정 또한 가능하다 !
fun printProblemCounts(response: Collection<String>) {
var clientErrors = 0
var serverErrors = 0
response.forEach {
if (it.startsWith("4")) {
// 람다 밖의 변수 접근/수정 가능
clientErrors++
} else {
}
}
}
멤버 참조
이미 선언된 함수를 값으로 사용해야 할 경우 멤버 참조를 사용한다.
val getAge = Person::age // 멤버 참조, 확장 함수도 동일
getAge(somePerson)
run(::salute) // 최상위 함수 참조
// Person::age(person)는 안 됨
fun sendMail(p: Person, msg: String): Unit = ...
val nextAction = ::sendMail // (p: Person, msg: String): Unit 타입 함수
val createPerson = ::Person // 생성자 참조
코틀린 1.1부터는 바운드 멤버 참조도 가능하다.
(멤버 참조를 생성할 때 클래스 인스턴스를 함께 저장하기 때문에 객체를 넘길 필요가 없어짐)
val p = Person("Dmitry", 34)
val ageFunc = p::age
println(ageFunc())
컬렉션 함수형 API
filter와 map
- filter : 조건을 충족하는 것만 걸러냄
- filterNot 도 사용 가능
- map : 값을 변환해서 새로운 컬렉션 생성
list.filter { it % 2 == 0}
list.map { it * it }
all, any, find(null 가능 타입, 조건 충족하는 첫번째 원소)로 조건 검사를 걸 수 있으며 count로 갯수를 셀 수 있다.
groupBy
group을 키 값으로, 이에 해당하는 객체 list들을 값으로 가진 map을 반환한다.
val strs = listOf("12", "345", "11", "456")
val grouped: Map<Int, List<String>> = strs.groupBy { it.length }
flatMap과 flatten
인자로 주어진 람다를 컬렉션 모든 객체에 적용하고, 적용한 결과로 얻어지는 여러 리스트를 한 리스트로 모은다.
val books = listOf(
Book("소설", listOf("작가1", "작가2")),
Book("시집", listOf("작가3", "작가1")))
val authors = books
.flatMap { it.authros }
.toSet()
지연 계산 lazy 컬렉션 연산
map과 filter는 즉시 연산을 수행한다.
즉 계산 중간 결과를 바로 새로운 컬렉션에 임시로 담는다.
원소 개수가 많고 변환 연산이 많으면 성능상 주의가 필요하다.
list.filter { it % 2 == 0 } // 새로운 컬렉션 생성
.map { it * 2 } // 새로운 컬렉션 생성
자바의 스트림과 동일하게 시퀀스를 사용해서 최종 연산에 대해서만 결과를 생성하게 할 수 있다.
people.asSequence()
.map(Person::name)
.filter { it.startsWith("A") }
.toList() // 다시 콜렉션으로 변환
자바 함수형 인터페이스 활용
인터페이스에 추상 메서드가 하나인 경우 함수형 인터페이스 혹은 sam 인터페이스라고 한다.
함수형 인터페이스를 인자로 받는 자바의 메서드를 호출할 때 람다를 넘길 수 있다.
이는 컴파일러가 알아서 무명 클래스와 인스턴스를 만들어주기 때문이다.
// java 코드
void postponeComputation(int delay, Runnable computation)
postponeComputation(10) { println(42) }
postponeComputation(10, object: Runnable { // 객체 식을 함수형 인터페이스 구현으로 넘김
override fun run() {
println(42)
}
})
// SAM 생성자 : 람다를 함수형 인터페이스의 인스턴스로 변환
val comp: Runnable = Runnable { println(42) }
람다와 무명 객체
// 무명 객체 : 메소드 호출할때마다 새로운 객체 생성
postponeComputation(1000, object: Runnable {
override fun run() {
println(42)
}
})
val runnable = Runnable { println(42) }
fun handleComputation() {
postponeComputation(1000, runnable) // 모든 handleComputation 호출에 같은 객체를 사용
}
만약 여기서 람다가 바깥 변수를 포획한다면 매 호출마다 새로운 인스턴스를 생성한다.
SAM(Single Abstract Method)
자바로 작성된 메서드가 하나의 인터페이스를 구현할 때는 좀 더 간편하게 대신 함수를 작성할 수 있는데 이를 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)
수신 객체 지정 람다: with와 apply
람다 내부에서 수신 객체를 지정하지 않고, 람다 바디 안에서 다른 객체의 메서드를 호출할 수 있게 하는 람다를 수신 객체 지정 람다라고 한다.
with
첫 번째 파라미터로 받은 객체를 두 번째 파라미터로 받은 람다의 수신 객체로 만든다.
마지막 반환 값이 람다의 결과이다.
fun alphabet(): String {
val sb = StringBuffer()
return with(sb) {
('A'..'Z').forEach { ch ‐> this.append(ch) }
append("\nNow I know tthe alphabet!") // this를 생략해도 sb.append 호출
this.toString() // this는 with의 첫 번째 인자로 전달한 sb
}
}
>>> println(alphabet())
ABCDEFGHIJKLMNOPQRSTUVWXYZ
// 일케도 쓸 수 있음
fun alphabet(): String {
val stringBuilder = StringBuilder()
return with(stringBuilder) {
for (letter in 'A'..'Z') {
this.append(letter)
}
append("\nNow I know the alphabet!")
this.toString()
}
}
apply
반환 값이 자신을 전달한 수신 객체라는 것만 빼면 with와 거의 동일하다.
fun alphabet() = StringBuilder().apply {
for (letter in 'A'..'Z'){
append(letter)
}
append("\nNow I know the alphabet!")
}
apply는 확장 함수로 사용된다.