기본적인 내용은 생략한다.
키워드 #
- 람다, 멤버 참조
- sequence (지연 컬렉션 연산)
- 함수형 인터페이스 (SAM 인터페이)
- 수신 객체 지정 람다
+ 참고 : 스마트 캐스트는 val(불변)일 때에만 사용 가능하다.
5장. 람다로 프로그래밍 #
람다(= 람다 식)은 기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각을 뜻한다.
5.1 람다 식과 멤버 참조 #
5.1.1 람다 소개 : 코드 블록을 함수 인자로 넘기기 #
button.setOnClickListener {
/* 클릭 시 수행할 동작 */
}
5.1.2 람다와 컬렉션 #
자바에서는 필요한 컬렉션 기능을 직접 작성하곤 했다. 코틀린에서는 (람다가 있기 때문에)이런 습관을 버려야 한다.
// 람다를 활용한다.
people.maxBy { it.age } // 나이로 비교하여, 가장 큰 원소를 찾는다.
// 멤버 참조를 활용한다.
people.maxBy(Person::age) // 나이로 비교하여, 가장 큰 원소를 찾는다.
코틀린 람다 식은 항상 ‘중괄호’로 둘러싸여 있다.
인자 목록 주변에 괄호가 없기도 하다. (-> 로 인자 목록과 본문을 구분할 수 있기 때문이다.)
{ (a, b) -> ...}
가 아니고,{ a, b -> ... }
로 작성한다.
코틀린에서 함수 호출 시 맨 뒤에 있는 인자가 람다 식이면, 그 람다를 괄호 밖으로 빼낼 수 있다.
따라서, 괄호 뒤에 람다 식을 둘 수 있다.
// 개선 전
people.maxBy({ p: Person -> p.age })
// 개선 후
people.maxBy() { p: Person -> p.age }
(위와 같이) 람다가 함수의 유일한 인자이고, 괄호 뒤에 람다를 썼다면 호출 시 빈 괄호를 없앨 수 있다.
people.maxBy { p: Person -> p.age }
람다의 파라미터명을 디폴트 파라미터명 it
로 사용할 수 있다.
people.maxBy { it.age }
5.1.4 현재 영역에 있는 변수에 접근 #
람다의 파라미터뿐만 아니라, 람다 정의 앞에 선언된 로컬 변수까지 람다에서 모두 사용할 수 있다.
fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
messages.foreach {
println("$prefix $it") // prefix 변수를 사용할 수 있다.
}
}
(자바와 다른 점은) 람다 안에서 파이널 변수가 아닌 변수에도 접근할 수 있다.
- 람다 안에서 바깥의 변수를 변경할 수도 있다.
fun printProblemCounts(responses: Collection<String>) {
var clientErros = 0
var serverErros = 0
responses.forEach {
if(it.startWith("4")) {
clientErros++
}
else {
serverErros++;
}
}
}
위와 같이 람다 내부에서 사용하는 외부 변수를 ‘람다가 포획(capture)한 변수’라고 부른다.
내부적으로 아래와 같이 동작한다.
- 파이널 변수를 포획한 경우 : 람다 코드를 변수 값과 함께 저장한다.
- 일반 변수를 포획한 경우 : 변수를 특별한 래퍼(
Ref
)로 감싸서 나중에 변경하거나 읽을 수 있게 한다. (래퍼를 참조하는 것)
컴파일된 코드를 한번 살펴보자.
// (1)
class Ref<T>(var value: T)
val counter = Ref(0)
val inc = { counter.value++ }
// (2)
var counter = 0
val inc = { counter++ }
(2)
코드의 내부 원리가 (1)
이다.
" 한 가지 꼭 알아둬야 할 함정이 있다. 람다를 이벤트 핸들러나 다른 비동기적으로 실행되는 코드로 활용하는 경우 함수 호출이 끝난 다음에 로컬 변수가 변경될 수도 있다. “
5.1.5 멤버 참조 #
람다와 같이 코드를 인자로 넘길 수 있다. 다만 함수로 생성이 되어 있는 코드를 넘기려면 어떻게 해야할까?
멤버 참조를 통해 해결할 수 있다.
val getAge = Person::age
// val getAge = { person:Person -> person.age } 와 같다.
::
를 사용하는 식을 멤버 참조 라고 부른다.
- 멤버 참조는 프로퍼티나 메서드를 ‘단 하나’만 호출하는 함수 값으로 만들어준다.
- 멤버 참조 뒤에는
()
를 붙여서는 안된다.
최상위에 선언된 함수/프로퍼티도 참조할 수 있다.
fun salute() = println("salute")
run(::salute) // 최상위 함수를 호출한다.
생성자 참조(constructor reference)를 사용하면 클래스 생성 작업을 연기하거나 저장해둘 수 있다.
:: 뒤에 클래스 이름을 넣으면 생성자 참조를 만들 수 있다.
val createPerson = ::Person
createPerson("alice", 29)
확장함수도 멤버 함수와 똑같은 방식으로 참조할 수 있다.
fun Person.isAudit() = age >= 21
val predicate = Person::isAudit
바운드 멤버 참조
코틀린 1.0에서는 클래스의 메서드, 프로퍼티에 대한 참조를 얻은 다음에 그 참조를 호출할 때 항상 인스턴스 객체를 제공해야 했다.
코틀린 1.1부터는 바운드 멤버 참조(bound member reference)를 지원한다. 바운드 멤버 참조를 사용하면 멤버 참조를 생성할 때 클래스 인스턴스를 함께 저장한 다음 나중에 그 인스턴스에 대해 멤버를 호출한다.
val p = Person("Dmitry", 34)
val personAgeFunction = Person::age
println(personAgeFunction(p)) // 인스턴스를 넘긴다.
val dmitryAgeFunction = p::age // 인스턴스를 함께 저장한다.
println(dmitryAgeFunction()) // 인스턴스를 넘기지 않는다.
5.2 컬렉션 함수형 API #
5.2.1 필수적인 함수 : filter
, map
#
생략
5.2.2 all, any, count, find : 컬렉션에 술어 적용 #
all : 모든 술어를 만족하는지 확인한다.
val canBeInClub27 = { p: Person -> p.age <= 27 }
people.all(list)
any : 하나라도 만족하는지 확인한다.
val canBeInClub27 = { p: Person -> p.age <= 27 }
people.any(list)
count : 술어를 만족하는 원소의 개수를 센다.
val canBeInClub27 = { p: Person -> p.age <= 27 }
people.count(list)
find : 술어를 만족하는 원소를 찾는다.
- 가장 먼저 발견된 원소 ‘하나’를 반환한다.
- 발견되는 원소가 없다면
null
을 반환한다.
val canBeInClub27 = { p: Person -> p.age <= 27 }
people.find(list)
5.2.3 groupBy: 리스트를 여러 그룹으로 이뤄진 맵으로 변경 #
컬렉션의 모든 원소를 어떤 특성에 따라 여러 그룹으로 나눌 때 사용할 수 있다.
val people = listOf(...)
println(people.groupBy { it.age })
// {
// 29 = [Person(...), Person(...)],
// 31 = [Person(...), Person(...)]
// }
5.2.4 flatMap, flatten : 중첩된 컬렉션 안의 원소 처리 #
flatMap : 주어진 람다를 컬렉션의 모든 객체에 적용하고(또는 매핑하고), 람다를 적용한 결과로 얻어지는 여러 리스트를 한 리스트로 모은다.
- 즉, map → flat 한다?
val strings = listOf("abc", "def")
strings.flatMap { it.toList() } // [a, b, c, d, e, f]
// 1. string -> list mapping
// 2. flat
val books = lostOf(Book(...), Book(...), Book(...), ...)
books.flatMap { it.authors }.toSet()
// [길동, 철수, ...]
flatten : (flatMap과 같이 매핑할 게 없고) 펼치기(flat)만 필요하다면 flatten 을 사용한다.
5.3 지연 계산(lazy) 컬렉션 연산 #
위에서 살펴본 map, filter 등의 연산은 즉시(eager)
연산한다.
= 체이닝 연산을 할 때 계산의 중간 결과를 새로운 컬렉션에 담는다는 의미이다.
시퀀스(sequence
) 를 사용하면 중간의 임시 컬렉션을 사용하지 않고도 컬렉션 연산을 체이닝(연쇄)할 수 있다.
- 시퀀스의 원소는 필요할 때 비로소 계산된다.
asSequence
확장 함수를 호출하면 어떤 컬렉션이든 시퀀스로 바꿀 수 있다.
아래 예시를 살펴보자.
참고로, map, filter 는 연산 후 리스트를 반환한다.
people.map(Person::name).filter { it.startsWith("A") }
위 코드는 리스트를 2개 만든다.
원소가 많을 수록, 체이닝이 많을 수록 문제가 심해진다.
이를 더 효율적으로 개선하기 위해서 시퀀스를 사용해볼 수 있다.
people.asSequence()
.map(Person::name)
.filter { it.startsWith("A") }
.toList()
위 코드는 중간 결과를 저장하는 컬렉션이 생기지 않기 때문에, 성능이 눈에 띄게 좋아진다.
코틀린 지연 계산 시퀀스는 Sequence
인터페이스에서 시작한다.
- 이 인터페이스는 단지 한 번에 하나씩 열거될 수 있는 원소의 시퀀스를 표현한다.
Sequence
안에는iterator
라는 단 하나의 메서드가 있다.- 이 메서드를 통해 시퀀스로부터 원소 값을 얻을 수 있다.
왜 시퀀스를 다시 리스트로 변경해야할까?
꼭 변경하지 않아도 된다.
이터레이터를 통해 접근해도 된다면 시퀀스를 그대로 써도 된다.
하지만 대부분의 경우 인덱스를 통해 접근하거나 다른 기능들을 사용하기 편리한 리스트를 다루기 때문에 대부분의 예시에서 변경하는 것이다.
” 큰 컬렉션에 대해서 연산을 체이닝할 때는 시퀀스를 사용하는 것을 규칙으로 삼아라 “
5.3.1 시퀀스 연산 실행: 중간 연산과 최종 연산 #
Java Stream 쪽 내용과 비슷해서 생략한다.
5.3.2 시퀀스 만들기 #
asSequence()
generateSequence()
생략
5.4 자바 함수형 인터페이스 활용 #
코틀린은 ‘함수형 인터페이스’를 인자로 취하는 자바 메서드를 호출할 때 람다를 넘길 수 있게 해준다.
” 자바와 달리 코틀린에서는 제대로 된 함수 타입이 존재한다. 따라서 코틀린에서 함수를 인자로 받을 필요가 있는 함수는 함수형 인터페이스가 아니라 함수 타입을 인자 타입으로 사용해야 한다. 다만, 컴파일러가 이 함수 타입을 함수형 인터페이스로 변환해주지는 않는다. (이에 대한 내용은 8.1 절에서 자세하게 다룬다.) “
5.4.1 자바 메서드에 람다를 인자로 전달 #
함수형 인터페이스를 인자로 원하는 자바 메서드에 코틀린 람다(혹은 익명객체)를 전달할 수 있다.
void postponeComputation(int delay, Runnable computation);
postponeComputation(1000) { println(42) }
postponeComputation(1000, object: Runnable {
override fun run() {
println(42)
}
})
람다와 익명객체 사이에는 차이가 있다.
- 익명 객체 : 메서드를 호출할 때마다 새로운 객체가 생성된다.
- 람다 : (내부적으로 생성한 익명 객체를) 재사용한다.
- 다만, 주변 영역의 변수를 포획한다면 매 호출마다 같은 인스턴스를 사용할 수 없기 때문에 새로 객체를 생성한다.
위 코드에서 postponeComputation(1000) { println(42) }
코드는 다음과 동일하다.
val runnable = Runnable { println(42) }
postponeComputation(1000, runnable)
람다의 자세한 구현
코틀린 1.0에서 인라인(inline) 되지 않은 모든 람다 식은 무명 클래스로 컴파일된다. (?)
생략
- 코틀린
inline
으로 표시된 코틀린 함수에게 람다를 넘기면 아무런 무명 클래스도 만들어지지 않는다.- 대부분의 코틀린 확장 함수는
inline
표시가 붙어 있다.
inline 함수에 대해 찾아볼 것
5.4.2 SAM 생성자: 람다를 함수형 인터페이스로 명시적으로 변경 #
‘SAM 생성자’에 대해 찾아볼 것
5.5 수신 객체 지정 람다 : with 와 apply #
생략
5.6 요약 #
생략